From 16df493ef901e7b186e87e8609fafce5fba104be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=91=D0=B0=D0=B1=D1=8B=D1=88=D0=B5=D0=B2=20=D0=9C=D0=B0?= =?UTF-8?q?=D0=BA=D1=81=D0=B8=D0=BC?= Date: Sat, 28 May 2022 01:37:22 +0300 Subject: [PATCH 01/22] Setups release generation --- .gitignore | 1 + app/build.gradle | 3 +++ 2 files changed, 4 insertions(+) diff --git a/.gitignore b/.gitignore index 4227450..7c823c7 100644 --- a/.gitignore +++ b/.gitignore @@ -269,3 +269,4 @@ fabric.properties # End of https://www.toptal.com/developers/gitignore/api/android,androidstudio,kotlin,windows /keystore.properties +/app/release/ diff --git a/app/build.gradle b/app/build.gradle index ea24768..5a57d11 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -54,6 +54,9 @@ android { freeCompilerArgs += "-Xcontext-receivers" } applicationVariants.all { variant -> + variant.outputs.all { + outputFileName = "DarkApp.apk" + } variant.sourceSets.java.each { it.srcDirs += "build/generated/ksp/${variant.name}/kotlin" } From aa39a687dc6ee7aaafe5a1016b5b7ad2ccd268d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=91=D0=B0=D0=B1=D1=8B=D1=88=D0=B5=D0=B2=20=D0=9C=D0=B0?= =?UTF-8?q?=D0=BA=D1=81=D0=B8=D0=BC?= Date: Sun, 29 May 2022 18:09:36 +0300 Subject: [PATCH 02/22] Use Glide to load images from network/cache --- app/build.gradle | 10 +++ .../dark/presentation/extra/ImageHelper.kt | 89 +------------------ .../maxb/dark/presentation/extra/Observe.kt | 1 - .../implementation/ImagesRepositoryImpl.kt | 12 +-- .../implementation/ProfileRepositoryImpl.kt | 1 - .../RecognitionTasksRepositoryImpl.kt | 5 +- .../repository/interfaces/ImagesRepository.kt | 2 + .../network/dark/AuthInterceptor.kt | 5 +- .../repository/network/dark/DarkService.kt | 4 +- .../network/dark/DarkServiceImpl.kt | 15 +++- .../network/dark/routes/RecognitionTask.kt | 11 ++- .../repository/room/dao/UserDAO.kt | 2 +- .../view/AddRecognitionTaskFragment.kt | 22 ++--- .../view/RecognitionTaskListFragment.kt | 11 ++- .../view/SolveRecognitionTaskFragment.kt | 18 ++-- .../view/adapter/ImagePreloader.kt | 36 ++++++++ .../view/adapter/ImageSliderAdapter.kt | 21 ++--- .../adapter/RecognitionTaskListAdapter.kt | 40 +++++---- .../presentation/viewModel/AuthViewModel.kt | 1 - .../viewModel/RecognitionTaskListViewModel.kt | 4 + .../SolveRecognitionTaskViewModel.kt | 4 + .../viewModel/utils/ItemHolder.kt | 7 +- .../RecognitionTaskOperationsKtTest.kt | 3 +- 23 files changed, 152 insertions(+), 172 deletions(-) create mode 100644 app/src/main/java/lab/maxb/dark/presentation/view/adapter/ImagePreloader.kt diff --git a/app/build.gradle b/app/build.gradle index 5a57d11..440a8d0 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -137,6 +137,16 @@ dependencies { // Desugaring (Time-related features support for API 21+) coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5' + // Images + def glide_version = "4.13.0" + implementation "com.github.bumptech.glide:glide:$glide_version" + annotationProcessor "com.github.bumptech.glide:compiler:$glide_version" + implementation "com.github.bumptech.glide:okhttp3-integration:4.11.0" + implementation ("com.github.bumptech.glide:recyclerview-integration:4.11.0") { + // Excludes the support library because it's already included by Glide. + transitive = false + } + // Tests testImplementation 'junit:junit:4.13.2' testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.2' diff --git a/app/src/main/java/lab/maxb/dark/presentation/extra/ImageHelper.kt b/app/src/main/java/lab/maxb/dark/presentation/extra/ImageHelper.kt index 655b645..329cd5c 100644 --- a/app/src/main/java/lab/maxb/dark/presentation/extra/ImageHelper.kt +++ b/app/src/main/java/lab/maxb/dark/presentation/extra/ImageHelper.kt @@ -2,84 +2,25 @@ package lab.maxb.dark.presentation.extra import android.content.Context import android.content.Intent -import android.graphics.Bitmap -import android.graphics.BitmapFactory import android.net.Uri import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import okhttp3.MediaType.Companion.toMediaTypeOrNull import okhttp3.MultipartBody import okhttp3.RequestBody.Companion.toRequestBody -import okhttp3.ResponseBody import org.koin.core.annotation.Single -private fun getContentResolver(context: Context) = context.applicationContext.contentResolver - -private fun bitmapFromUri(uri: Uri, - context: Context, - opts: BitmapFactory.Options): Bitmap? - = BitmapFactory.decodeFileDescriptor( - getContentResolver(context).openFileDescriptor( - uri, "r" - )?.fileDescriptor, - null, - opts -) - fun Uri.takePersistablePermission(context: Context) { - getContentResolver(context).takePersistableUriPermission(this, + context.applicationContext.contentResolver.takePersistableUriPermission(this, Intent.FLAG_GRANT_READ_URI_PERMISSION) } -fun Uri.toBitmap( - context: Context, - reqWidth: Int, - reqHeight: Int -): Bitmap? - // First decode with inJustDecodeBounds=true to check dimensions - = try {BitmapFactory.Options().run { - inJustDecodeBounds = true - bitmapFromUri(this@toBitmap, context, this) - - // Calculate inSampleSize - inSampleSize = calculateInSampleSize(this, reqWidth, reqHeight) - - // Decode bitmap with inSampleSize set - inJustDecodeBounds = false - - bitmapFromUri(this@toBitmap, context, this) ?: return null - }} catch (exception: Throwable) { null } - -private fun calculateInSampleSize(options: BitmapFactory.Options, - reqWidth: Int, - reqHeight: Int): Int { - // Raw height and width of image - val (height: Int, width: Int) = options.run { - outHeight to outWidth - } - var inSampleSize = 1 - - if (height > reqHeight || width > reqWidth) { - - val halfHeight: Int = height / 2 - val halfWidth: Int = width / 2 - - // Calculate the largest inSampleSize value that is a power of 2 and keeps both - // height and width larger than the requested height and width. - while (halfHeight / inSampleSize >= reqHeight && halfWidth / inSampleSize >= reqWidth) { - inSampleSize *= 2 - } - } - - return inSampleSize -} - @Single class ImageLoader(context: Context) { private var context = context.applicationContext suspend fun fromUri(uri: Uri): MultipartBody.Part = withContext(Dispatchers.IO) { - val contentResolver = getContentResolver(context) + val contentResolver = context.applicationContext.contentResolver val content = contentResolver.openInputStream(uri)!!.readBytes() return@withContext MultipartBody.Part.createFormData( "file", @@ -90,30 +31,4 @@ class ImageLoader(context: Context) { ) ) } - - suspend fun fromResponse(body: ResponseBody?, filename: String): String { - body ?: return "" - - val path = "image_$filename" - - return withContext(Dispatchers.IO) { - try { - body.byteStream().use { input -> - context.openFileOutput(path, Context.MODE_PRIVATE).use { output -> - val buffer = ByteArray(4 * 1024) - var read: Int - while (input.read(buffer).also { read = it } != -1) - output.write(buffer, 0, read) - output.flush() - } - } - return@withContext Uri.fromFile( - context.getFileStreamPath(path) - ).toString() - } catch (e: Exception){ - e.printStackTrace() - } - "" - } - } } \ No newline at end of file diff --git a/app/src/main/java/lab/maxb/dark/presentation/extra/Observe.kt b/app/src/main/java/lab/maxb/dark/presentation/extra/Observe.kt index 0738975..c412eb5 100644 --- a/app/src/main/java/lab/maxb/dark/presentation/extra/Observe.kt +++ b/app/src/main/java/lab/maxb/dark/presentation/extra/Observe.kt @@ -5,7 +5,6 @@ import androidx.lifecycle.* import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineStart import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch import kotlin.coroutines.CoroutineContext diff --git a/app/src/main/java/lab/maxb/dark/presentation/repository/implementation/ImagesRepositoryImpl.kt b/app/src/main/java/lab/maxb/dark/presentation/repository/implementation/ImagesRepositoryImpl.kt index 29c19c5..5fb8a8a 100644 --- a/app/src/main/java/lab/maxb/dark/presentation/repository/implementation/ImagesRepositoryImpl.kt +++ b/app/src/main/java/lab/maxb/dark/presentation/repository/implementation/ImagesRepositoryImpl.kt @@ -23,14 +23,7 @@ class ImagesRepositoryImpl( @OptIn(ExperimentalCoroutinesApi::class) private val imageResource = StaticResource().apply { fetchRemote = { - try { - Image(imageLoader.fromResponse( - mDarkService.downloadImage(it), - it - ), it) - } catch (e: NullPointerException) { - null - } + Image(it, it) } localStore = { mImageDao.save(it.toImageDTO()) @@ -49,5 +42,8 @@ class ImagesRepositoryImpl( override suspend fun getById(id: String) = imageResource.query(id) + override fun getUri(path: String) + = mDarkService.getImageSource(path) + override suspend fun delete(id: String) = mImageDao.delete(id) } \ No newline at end of file diff --git a/app/src/main/java/lab/maxb/dark/presentation/repository/implementation/ProfileRepositoryImpl.kt b/app/src/main/java/lab/maxb/dark/presentation/repository/implementation/ProfileRepositoryImpl.kt index e318209..7bdd517 100644 --- a/app/src/main/java/lab/maxb/dark/presentation/repository/implementation/ProfileRepositoryImpl.kt +++ b/app/src/main/java/lab/maxb/dark/presentation/repository/implementation/ProfileRepositoryImpl.kt @@ -1,7 +1,6 @@ package lab.maxb.dark.presentation.repository.implementation import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.flow.* import lab.maxb.dark.domain.model.Profile import lab.maxb.dark.presentation.extra.UserSettings diff --git a/app/src/main/java/lab/maxb/dark/presentation/repository/implementation/RecognitionTasksRepositoryImpl.kt b/app/src/main/java/lab/maxb/dark/presentation/repository/implementation/RecognitionTasksRepositoryImpl.kt index fcc5bed..c51a564 100644 --- a/app/src/main/java/lab/maxb/dark/presentation/repository/implementation/RecognitionTasksRepositoryImpl.kt +++ b/app/src/main/java/lab/maxb/dark/presentation/repository/implementation/RecognitionTasksRepositoryImpl.kt @@ -102,7 +102,10 @@ class RecognitionTasksRepositoryImpl( it.id = mDarkService.addImage( taskLocal.id, imageLoader.fromUri(it.path.toUri()) - )!! + )!!.also { id -> + it.id = id + it.path = id + } imagesRepository.save(it) RecognitionTaskImageCrossref( taskLocal.id, it.id, diff --git a/app/src/main/java/lab/maxb/dark/presentation/repository/interfaces/ImagesRepository.kt b/app/src/main/java/lab/maxb/dark/presentation/repository/interfaces/ImagesRepository.kt index 8317f45..0d73176 100644 --- a/app/src/main/java/lab/maxb/dark/presentation/repository/interfaces/ImagesRepository.kt +++ b/app/src/main/java/lab/maxb/dark/presentation/repository/interfaces/ImagesRepository.kt @@ -1,10 +1,12 @@ package lab.maxb.dark.presentation.repository.interfaces +import com.bumptech.glide.load.model.GlideUrl import kotlinx.coroutines.flow.Flow import lab.maxb.dark.domain.model.Image interface ImagesRepository { suspend fun save(image: Image) suspend fun getById(id: String): Flow + fun getUri(path: String): GlideUrl suspend fun delete(id: String) } diff --git a/app/src/main/java/lab/maxb/dark/presentation/repository/network/dark/AuthInterceptor.kt b/app/src/main/java/lab/maxb/dark/presentation/repository/network/dark/AuthInterceptor.kt index 7c70379..88d2819 100644 --- a/app/src/main/java/lab/maxb/dark/presentation/repository/network/dark/AuthInterceptor.kt +++ b/app/src/main/java/lab/maxb/dark/presentation/repository/network/dark/AuthInterceptor.kt @@ -9,9 +9,12 @@ import org.koin.core.annotation.Single class AuthInterceptor( private val userSettings: UserSettings ): Interceptor { + val header = "Authorization" + val value get() = "Bearer ${userSettings.token}" + override fun intercept(chain: Interceptor.Chain): Response = with(chain.request().newBuilder()) { - addHeader("Authorization", "Bearer ${userSettings.token}") + addHeader(header, value) chain.proceed(build()) } } diff --git a/app/src/main/java/lab/maxb/dark/presentation/repository/network/dark/DarkService.kt b/app/src/main/java/lab/maxb/dark/presentation/repository/network/dark/DarkService.kt index 0ab9be4..b2d7a38 100644 --- a/app/src/main/java/lab/maxb/dark/presentation/repository/network/dark/DarkService.kt +++ b/app/src/main/java/lab/maxb/dark/presentation/repository/network/dark/DarkService.kt @@ -1,9 +1,9 @@ package lab.maxb.dark.presentation.repository.network.dark +import com.bumptech.glide.load.model.GlideUrl import lab.maxb.dark.domain.model.User import lab.maxb.dark.presentation.repository.network.dark.model.* import okhttp3.MultipartBody -import okhttp3.ResponseBody interface DarkService { @@ -13,7 +13,7 @@ interface DarkService { suspend fun markTask(id: String, isAllowed: Boolean): Boolean suspend fun solveTask(id: String, answer: String): Boolean suspend fun addImage(id: String, filePart: MultipartBody.Part): String? - suspend fun downloadImage(path: String): ResponseBody? + fun getImageSource(path: String): GlideUrl suspend fun getUser(id: String): User? suspend fun login(request: AuthRequest): AuthResponse suspend fun signup(request: AuthRequest): AuthResponse diff --git a/app/src/main/java/lab/maxb/dark/presentation/repository/network/dark/DarkServiceImpl.kt b/app/src/main/java/lab/maxb/dark/presentation/repository/network/dark/DarkServiceImpl.kt index 49ef73b..3826dde 100644 --- a/app/src/main/java/lab/maxb/dark/presentation/repository/network/dark/DarkServiceImpl.kt +++ b/app/src/main/java/lab/maxb/dark/presentation/repository/network/dark/DarkServiceImpl.kt @@ -1,5 +1,7 @@ package lab.maxb.dark.presentation.repository.network.dark +import com.bumptech.glide.load.model.GlideUrl +import com.bumptech.glide.load.model.LazyHeaders import com.google.gson.GsonBuilder import lab.maxb.dark.BuildConfig import lab.maxb.dark.presentation.repository.network.dark.model.AuthRequest @@ -42,9 +44,16 @@ class DarkServiceImpl( api.addImage(id, filePart) } - override suspend fun downloadImage(path: String) = catchAll { - api.downloadImage(path) - } + override fun getImageSource(path: String) + = GlideUrl( + "${BuildConfig.DARK_API_URL}/task/image/$path", + LazyHeaders.Builder() + .addHeader(authInterceptor.header, authInterceptor.value) + .build() + ) + +// api.downloadImage(path) + override suspend fun getUser(id: String) = catchAll { api.getUser(id) diff --git a/app/src/main/java/lab/maxb/dark/presentation/repository/network/dark/routes/RecognitionTask.kt b/app/src/main/java/lab/maxb/dark/presentation/repository/network/dark/routes/RecognitionTask.kt index 2a14cbc..2258a9c 100644 --- a/app/src/main/java/lab/maxb/dark/presentation/repository/network/dark/routes/RecognitionTask.kt +++ b/app/src/main/java/lab/maxb/dark/presentation/repository/network/dark/routes/RecognitionTask.kt @@ -4,7 +4,6 @@ import lab.maxb.dark.presentation.repository.network.dark.model.RecognitionTaskC import lab.maxb.dark.presentation.repository.network.dark.model.RecognitionTaskFullViewDTO import lab.maxb.dark.presentation.repository.network.dark.model.RecognitionTaskListViewDTO import okhttp3.MultipartBody -import okhttp3.ResponseBody import retrofit2.http.* interface RecognitionTask { @@ -39,11 +38,11 @@ interface RecognitionTask { @Part filePart: MultipartBody.Part ): String? - @Streaming - @GET("$path/image/{path}") - suspend fun downloadImage( - @Path("path") path: String - ): ResponseBody? +// @Streaming +// @GET("$path/image/{path}") +// suspend fun downloadImage( +// @Path("path") path: String +// ): ResponseBody? companion object { const val path = "/task" diff --git a/app/src/main/java/lab/maxb/dark/presentation/repository/room/dao/UserDAO.kt b/app/src/main/java/lab/maxb/dark/presentation/repository/room/dao/UserDAO.kt index 295bc54..fc2d972 100644 --- a/app/src/main/java/lab/maxb/dark/presentation/repository/room/dao/UserDAO.kt +++ b/app/src/main/java/lab/maxb/dark/presentation/repository/room/dao/UserDAO.kt @@ -1,7 +1,7 @@ package lab.maxb.dark.presentation.repository.room.dao import androidx.room.* -import androidx.room.OnConflictStrategy.* +import androidx.room.OnConflictStrategy.IGNORE import kotlinx.coroutines.flow.Flow import lab.maxb.dark.presentation.repository.room.model.UserDTO diff --git a/app/src/main/java/lab/maxb/dark/presentation/view/AddRecognitionTaskFragment.kt b/app/src/main/java/lab/maxb/dark/presentation/view/AddRecognitionTaskFragment.kt index 3691b8d..a4bf3c6 100644 --- a/app/src/main/java/lab/maxb/dark/presentation/view/AddRecognitionTaskFragment.kt +++ b/app/src/main/java/lab/maxb/dark/presentation/view/AddRecognitionTaskFragment.kt @@ -11,8 +11,10 @@ import android.widget.AutoCompleteTextView import android.widget.Toast import androidx.activity.addCallback import androidx.activity.result.contract.ActivityResultContracts +import androidx.core.net.toUri import androidx.core.view.isVisible import androidx.fragment.app.Fragment +import com.bumptech.glide.Glide import lab.maxb.dark.R import lab.maxb.dark.databinding.AddRecognitionTaskFragmentBinding import lab.maxb.dark.domain.operations.unicname @@ -21,7 +23,6 @@ import lab.maxb.dark.presentation.extra.delegates.viewBinding import lab.maxb.dark.presentation.extra.goBack import lab.maxb.dark.presentation.extra.launch import lab.maxb.dark.presentation.extra.observe -import lab.maxb.dark.presentation.extra.toBitmap import lab.maxb.dark.presentation.view.adapter.ImageSliderAdapter import lab.maxb.dark.presentation.view.adapter.InputListAdapter import lab.maxb.dark.presentation.viewModel.AddRecognitionTaskViewModel @@ -45,20 +46,15 @@ class AddRecognitionTaskFragment : Fragment(R.layout.add_recognition_task_fragme } private fun setupImageUploadPanel() = with (mBinding) { - mImagesAdapter = ImageSliderAdapter() + val mGlide = Glide.with(this@AddRecognitionTaskFragment) + mImagesAdapter = ImageSliderAdapter { + mGlide.load(it.toUri()) + } imageSlider.adapter = mImagesAdapter - +// imageSlider.set() mImagesAdapter.preloader mViewModel.images observe { uris -> - uris.mapNotNull { - it.value.toBitmap( - requireContext(), - imageSlider.layoutParams.width, - imageSlider.layoutParams.height, - )?.let { image -> - it.map { uri -> - uri.toString() to image - } - } + uris.map { + it.map { uri -> uri.toString() } }.also { mImagesAdapter.submitList(it) } diff --git a/app/src/main/java/lab/maxb/dark/presentation/view/RecognitionTaskListFragment.kt b/app/src/main/java/lab/maxb/dark/presentation/view/RecognitionTaskListFragment.kt index aae852b..40d22e4 100644 --- a/app/src/main/java/lab/maxb/dark/presentation/view/RecognitionTaskListFragment.kt +++ b/app/src/main/java/lab/maxb/dark/presentation/view/RecognitionTaskListFragment.kt @@ -4,7 +4,8 @@ import android.os.Bundle import android.view.View import androidx.core.view.isVisible import androidx.fragment.app.Fragment -import androidx.recyclerview.widget.LinearLayoutManager +import com.bumptech.glide.Glide +import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions.withCrossFade import lab.maxb.dark.R import lab.maxb.dark.databinding.RecognitionTaskListFragmentBinding import lab.maxb.dark.domain.model.RecognitionTask @@ -27,12 +28,14 @@ class RecognitionTaskListFragment : Fragment(R.layout.recognition_task_list_frag mViewModel.isTaskCreationAllowed observe { mBinding.fab.isVisible = it } - mBinding.fab.setOnClickListener { v -> + mBinding.fab.setOnClickListener { RecognitionTaskListFragmentDirections.navToAddTaskFragment().navigate() } - - mAdapter = RecognitionTaskListAdapter() + mAdapter = RecognitionTaskListAdapter(Glide.with(this)) { + load(mViewModel.getImage(it)).transition(withCrossFade()) + } mBinding.recognitionTaskListRecycler.adapter = mAdapter + mBinding.recognitionTaskListRecycler.addOnScrollListener(mAdapter.preloader.raw) mAdapter.onElementClickListener = { _: View, task: RecognitionTask -> RecognitionTaskListFragmentDirections.navToSolveTaskFragment( task.id diff --git a/app/src/main/java/lab/maxb/dark/presentation/view/SolveRecognitionTaskFragment.kt b/app/src/main/java/lab/maxb/dark/presentation/view/SolveRecognitionTaskFragment.kt index 100e458..e8575c9 100644 --- a/app/src/main/java/lab/maxb/dark/presentation/view/SolveRecognitionTaskFragment.kt +++ b/app/src/main/java/lab/maxb/dark/presentation/view/SolveRecognitionTaskFragment.kt @@ -7,10 +7,10 @@ import android.view.MenuInflater import android.view.MenuItem import android.view.View import android.widget.Toast -import androidx.core.net.toUri import androidx.core.view.isVisible import androidx.fragment.app.Fragment import androidx.navigation.fragment.navArgs +import com.bumptech.glide.Glide import com.wada811.databinding.dataBinding import lab.maxb.dark.R import lab.maxb.dark.databinding.SolveRecognitionTaskFragmentBinding @@ -18,7 +18,6 @@ import lab.maxb.dark.presentation.extra.delegates.autoCleaned import lab.maxb.dark.presentation.extra.goBack import lab.maxb.dark.presentation.extra.launchRepeatingOnLifecycle import lab.maxb.dark.presentation.extra.observe -import lab.maxb.dark.presentation.extra.toBitmap import lab.maxb.dark.presentation.view.adapter.ImageSliderAdapter import lab.maxb.dark.presentation.viewModel.SolveRecognitionTaskViewModel import lab.maxb.dark.presentation.viewModel.utils.ItemHolder @@ -34,7 +33,10 @@ class SolveRecognitionTaskFragment : Fragment(R.layout.solve_recognition_task_fr super.onViewCreated(view, savedInstanceState) mViewModel.init(args.id) data = mViewModel - mAdapter = ImageSliderAdapter() + val mGlide = Glide.with(this@SolveRecognitionTaskFragment) + mAdapter = ImageSliderAdapter { + mGlide.load(mViewModel.getImage(it)) + } imageSlider.adapter = mAdapter checkAnswer.setOnClickListener { launchRepeatingOnLifecycle { @@ -50,14 +52,8 @@ class SolveRecognitionTaskFragment : Fragment(R.layout.solve_recognition_task_fr goBack() return@ifLoaded } - (task.images ?: listOf()).mapNotNull { image -> - image.path.toUri().toBitmap( - requireContext(), - imageSlider.layoutParams.width, - imageSlider.layoutParams.height, - )?.let { content -> - ItemHolder(image.path to content) - } + (task.images ?: listOf()).map { image -> + ItemHolder(image.path) }.run { mAdapter.submitList(this) } mViewModel.isReviewMode observe { diff --git a/app/src/main/java/lab/maxb/dark/presentation/view/adapter/ImagePreloader.kt b/app/src/main/java/lab/maxb/dark/presentation/view/adapter/ImagePreloader.kt new file mode 100644 index 0000000..394b6d3 --- /dev/null +++ b/app/src/main/java/lab/maxb/dark/presentation/view/adapter/ImagePreloader.kt @@ -0,0 +1,36 @@ +package lab.maxb.dark.presentation.view.adapter + +import com.bumptech.glide.ListPreloader +import com.bumptech.glide.RequestBuilder +import com.bumptech.glide.RequestManager +import com.bumptech.glide.integration.recyclerview.RecyclerViewPreloader +import com.bumptech.glide.util.ViewPreloadSizeProvider +import java.util.* + +class ImagePreloader( + private val manager: RequestManager, + private val getItem: (Int) -> String?, + private val getImageLoader: RequestManager.(String) -> RequestBuilder<*>, + maxPreload: Int = 10, +) { + private val sizeProvider = ViewPreloadSizeProvider() + private val modelProvider = MyPreloadModelProvider() + val raw = RecyclerViewPreloader( + manager, + modelProvider, + sizeProvider, + maxPreload, + ) + + private inner class MyPreloadModelProvider : ListPreloader.PreloadModelProvider { + override fun getPreloadItems(position: Int): MutableList { + val url = getItem(position) + return if (url.isNullOrEmpty()) { + Collections.emptyList() + } else Collections.singletonList(url) + } + + override fun getPreloadRequestBuilder(item: String): RequestBuilder<*> + = getImageLoader(manager, item) + } +} \ No newline at end of file diff --git a/app/src/main/java/lab/maxb/dark/presentation/view/adapter/ImageSliderAdapter.kt b/app/src/main/java/lab/maxb/dark/presentation/view/adapter/ImageSliderAdapter.kt index bdfc5b9..e28da71 100644 --- a/app/src/main/java/lab/maxb/dark/presentation/view/adapter/ImageSliderAdapter.kt +++ b/app/src/main/java/lab/maxb/dark/presentation/view/adapter/ImageSliderAdapter.kt @@ -1,19 +1,21 @@ package lab.maxb.dark.presentation.view.adapter -import android.graphics.Bitmap import android.view.LayoutInflater import android.view.ViewGroup import androidx.recyclerview.widget.AsyncDifferConfig import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.ListAdapter import androidx.recyclerview.widget.RecyclerView +import com.bumptech.glide.RequestBuilder import lab.maxb.dark.databinding.ImageElementBinding import lab.maxb.dark.presentation.viewModel.utils.ItemHolder -typealias ComparableImage = Pair -open class ImageSliderAdapter : ListAdapter, - ImageSliderAdapter.ImageSliderViewHolder>(DiffCallback) { +open class ImageSliderAdapter( + private val getImageLoader: (String) -> RequestBuilder<*>, +) : ListAdapter, + ImageSliderAdapter.ImageSliderViewHolder>(DiffCallback) { + override fun onCreateViewHolder( parent: ViewGroup, viewType: Int @@ -28,20 +30,19 @@ open class ImageSliderAdapter : ListAdapter, holder: ImageSliderAdapter.ImageSliderViewHolder, position: Int ) { - val item = getItem(position) - holder.binding.imageContent.setImageBitmap(item.value.second) + getImageLoader(getItem(position).value).into(holder.binding.imageContent) } inner class ImageSliderViewHolder(var binding: ImageElementBinding) : RecyclerView.ViewHolder(binding.root) companion object { - private val DiffCallback = object : DiffUtil.ItemCallback>() { - override fun areItemsTheSame(oldItem: ItemHolder, newItem: ItemHolder) + private val DiffCallback = object : DiffUtil.ItemCallback>() { + override fun areItemsTheSame(oldItem: ItemHolder, newItem: ItemHolder) = oldItem.id == newItem.id - override fun areContentsTheSame(oldItem: ItemHolder, newItem: ItemHolder) - = oldItem.value.first == newItem.value.first + override fun areContentsTheSame(oldItem: ItemHolder, newItem: ItemHolder) + = oldItem.value == newItem.value }.let { AsyncDifferConfig.Builder(it).build() } } } \ No newline at end of file diff --git a/app/src/main/java/lab/maxb/dark/presentation/view/adapter/RecognitionTaskListAdapter.kt b/app/src/main/java/lab/maxb/dark/presentation/view/adapter/RecognitionTaskListAdapter.kt index b3225df..8dff52f 100644 --- a/app/src/main/java/lab/maxb/dark/presentation/view/adapter/RecognitionTaskListAdapter.kt +++ b/app/src/main/java/lab/maxb/dark/presentation/view/adapter/RecognitionTaskListAdapter.kt @@ -1,19 +1,29 @@ package lab.maxb.dark.presentation.view.adapter -import android.net.Uri import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.paging.PagingDataAdapter import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.RecyclerView +import com.bumptech.glide.RequestBuilder +import com.bumptech.glide.RequestManager import lab.maxb.dark.databinding.RecognitionTaskListElementBinding import lab.maxb.dark.domain.model.RecognitionTask -import lab.maxb.dark.presentation.extra.toBitmap import lab.maxb.dark.presentation.view.adapter.RecognitionTaskListAdapter.TaskViewHolder -class RecognitionTaskListAdapter: - PagingDataAdapter(COMPARATOR) { +class RecognitionTaskListAdapter( + private val manager: RequestManager, + private val getImageLoader: RequestManager.(String) -> RequestBuilder<*>, +): PagingDataAdapter(COMPARATOR) { + + val preloader = ImagePreloader( + manager, + getItem = { + getItem(it)?.images?.firstOrNull()?.path + }, + getImageLoader = getImageLoader + ) companion object { private val COMPARATOR = object : DiffUtil.ItemCallback() { @@ -40,20 +50,14 @@ class RecognitionTaskListAdapter: var onElementClickListener: ((view: View, item: RecognitionTask) -> Unit)? = null - override fun onBindViewHolder(holder: TaskViewHolder, position: Int) { - val item = getItem(position) ?: return - holder.binding.taskOwnerName.text = item.owner?.name - try { - holder.binding.taskImage.setImageBitmap( - Uri.parse(item.images?.get(0)?.path).toBitmap( - holder.itemView.context, - holder.binding.taskImage.layoutParams.width, - holder.binding.taskImage.layoutParams.height, - ) - ) - } catch (ignored: Throwable) {} - holder.itemView.setOnClickListener { v -> - if (position != RecyclerView.NO_POSITION) + override fun onBindViewHolder(holder: TaskViewHolder, position: Int) = with(holder.binding) { + val item = getItem(position) + taskOwnerName.text = item?.owner?.name ?: "" + item?.images?.firstOrNull()?.path?.let { + getImageLoader(manager, it).into(taskImage) + } ?: manager.clear(taskImage) + root.setOnClickListener { v -> + if (position != RecyclerView.NO_POSITION && item != null) onElementClickListener?.invoke(v, item) } } diff --git a/app/src/main/java/lab/maxb/dark/presentation/viewModel/AuthViewModel.kt b/app/src/main/java/lab/maxb/dark/presentation/viewModel/AuthViewModel.kt index ce0873e..bcc4bf6 100644 --- a/app/src/main/java/lab/maxb/dark/presentation/viewModel/AuthViewModel.kt +++ b/app/src/main/java/lab/maxb/dark/presentation/viewModel/AuthViewModel.kt @@ -3,7 +3,6 @@ package lab.maxb.dark.presentation.viewModel import androidx.lifecycle.ViewModel import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.flow.* import kotlinx.coroutines.withTimeout import lab.maxb.dark.domain.model.Profile diff --git a/app/src/main/java/lab/maxb/dark/presentation/viewModel/RecognitionTaskListViewModel.kt b/app/src/main/java/lab/maxb/dark/presentation/viewModel/RecognitionTaskListViewModel.kt index 8b0fa05..21521a7 100644 --- a/app/src/main/java/lab/maxb/dark/presentation/viewModel/RecognitionTaskListViewModel.kt +++ b/app/src/main/java/lab/maxb/dark/presentation/viewModel/RecognitionTaskListViewModel.kt @@ -8,6 +8,7 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.flow.mapLatest import lab.maxb.dark.domain.model.isUser +import lab.maxb.dark.presentation.repository.interfaces.ImagesRepository import lab.maxb.dark.presentation.repository.interfaces.ProfileRepository import lab.maxb.dark.presentation.repository.interfaces.RecognitionTasksRepository import lab.maxb.dark.presentation.viewModel.utils.stateIn @@ -17,6 +18,7 @@ import org.koin.android.annotation.KoinViewModel class RecognitionTaskListViewModel( recognitionTasksRepository: RecognitionTasksRepository, profileRepository: ProfileRepository, + private val imagesRepository: ImagesRepository, ) : ViewModel() { private val profile = profileRepository.profileState @@ -32,4 +34,6 @@ class RecognitionTaskListViewModel( val isTaskCreationAllowed = profile.mapLatest { it?.role?.isUser ?: false }.stateIn(false) + + fun getImage(path: String) = imagesRepository.getUri(path) } \ No newline at end of file diff --git a/app/src/main/java/lab/maxb/dark/presentation/viewModel/SolveRecognitionTaskViewModel.kt b/app/src/main/java/lab/maxb/dark/presentation/viewModel/SolveRecognitionTaskViewModel.kt index 828f6d2..e2c1f2a 100644 --- a/app/src/main/java/lab/maxb/dark/presentation/viewModel/SolveRecognitionTaskViewModel.kt +++ b/app/src/main/java/lab/maxb/dark/presentation/viewModel/SolveRecognitionTaskViewModel.kt @@ -4,6 +4,7 @@ import androidx.lifecycle.ViewModel import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.* import lab.maxb.dark.domain.model.Role +import lab.maxb.dark.presentation.repository.interfaces.ImagesRepository import lab.maxb.dark.presentation.repository.interfaces.ProfileRepository import lab.maxb.dark.presentation.repository.interfaces.RecognitionTasksRepository import lab.maxb.dark.presentation.repository.interfaces.UsersRepository @@ -16,6 +17,7 @@ import org.koin.android.annotation.KoinViewModel @KoinViewModel class SolveRecognitionTaskViewModel( private val recognitionTasksRepository: RecognitionTasksRepository, + private val imagesRepository: ImagesRepository, private val usersRepository: UsersRepository, profileRepository: ProfileRepository, ) : ViewModel() { @@ -65,4 +67,6 @@ class SolveRecognitionTaskViewModel( } } } ?: false + + fun getImage(path: String) = imagesRepository.getUri(path) } \ No newline at end of file diff --git a/app/src/main/java/lab/maxb/dark/presentation/viewModel/utils/ItemHolder.kt b/app/src/main/java/lab/maxb/dark/presentation/viewModel/utils/ItemHolder.kt index bd30c31..7e516a9 100644 --- a/app/src/main/java/lab/maxb/dark/presentation/viewModel/utils/ItemHolder.kt +++ b/app/src/main/java/lab/maxb/dark/presentation/viewModel/utils/ItemHolder.kt @@ -1,11 +1,12 @@ package lab.maxb.dark.presentation.viewModel.utils - import java.util.* data class ItemHolder( var value: T, val id: UUID = UUID.randomUUID(), -) +) { + var key = "" +} fun T.asItemHolder() = ItemHolder(this) @@ -14,4 +15,4 @@ fun ItemHolder.map(value: R) = ItemHolder(value, id) inline fun ItemHolder.map(block: (T) -> R) - = map(block(value)) \ No newline at end of file + = map(block(value)) diff --git a/app/src/test/java/lab/maxb/dark/domain/operations/RecognitionTaskOperationsKtTest.kt b/app/src/test/java/lab/maxb/dark/domain/operations/RecognitionTaskOperationsKtTest.kt index 7078400..ce15a72 100644 --- a/app/src/test/java/lab/maxb/dark/domain/operations/RecognitionTaskOperationsKtTest.kt +++ b/app/src/test/java/lab/maxb/dark/domain/operations/RecognitionTaskOperationsKtTest.kt @@ -1,7 +1,8 @@ package lab.maxb.dark.domain.operations import lab.maxb.dark.domain.model.User -import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.Assertions.assertNotNull +import org.junit.jupiter.api.Assertions.assertNull import org.junit.jupiter.api.Test From 7d7bbcce393f95e0f18d4e041040dd8ab62fd4b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=91=D0=B0=D0=B1=D1=8B=D1=88=D0=B5=D0=B2=20=D0=9C=D0=B0?= =?UTF-8?q?=D0=BA=D1=81=D0=B8=D0=BC?= Date: Sun, 29 May 2022 18:58:43 +0300 Subject: [PATCH 03/22] Simplify ItemHolder use-cases --- .../network/dark/DarkServiceImpl.kt | 16 ++++------ .../network/dark/routes/RecognitionTask.kt | 6 ---- .../view/AddRecognitionTaskFragment.kt | 9 ++---- .../view/SolveRecognitionTaskFragment.kt | 1 + .../view/adapter/ImageSliderAdapter.kt | 14 +-------- .../view/adapter/InputListAdapter.kt | 30 ++++++++----------- .../adapter/RecognitionTaskListAdapter.kt | 1 - .../view/adapter/StringHolderDiffchecker.kt | 13 ++++++++ .../viewModel/AddRecognitionTaskViewModel.kt | 9 +++--- .../viewModel/utils/ItemHolder.kt | 9 ++---- 10 files changed, 42 insertions(+), 66 deletions(-) create mode 100644 app/src/main/java/lab/maxb/dark/presentation/view/adapter/StringHolderDiffchecker.kt diff --git a/app/src/main/java/lab/maxb/dark/presentation/repository/network/dark/DarkServiceImpl.kt b/app/src/main/java/lab/maxb/dark/presentation/repository/network/dark/DarkServiceImpl.kt index 3826dde..de5a4ee 100644 --- a/app/src/main/java/lab/maxb/dark/presentation/repository/network/dark/DarkServiceImpl.kt +++ b/app/src/main/java/lab/maxb/dark/presentation/repository/network/dark/DarkServiceImpl.kt @@ -44,16 +44,12 @@ class DarkServiceImpl( api.addImage(id, filePart) } - override fun getImageSource(path: String) - = GlideUrl( - "${BuildConfig.DARK_API_URL}/task/image/$path", - LazyHeaders.Builder() - .addHeader(authInterceptor.header, authInterceptor.value) - .build() - ) - -// api.downloadImage(path) - + override fun getImageSource(path: String) = GlideUrl( + "${BuildConfig.DARK_API_URL}/task/image/$path", + LazyHeaders.Builder() + .addHeader(authInterceptor.header, authInterceptor.value) + .build() + ) override suspend fun getUser(id: String) = catchAll { api.getUser(id) diff --git a/app/src/main/java/lab/maxb/dark/presentation/repository/network/dark/routes/RecognitionTask.kt b/app/src/main/java/lab/maxb/dark/presentation/repository/network/dark/routes/RecognitionTask.kt index 2258a9c..0d7392d 100644 --- a/app/src/main/java/lab/maxb/dark/presentation/repository/network/dark/routes/RecognitionTask.kt +++ b/app/src/main/java/lab/maxb/dark/presentation/repository/network/dark/routes/RecognitionTask.kt @@ -38,12 +38,6 @@ interface RecognitionTask { @Part filePart: MultipartBody.Part ): String? -// @Streaming -// @GET("$path/image/{path}") -// suspend fun downloadImage( -// @Path("path") path: String -// ): ResponseBody? - companion object { const val path = "/task" } diff --git a/app/src/main/java/lab/maxb/dark/presentation/view/AddRecognitionTaskFragment.kt b/app/src/main/java/lab/maxb/dark/presentation/view/AddRecognitionTaskFragment.kt index a4bf3c6..7092856 100644 --- a/app/src/main/java/lab/maxb/dark/presentation/view/AddRecognitionTaskFragment.kt +++ b/app/src/main/java/lab/maxb/dark/presentation/view/AddRecognitionTaskFragment.kt @@ -26,7 +26,6 @@ import lab.maxb.dark.presentation.extra.observe import lab.maxb.dark.presentation.view.adapter.ImageSliderAdapter import lab.maxb.dark.presentation.view.adapter.InputListAdapter import lab.maxb.dark.presentation.viewModel.AddRecognitionTaskViewModel -import lab.maxb.dark.presentation.viewModel.utils.map import org.koin.androidx.viewmodel.ext.android.sharedViewModel class AddRecognitionTaskFragment : Fragment(R.layout.add_recognition_task_fragment) { @@ -51,13 +50,9 @@ class AddRecognitionTaskFragment : Fragment(R.layout.add_recognition_task_fragme mGlide.load(it.toUri()) } imageSlider.adapter = mImagesAdapter -// imageSlider.set() mImagesAdapter.preloader + mViewModel.images observe { uris -> - uris.map { - it.map { uri -> uri.toString() } - }.also { - mImagesAdapter.submitList(it) - } + mImagesAdapter.submitList(uris) val hasUris = uris.isNotEmpty() addImageButtonAlternative.isVisible = !hasUris imageSlider.isVisible = hasUris diff --git a/app/src/main/java/lab/maxb/dark/presentation/view/SolveRecognitionTaskFragment.kt b/app/src/main/java/lab/maxb/dark/presentation/view/SolveRecognitionTaskFragment.kt index e8575c9..edf997b 100644 --- a/app/src/main/java/lab/maxb/dark/presentation/view/SolveRecognitionTaskFragment.kt +++ b/app/src/main/java/lab/maxb/dark/presentation/view/SolveRecognitionTaskFragment.kt @@ -52,6 +52,7 @@ class SolveRecognitionTaskFragment : Fragment(R.layout.solve_recognition_task_fr goBack() return@ifLoaded } + (task.images ?: listOf()).map { image -> ItemHolder(image.path) }.run { mAdapter.submitList(this) } diff --git a/app/src/main/java/lab/maxb/dark/presentation/view/adapter/ImageSliderAdapter.kt b/app/src/main/java/lab/maxb/dark/presentation/view/adapter/ImageSliderAdapter.kt index e28da71..6a80884 100644 --- a/app/src/main/java/lab/maxb/dark/presentation/view/adapter/ImageSliderAdapter.kt +++ b/app/src/main/java/lab/maxb/dark/presentation/view/adapter/ImageSliderAdapter.kt @@ -2,8 +2,6 @@ package lab.maxb.dark.presentation.view.adapter import android.view.LayoutInflater import android.view.ViewGroup -import androidx.recyclerview.widget.AsyncDifferConfig -import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.ListAdapter import androidx.recyclerview.widget.RecyclerView import com.bumptech.glide.RequestBuilder @@ -14,7 +12,7 @@ import lab.maxb.dark.presentation.viewModel.utils.ItemHolder open class ImageSliderAdapter( private val getImageLoader: (String) -> RequestBuilder<*>, ) : ListAdapter, - ImageSliderAdapter.ImageSliderViewHolder>(DiffCallback) { + ImageSliderAdapter.ImageSliderViewHolder>(stringHolderDiffCallback) { override fun onCreateViewHolder( parent: ViewGroup, @@ -35,14 +33,4 @@ open class ImageSliderAdapter( inner class ImageSliderViewHolder(var binding: ImageElementBinding) : RecyclerView.ViewHolder(binding.root) - - companion object { - private val DiffCallback = object : DiffUtil.ItemCallback>() { - override fun areItemsTheSame(oldItem: ItemHolder, newItem: ItemHolder) - = oldItem.id == newItem.id - - override fun areContentsTheSame(oldItem: ItemHolder, newItem: ItemHolder) - = oldItem.value == newItem.value - }.let { AsyncDifferConfig.Builder(it).build() } - } } \ No newline at end of file diff --git a/app/src/main/java/lab/maxb/dark/presentation/view/adapter/InputListAdapter.kt b/app/src/main/java/lab/maxb/dark/presentation/view/adapter/InputListAdapter.kt index 75acdfd..70542a6 100644 --- a/app/src/main/java/lab/maxb/dark/presentation/view/adapter/InputListAdapter.kt +++ b/app/src/main/java/lab/maxb/dark/presentation/view/adapter/InputListAdapter.kt @@ -4,8 +4,6 @@ import android.view.LayoutInflater import android.view.ViewGroup import android.widget.EditText import androidx.core.widget.doOnTextChanged -import androidx.recyclerview.widget.AsyncDifferConfig -import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.ListAdapter import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView.NO_POSITION @@ -14,7 +12,7 @@ import lab.maxb.dark.presentation.viewModel.utils.ItemHolder class InputListAdapter : - ListAdapter, InputListAdapter.ViewHolder>(DiffCallback) { + ListAdapter, InputListAdapter.ViewHolder>(stringHolderDiffCallback) { var onItemTextChangedListener: ((input: EditText, position: Int, newValue: String?) -> Unit)? = null @@ -34,26 +32,24 @@ class InputListAdapter : with (holder.binding.input){ setText(item.value) doOnTextChanged { text, _, _, _ -> - if (holder.adapterPosition != NO_POSITION) - onItemTextChangedListener?.invoke(this, holder.adapterPosition, text?.toString()) + if (holder.absoluteAdapterPosition != NO_POSITION) + onItemTextChangedListener?.invoke( + this, + holder.absoluteAdapterPosition, + text?.toString() + ) } setOnFocusChangeListener { _, hasFocus -> - if (holder.adapterPosition != NO_POSITION) - onItemFocusedListener?.invoke(this, holder.adapterPosition, hasFocus) + if (holder.absoluteAdapterPosition != NO_POSITION) + onItemFocusedListener?.invoke( + this, + holder.absoluteAdapterPosition, + hasFocus + ) } } } inner class ViewHolder(var binding: InputListItemBinding): RecyclerView.ViewHolder(binding.root) - - companion object { - private val DiffCallback = object : DiffUtil.ItemCallback>() { - override fun areItemsTheSame(oldItem: ItemHolder, newItem: ItemHolder) = - oldItem.id == newItem.id - - override fun areContentsTheSame(oldItem: ItemHolder, newItem: ItemHolder) = - oldItem.value == newItem.value - }.let { AsyncDifferConfig.Builder(it).build() } - } } diff --git a/app/src/main/java/lab/maxb/dark/presentation/view/adapter/RecognitionTaskListAdapter.kt b/app/src/main/java/lab/maxb/dark/presentation/view/adapter/RecognitionTaskListAdapter.kt index 8dff52f..98144ef 100644 --- a/app/src/main/java/lab/maxb/dark/presentation/view/adapter/RecognitionTaskListAdapter.kt +++ b/app/src/main/java/lab/maxb/dark/presentation/view/adapter/RecognitionTaskListAdapter.kt @@ -31,7 +31,6 @@ class RecognitionTaskListAdapter( oldItem.id == newItem.id override fun areContentsTheSame(oldItem: RecognitionTask, newItem: RecognitionTask): Boolean = - oldItem.id == newItem.id && oldItem.owner?.id == newItem.owner?.id && oldItem.owner?.name == newItem.owner?.name && oldItem.images?.firstOrNull()?.id == newItem.images?.firstOrNull()?.id diff --git a/app/src/main/java/lab/maxb/dark/presentation/view/adapter/StringHolderDiffchecker.kt b/app/src/main/java/lab/maxb/dark/presentation/view/adapter/StringHolderDiffchecker.kt new file mode 100644 index 0000000..46ecf49 --- /dev/null +++ b/app/src/main/java/lab/maxb/dark/presentation/view/adapter/StringHolderDiffchecker.kt @@ -0,0 +1,13 @@ +package lab.maxb.dark.presentation.view.adapter + +import androidx.recyclerview.widget.AsyncDifferConfig +import androidx.recyclerview.widget.DiffUtil +import lab.maxb.dark.presentation.viewModel.utils.ItemHolder + +val stringHolderDiffCallback = object : DiffUtil.ItemCallback>() { + override fun areItemsTheSame(oldItem: ItemHolder, newItem: ItemHolder) + = oldItem.id == newItem.id + + override fun areContentsTheSame(oldItem: ItemHolder, newItem: ItemHolder) + = oldItem.value == newItem.value +}.let { AsyncDifferConfig.Builder(it).build() } diff --git a/app/src/main/java/lab/maxb/dark/presentation/viewModel/AddRecognitionTaskViewModel.kt b/app/src/main/java/lab/maxb/dark/presentation/viewModel/AddRecognitionTaskViewModel.kt index 5fc0600..39f27dd 100644 --- a/app/src/main/java/lab/maxb/dark/presentation/viewModel/AddRecognitionTaskViewModel.kt +++ b/app/src/main/java/lab/maxb/dark/presentation/viewModel/AddRecognitionTaskViewModel.kt @@ -15,7 +15,6 @@ import lab.maxb.dark.presentation.repository.interfaces.ProfileRepository import lab.maxb.dark.presentation.repository.interfaces.RecognitionTasksRepository import lab.maxb.dark.presentation.repository.interfaces.SynonymsRepository import lab.maxb.dark.presentation.viewModel.utils.ItemHolder -import lab.maxb.dark.presentation.viewModel.utils.asItemHolder import lab.maxb.dark.presentation.viewModel.utils.firstNotNull import lab.maxb.dark.presentation.viewModel.utils.stateIn import org.koin.android.annotation.KoinViewModel @@ -30,7 +29,7 @@ class AddRecognitionTaskViewModel( private val _namesRaw = mutableListOf(ItemHolder("")) private val _names = MutableStateFlow(_namesRaw.toList()) val names = _names.asStateFlow() - private var _imagesRaw = mutableListOf>() + private var _imagesRaw = mutableListOf>() private val _images = MutableStateFlow(_imagesRaw.toList()) val images = _images.asStateFlow() @@ -50,7 +49,7 @@ class AddRecognitionTaskViewModel( val user = profile.firstNotNull().user!! val task = createRecognitionTask( _namesRaw.map { it.value }.filter { it.isNotBlank() }, - _imagesRaw.map { it.value.toString() }, + _imagesRaw.map { it.value }, user)!! recognitionTasksRepository.addRecognitionTask(task) true @@ -108,14 +107,14 @@ class AddRecognitionTaskViewModel( if (!allowImageAddition) return@forEach it.takePersistablePermission(getApplication()) - _imagesRaw.add(it.asItemHolder()) + _imagesRaw.add(ItemHolder(it.toString())) } updateImages() } fun updateImage(position: Int, uri: Uri) { uri.takePersistablePermission(getApplication()) - _imagesRaw[position].value = uri + _imagesRaw[position].value = uri.toString() updateImages() } } diff --git a/app/src/main/java/lab/maxb/dark/presentation/viewModel/utils/ItemHolder.kt b/app/src/main/java/lab/maxb/dark/presentation/viewModel/utils/ItemHolder.kt index 7e516a9..2e2cf38 100644 --- a/app/src/main/java/lab/maxb/dark/presentation/viewModel/utils/ItemHolder.kt +++ b/app/src/main/java/lab/maxb/dark/presentation/viewModel/utils/ItemHolder.kt @@ -1,15 +1,10 @@ package lab.maxb.dark.presentation.viewModel.utils import java.util.* -data class ItemHolder( +class ItemHolder( var value: T, val id: UUID = UUID.randomUUID(), -) { - var key = "" -} - -fun T.asItemHolder() - = ItemHolder(this) +) fun ItemHolder.map(value: R) = ItemHolder(value, id) From d1b272cba645364d06eaacdf9fbcafb0d9c4708f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=91=D0=B0=D0=B1=D1=8B=D1=88=D0=B5=D0=B2=20=D0=9C=D0=B0?= =?UTF-8?q?=D0=BA=D1=81=D0=B8=D0=BC?= Date: Sun, 29 May 2022 19:50:57 +0300 Subject: [PATCH 04/22] Remove Image as separate type/entity --- .../java/lab/maxb/dark/domain/model/Image.kt | 8 --- .../maxb/dark/domain/model/RecognitionTask.kt | 2 +- .../operations/RecognitionTaskOperations.kt | 3 +- .../implementation/ImagesRepositoryImpl.kt | 49 ------------------- .../RecognitionTasksRepositoryImpl.kt | 42 ++++------------ .../repository/interfaces/ImagesRepository.kt | 12 ----- .../interfaces/RecognitionTasksRepository.kt | 2 + .../repository/room/LocalDatabase.kt | 28 ++++++----- .../repository/room/converters/Collections.kt | 24 +++++++++ .../repository/room/dao/ImageDAO.kt | 20 -------- .../repository/room/dao/RecognitionTaskDAO.kt | 9 +--- .../repository/room/model/ImageDTO.kt | 21 -------- .../room/model/RecognitionTaskDTO.kt | 4 +- .../model/RecognitionTaskImageCrossref.kt | 29 ----------- .../RecognitionTaskWithNamesAndImages.kt | 11 +---- .../RecognitionTaskWithOwnerAndImage.kt | 13 ++--- .../view/SolveRecognitionTaskFragment.kt | 2 +- .../adapter/RecognitionTaskListAdapter.kt | 6 +-- .../viewModel/RecognitionTaskListViewModel.kt | 6 +-- .../SolveRecognitionTaskViewModel.kt | 4 +- 20 files changed, 70 insertions(+), 225 deletions(-) delete mode 100644 app/src/main/java/lab/maxb/dark/domain/model/Image.kt delete mode 100644 app/src/main/java/lab/maxb/dark/presentation/repository/implementation/ImagesRepositoryImpl.kt delete mode 100644 app/src/main/java/lab/maxb/dark/presentation/repository/interfaces/ImagesRepository.kt create mode 100644 app/src/main/java/lab/maxb/dark/presentation/repository/room/converters/Collections.kt delete mode 100644 app/src/main/java/lab/maxb/dark/presentation/repository/room/dao/ImageDAO.kt delete mode 100644 app/src/main/java/lab/maxb/dark/presentation/repository/room/model/ImageDTO.kt delete mode 100644 app/src/main/java/lab/maxb/dark/presentation/repository/room/model/RecognitionTaskImageCrossref.kt diff --git a/app/src/main/java/lab/maxb/dark/domain/model/Image.kt b/app/src/main/java/lab/maxb/dark/domain/model/Image.kt deleted file mode 100644 index 63cb384..0000000 --- a/app/src/main/java/lab/maxb/dark/domain/model/Image.kt +++ /dev/null @@ -1,8 +0,0 @@ -package lab.maxb.dark.domain.model - -import lab.maxb.dark.domain.operations.randomUUID - -open class Image( - open var path: String = "", - open var id: String = randomUUID, -) \ No newline at end of file diff --git a/app/src/main/java/lab/maxb/dark/domain/model/RecognitionTask.kt b/app/src/main/java/lab/maxb/dark/domain/model/RecognitionTask.kt index 98fec0a..df7096e 100644 --- a/app/src/main/java/lab/maxb/dark/domain/model/RecognitionTask.kt +++ b/app/src/main/java/lab/maxb/dark/domain/model/RecognitionTask.kt @@ -4,7 +4,7 @@ import lab.maxb.dark.domain.operations.randomUUID open class RecognitionTask( open var names: Set? = null, - open var images: List? = null, + open var images: List? = null, open var owner: User? = null, open var reviewed: Boolean = false, open var id: String = randomUUID, diff --git a/app/src/main/java/lab/maxb/dark/domain/operations/RecognitionTaskOperations.kt b/app/src/main/java/lab/maxb/dark/domain/operations/RecognitionTaskOperations.kt index cdfd364..66b8fc2 100644 --- a/app/src/main/java/lab/maxb/dark/domain/operations/RecognitionTaskOperations.kt +++ b/app/src/main/java/lab/maxb/dark/domain/operations/RecognitionTaskOperations.kt @@ -1,6 +1,5 @@ package lab.maxb.dark.domain.operations -import lab.maxb.dark.domain.model.Image import lab.maxb.dark.domain.model.RecognitionTask import lab.maxb.dark.domain.model.User @@ -18,7 +17,7 @@ fun createRecognitionTask( return RecognitionTask( namesSet, - images.map { Image(it) }, + images, owner, ) } diff --git a/app/src/main/java/lab/maxb/dark/presentation/repository/implementation/ImagesRepositoryImpl.kt b/app/src/main/java/lab/maxb/dark/presentation/repository/implementation/ImagesRepositoryImpl.kt deleted file mode 100644 index 5fb8a8a..0000000 --- a/app/src/main/java/lab/maxb/dark/presentation/repository/implementation/ImagesRepositoryImpl.kt +++ /dev/null @@ -1,49 +0,0 @@ -package lab.maxb.dark.presentation.repository.implementation - -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.flow.mapLatest -import lab.maxb.dark.domain.model.Image -import lab.maxb.dark.presentation.extra.ImageLoader -import lab.maxb.dark.presentation.repository.interfaces.ImagesRepository -import lab.maxb.dark.presentation.repository.network.dark.DarkService -import lab.maxb.dark.presentation.repository.room.LocalDatabase -import lab.maxb.dark.presentation.repository.room.model.toImage -import lab.maxb.dark.presentation.repository.room.model.toImageDTO -import lab.maxb.dark.presentation.repository.utils.StaticResource -import org.koin.core.annotation.Single - -@Single -class ImagesRepositoryImpl( - db: LocalDatabase, - private val mDarkService: DarkService, - private val imageLoader: ImageLoader, -) : ImagesRepository { - private val mImageDao = db.imageDao() - - @OptIn(ExperimentalCoroutinesApi::class) - private val imageResource = StaticResource().apply { - fetchRemote = { - Image(it, it) - } - localStore = { - mImageDao.save(it.toImageDTO()) - } - clearLocalStore = { - mImageDao.delete(it) - } - fetchLocal = { - mImageDao.getById(it).mapLatest { image -> - image?.toImage() - } - } - } - - override suspend fun save(image: Image) = mImageDao.save(image.toImageDTO()) - - override suspend fun getById(id: String) = imageResource.query(id) - - override fun getUri(path: String) - = mDarkService.getImageSource(path) - - override suspend fun delete(id: String) = mImageDao.delete(id) -} \ No newline at end of file diff --git a/app/src/main/java/lab/maxb/dark/presentation/repository/implementation/RecognitionTasksRepositoryImpl.kt b/app/src/main/java/lab/maxb/dark/presentation/repository/implementation/RecognitionTasksRepositoryImpl.kt index c51a564..adeacc1 100644 --- a/app/src/main/java/lab/maxb/dark/presentation/repository/implementation/RecognitionTasksRepositoryImpl.kt +++ b/app/src/main/java/lab/maxb/dark/presentation/repository/implementation/RecognitionTasksRepositoryImpl.kt @@ -12,7 +12,6 @@ import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.mapLatest import lab.maxb.dark.domain.model.RecognitionTask import lab.maxb.dark.presentation.extra.ImageLoader -import lab.maxb.dark.presentation.repository.interfaces.ImagesRepository import lab.maxb.dark.presentation.repository.interfaces.RecognitionTasksRepository import lab.maxb.dark.presentation.repository.interfaces.UsersRepository import lab.maxb.dark.presentation.repository.network.dark.DarkService @@ -20,7 +19,6 @@ import lab.maxb.dark.presentation.repository.network.dark.model.RecognitionTaskC import lab.maxb.dark.presentation.repository.room.LocalDatabase import lab.maxb.dark.presentation.repository.room.dao.RecognitionTaskDAO import lab.maxb.dark.presentation.repository.room.model.RecognitionTaskDTO -import lab.maxb.dark.presentation.repository.room.model.RecognitionTaskImageCrossref import lab.maxb.dark.presentation.repository.room.model.RecognitionTaskName import lab.maxb.dark.presentation.repository.utils.Resource import lab.maxb.dark.presentation.repository.utils.pagination.Page @@ -32,7 +30,6 @@ class RecognitionTasksRepositoryImpl( private val db: LocalDatabase, private val mDarkService: DarkService, private val usersRepository: UsersRepository, - private val imagesRepository: ImagesRepository, private val imageLoader: ImageLoader, ) : RecognitionTasksRepository { private val mRecognitionTaskDao: RecognitionTaskDAO = db.recognitionTaskDao() @@ -48,7 +45,7 @@ class RecognitionTasksRepositoryImpl( mDarkService.getAllTasks(page.page, page.size)?.map { RecognitionTask( setOf(), - imagesRepository.getById(it.image!!).firstOrNull()?.let { listOf(it) }, + it.image?.let { x -> listOf(x) }, usersRepository.getUser(it.owner_id).firstOrNull()!!, it.reviewed, it.id, @@ -61,12 +58,6 @@ class RecognitionTasksRepositoryImpl( mRecognitionTaskDao.addRecognitionTask( RecognitionTaskDTO(task) ) - mRecognitionTaskDao.addRecognitionTaskImages( - listOf(RecognitionTaskImageCrossref( - task.id, - task.images?.get(0)?.id ?: return@forEach, - )) - ) } } } @@ -98,26 +89,18 @@ class RecognitionTasksRepositoryImpl( ) )?.also { taskLocal.id = it } - val images = task.images!!.map { - it.id = mDarkService.addImage( + taskLocal.images = task.images!!.map { + mDarkService.addImage( taskLocal.id, - imageLoader.fromUri(it.path.toUri()) - )!!.also { id -> - it.id = id - it.path = id - } - imagesRepository.save(it) - RecognitionTaskImageCrossref( - taskLocal.id, it.id, - ) + imageLoader.fromUri(it.toUri()) + )!! } mRecognitionTaskDao.addRecognitionTask( taskLocal, task.names!!.map { RecognitionTaskName(taskLocal.id, it) - }, - images + } ) } @@ -151,9 +134,7 @@ class RecognitionTasksRepositoryImpl( mDarkService.getTask(id)?.let { task -> RecognitionTask( task.names, - task.images?.map { image -> - imagesRepository.getById(image).firstOrNull()!! - }, + task.images, usersRepository.getUser( task.owner_id ).firstOrNull()!!, @@ -173,12 +154,6 @@ class RecognitionTasksRepositoryImpl( task.names!!.map { name -> RecognitionTaskName(task.id, name) }, - task.images!!.map { image -> - RecognitionTaskImageCrossref( - task.id, - image.id, - ) - }, ) } clearLocalStore = { @@ -188,4 +163,7 @@ class RecognitionTasksRepositoryImpl( override suspend fun getRecognitionTask(id: String, forceUpdate: Boolean) = taskResource.query(id, forceUpdate) + + override fun getRecognitionTaskImage(path: String) + = mDarkService.getImageSource(path) } \ No newline at end of file diff --git a/app/src/main/java/lab/maxb/dark/presentation/repository/interfaces/ImagesRepository.kt b/app/src/main/java/lab/maxb/dark/presentation/repository/interfaces/ImagesRepository.kt deleted file mode 100644 index 0d73176..0000000 --- a/app/src/main/java/lab/maxb/dark/presentation/repository/interfaces/ImagesRepository.kt +++ /dev/null @@ -1,12 +0,0 @@ -package lab.maxb.dark.presentation.repository.interfaces - -import com.bumptech.glide.load.model.GlideUrl -import kotlinx.coroutines.flow.Flow -import lab.maxb.dark.domain.model.Image - -interface ImagesRepository { - suspend fun save(image: Image) - suspend fun getById(id: String): Flow - fun getUri(path: String): GlideUrl - suspend fun delete(id: String) -} diff --git a/app/src/main/java/lab/maxb/dark/presentation/repository/interfaces/RecognitionTasksRepository.kt b/app/src/main/java/lab/maxb/dark/presentation/repository/interfaces/RecognitionTasksRepository.kt index 0896c9f..f5175dc 100644 --- a/app/src/main/java/lab/maxb/dark/presentation/repository/interfaces/RecognitionTasksRepository.kt +++ b/app/src/main/java/lab/maxb/dark/presentation/repository/interfaces/RecognitionTasksRepository.kt @@ -1,12 +1,14 @@ package lab.maxb.dark.presentation.repository.interfaces import androidx.paging.PagingData +import com.bumptech.glide.load.model.GlideUrl import kotlinx.coroutines.flow.Flow import lab.maxb.dark.domain.model.RecognitionTask interface RecognitionTasksRepository { fun getAllRecognitionTasks(): Flow> suspend fun getRecognitionTask(id: String, forceUpdate: Boolean = false): Flow + fun getRecognitionTaskImage(path: String): GlideUrl suspend fun addRecognitionTask(task: RecognitionTask) suspend fun markRecognitionTask(task: RecognitionTask) suspend fun solveRecognitionTask(id: String, answer: String): Boolean diff --git a/app/src/main/java/lab/maxb/dark/presentation/repository/room/LocalDatabase.kt b/app/src/main/java/lab/maxb/dark/presentation/repository/room/LocalDatabase.kt index 8482915..6d9c868 100644 --- a/app/src/main/java/lab/maxb/dark/presentation/repository/room/LocalDatabase.kt +++ b/app/src/main/java/lab/maxb/dark/presentation/repository/room/LocalDatabase.kt @@ -4,32 +4,36 @@ import android.app.Application import androidx.room.Database import androidx.room.Room import androidx.room.RoomDatabase +import androidx.room.TypeConverters import lab.maxb.dark.presentation.repository.room.Server.Model.ProfileDTO -import lab.maxb.dark.presentation.repository.room.dao.* -import lab.maxb.dark.presentation.repository.room.model.* +import lab.maxb.dark.presentation.repository.room.converters.ListConverter +import lab.maxb.dark.presentation.repository.room.dao.ProfileDAO +import lab.maxb.dark.presentation.repository.room.dao.RecognitionTaskDAO +import lab.maxb.dark.presentation.repository.room.dao.RemoteKeysDAO +import lab.maxb.dark.presentation.repository.room.dao.UserDAO +import lab.maxb.dark.presentation.repository.room.model.RecognitionTaskDTO +import lab.maxb.dark.presentation.repository.room.model.RecognitionTaskName +import lab.maxb.dark.presentation.repository.room.model.RemoteKeys +import lab.maxb.dark.presentation.repository.room.model.UserDTO @Database(entities = [ UserDTO::class, RecognitionTaskDTO::class, RecognitionTaskName::class, - ImageDTO::class, - RecognitionTaskImageCrossref::class, ProfileDTO::class, RemoteKeys::class, - ], version = 5, exportSchema = false) + ], version = 6, exportSchema = false) +@TypeConverters(ListConverter::class) abstract class LocalDatabase : RoomDatabase() { abstract fun recognitionTaskDao(): RecognitionTaskDAO abstract fun userDao(): UserDAO abstract fun profileDao(): ProfileDAO - abstract fun imageDao(): ImageDAO abstract fun remoteKeysDao(): RemoteKeysDAO companion object { - internal fun build(app: Application) - = Room.databaseBuilder( - app.applicationContext, - LocalDatabase::class.java, "dark_database" - ).fallbackToDestructiveMigration() - .build() + internal fun build(app: Application) = Room.databaseBuilder( + app.applicationContext, + LocalDatabase::class.java, "dark_database" + ).fallbackToDestructiveMigration().build() } } \ No newline at end of file diff --git a/app/src/main/java/lab/maxb/dark/presentation/repository/room/converters/Collections.kt b/app/src/main/java/lab/maxb/dark/presentation/repository/room/converters/Collections.kt new file mode 100644 index 0000000..185c178 --- /dev/null +++ b/app/src/main/java/lab/maxb/dark/presentation/repository/room/converters/Collections.kt @@ -0,0 +1,24 @@ +package lab.maxb.dark.presentation.repository.room.converters + +import androidx.room.TypeConverter +import com.google.gson.Gson +import com.google.gson.reflect.TypeToken +import java.lang.reflect.Type + + +class ListConverter { + @TypeConverter + fun fromList(list: List?) = list?.let { + gson.toJson(list, type) + } + + @TypeConverter + fun toList(list: String?) = list?.let { + gson.fromJson>(list, type) + } + + companion object { + private val gson = Gson() + private val type: Type = object : TypeToken?>() {}.type + } +} \ No newline at end of file diff --git a/app/src/main/java/lab/maxb/dark/presentation/repository/room/dao/ImageDAO.kt b/app/src/main/java/lab/maxb/dark/presentation/repository/room/dao/ImageDAO.kt deleted file mode 100644 index 9278c6f..0000000 --- a/app/src/main/java/lab/maxb/dark/presentation/repository/room/dao/ImageDAO.kt +++ /dev/null @@ -1,20 +0,0 @@ -package lab.maxb.dark.presentation.repository.room.dao - -import androidx.room.Dao -import androidx.room.Insert -import androidx.room.OnConflictStrategy.REPLACE -import androidx.room.Query -import kotlinx.coroutines.flow.Flow -import lab.maxb.dark.presentation.repository.room.model.ImageDTO - -@Dao -interface ImageDAO { - @Insert(onConflict = REPLACE) - suspend fun save(image: ImageDTO) - - @Query("SELECT * FROM image WHERE imageId = :id") - fun getById(id: String): Flow - - @Query("DELETE FROM image WHERE imageId = :id") - suspend fun delete(id: String) -} \ No newline at end of file diff --git a/app/src/main/java/lab/maxb/dark/presentation/repository/room/dao/RecognitionTaskDAO.kt b/app/src/main/java/lab/maxb/dark/presentation/repository/room/dao/RecognitionTaskDAO.kt index 67f6bfb..d8c10b1 100644 --- a/app/src/main/java/lab/maxb/dark/presentation/repository/room/dao/RecognitionTaskDAO.kt +++ b/app/src/main/java/lab/maxb/dark/presentation/repository/room/dao/RecognitionTaskDAO.kt @@ -2,11 +2,9 @@ package lab.maxb.dark.presentation.repository.room.dao import androidx.paging.PagingSource import androidx.room.* -import androidx.room.OnConflictStrategy.IGNORE import androidx.room.OnConflictStrategy.REPLACE import kotlinx.coroutines.flow.Flow import lab.maxb.dark.presentation.repository.room.model.RecognitionTaskDTO -import lab.maxb.dark.presentation.repository.room.model.RecognitionTaskImageCrossref import lab.maxb.dark.presentation.repository.room.model.RecognitionTaskName import lab.maxb.dark.presentation.repository.room.relations.RecognitionTaskWithNamesAndImages import lab.maxb.dark.presentation.repository.room.relations.RecognitionTaskWithOwnerAndImage @@ -19,17 +17,12 @@ interface RecognitionTaskDAO { @Insert(onConflict = REPLACE) suspend fun addRecognitionTaskNames(names: List) - @Insert(onConflict = IGNORE) - suspend fun addRecognitionTaskImages(images: List) - @Transaction suspend fun addRecognitionTask(task: RecognitionTaskDTO, - names: List, - images: List) { + names: List) { deleteRecognitionTask(task) addRecognitionTask(task) addRecognitionTaskNames(names) - addRecognitionTaskImages(images) } @Update diff --git a/app/src/main/java/lab/maxb/dark/presentation/repository/room/model/ImageDTO.kt b/app/src/main/java/lab/maxb/dark/presentation/repository/room/model/ImageDTO.kt deleted file mode 100644 index 0c36fbe..0000000 --- a/app/src/main/java/lab/maxb/dark/presentation/repository/room/model/ImageDTO.kt +++ /dev/null @@ -1,21 +0,0 @@ -package lab.maxb.dark.presentation.repository.room.model -import androidx.room.Entity -import androidx.room.PrimaryKey -import lab.maxb.dark.domain.model.Image -import lab.maxb.dark.domain.operations.randomUUID - -@Entity(tableName = "image") -data class ImageDTO( - val path: String, - @PrimaryKey - val imageId: String = randomUUID, -) - -fun ImageDTO.toImage() = Image( - path, imageId -) - -fun Image.toImageDTO() = ImageDTO( - path, id -) - diff --git a/app/src/main/java/lab/maxb/dark/presentation/repository/room/model/RecognitionTaskDTO.kt b/app/src/main/java/lab/maxb/dark/presentation/repository/room/model/RecognitionTaskDTO.kt index 2467987..1e2c2fd 100644 --- a/app/src/main/java/lab/maxb/dark/presentation/repository/room/model/RecognitionTaskDTO.kt +++ b/app/src/main/java/lab/maxb/dark/presentation/repository/room/model/RecognitionTaskDTO.kt @@ -6,7 +6,7 @@ import androidx.room.PrimaryKey import lab.maxb.dark.domain.model.RecognitionTask @Entity(tableName = "recognition_task", - ignoredColumns = ["owner", "names", "images"], + ignoredColumns = ["owner", "names"], foreignKeys = [ ForeignKey( entity = UserDTO::class, @@ -19,11 +19,13 @@ import lab.maxb.dark.domain.model.RecognitionTask data class RecognitionTaskDTO( @PrimaryKey override var id: String, + override var images: List?, val owner_id: String, override var reviewed: Boolean = false, ): RecognitionTask(id=id) { constructor(task: RecognitionTask) : this( task.id, + task.images, task.owner!!.id, task.reviewed, ) diff --git a/app/src/main/java/lab/maxb/dark/presentation/repository/room/model/RecognitionTaskImageCrossref.kt b/app/src/main/java/lab/maxb/dark/presentation/repository/room/model/RecognitionTaskImageCrossref.kt deleted file mode 100644 index 7452b42..0000000 --- a/app/src/main/java/lab/maxb/dark/presentation/repository/room/model/RecognitionTaskImageCrossref.kt +++ /dev/null @@ -1,29 +0,0 @@ -package lab.maxb.dark.presentation.repository.room.model -import androidx.room.Entity -import androidx.room.ForeignKey -import androidx.room.ForeignKey.CASCADE - -@Entity(tableName = "recognition_task_image", - primaryKeys = ["id", "imageId"], - foreignKeys = [ - ForeignKey( - entity = RecognitionTaskDTO::class, - parentColumns = ["id"], - childColumns = ["id"], - onDelete = CASCADE, - onUpdate = CASCADE, - deferred = true - ), - ForeignKey( - entity = ImageDTO::class, - parentColumns = ["imageId"], - childColumns = ["imageId"], - onDelete = CASCADE, - deferred = true - ) - ], -) -data class RecognitionTaskImageCrossref( - var id: String, - var imageId: String, -) diff --git a/app/src/main/java/lab/maxb/dark/presentation/repository/room/relations/RecognitionTaskWithNamesAndImages.kt b/app/src/main/java/lab/maxb/dark/presentation/repository/room/relations/RecognitionTaskWithNamesAndImages.kt index d010f90..7a32aea 100644 --- a/app/src/main/java/lab/maxb/dark/presentation/repository/room/relations/RecognitionTaskWithNamesAndImages.kt +++ b/app/src/main/java/lab/maxb/dark/presentation/repository/room/relations/RecognitionTaskWithNamesAndImages.kt @@ -1,9 +1,9 @@ package lab.maxb.dark.presentation.repository.room.relations import androidx.room.Embedded -import androidx.room.Junction import androidx.room.Relation import lab.maxb.dark.domain.model.RecognitionTask -import lab.maxb.dark.presentation.repository.room.model.* +import lab.maxb.dark.presentation.repository.room.model.RecognitionTaskDTO +import lab.maxb.dark.presentation.repository.room.model.RecognitionTaskName data class RecognitionTaskWithNamesAndImages( @Embedded val recognition_task: RecognitionTaskDTO, @@ -12,15 +12,8 @@ data class RecognitionTaskWithNamesAndImages( entityColumn = "recognition_task" ) val names: List, - @Relation( - parentColumn = "id", - entityColumn = "imageId", - associateBy = Junction(RecognitionTaskImageCrossref::class) - ) - val images: List ) { fun toRecognitionTask() = recognition_task.also { task -> task.names = names.map { it.name }.toSet() - task.images = images.map { it.toImage() } } as RecognitionTask } diff --git a/app/src/main/java/lab/maxb/dark/presentation/repository/room/relations/RecognitionTaskWithOwnerAndImage.kt b/app/src/main/java/lab/maxb/dark/presentation/repository/room/relations/RecognitionTaskWithOwnerAndImage.kt index 8a1a7c8..8bee814 100644 --- a/app/src/main/java/lab/maxb/dark/presentation/repository/room/relations/RecognitionTaskWithOwnerAndImage.kt +++ b/app/src/main/java/lab/maxb/dark/presentation/repository/room/relations/RecognitionTaskWithOwnerAndImage.kt @@ -1,11 +1,11 @@ package lab.maxb.dark.presentation.repository.room.relations import androidx.room.Embedded -import androidx.room.Junction import androidx.room.Relation import lab.maxb.dark.domain.model.RecognitionTask import lab.maxb.dark.domain.model.User -import lab.maxb.dark.presentation.repository.room.model.* +import lab.maxb.dark.presentation.repository.room.model.RecognitionTaskDTO +import lab.maxb.dark.presentation.repository.room.model.UserDTO data class RecognitionTaskWithOwnerAndImage( @Embedded val recognition_task: RecognitionTaskDTO, @@ -14,16 +14,9 @@ data class RecognitionTaskWithOwnerAndImage( entityColumn = "id", entity = UserDTO::class ) - val owner: User?, - @Relation( - parentColumn = "id", - entityColumn = "imageId", - associateBy = Junction(RecognitionTaskImageCrossref::class) - ) - val image: ImageDTO? + val owner: User? ) { fun toRecognitionTask() = recognition_task.also { task -> task.owner = owner - task.images = image?.let { listOf(it.toImage()) } ?: listOf() } as RecognitionTask } diff --git a/app/src/main/java/lab/maxb/dark/presentation/view/SolveRecognitionTaskFragment.kt b/app/src/main/java/lab/maxb/dark/presentation/view/SolveRecognitionTaskFragment.kt index edf997b..1de4077 100644 --- a/app/src/main/java/lab/maxb/dark/presentation/view/SolveRecognitionTaskFragment.kt +++ b/app/src/main/java/lab/maxb/dark/presentation/view/SolveRecognitionTaskFragment.kt @@ -54,7 +54,7 @@ class SolveRecognitionTaskFragment : Fragment(R.layout.solve_recognition_task_fr } (task.images ?: listOf()).map { image -> - ItemHolder(image.path) + ItemHolder(image) }.run { mAdapter.submitList(this) } mViewModel.isReviewMode observe { diff --git a/app/src/main/java/lab/maxb/dark/presentation/view/adapter/RecognitionTaskListAdapter.kt b/app/src/main/java/lab/maxb/dark/presentation/view/adapter/RecognitionTaskListAdapter.kt index 98144ef..c2362bf 100644 --- a/app/src/main/java/lab/maxb/dark/presentation/view/adapter/RecognitionTaskListAdapter.kt +++ b/app/src/main/java/lab/maxb/dark/presentation/view/adapter/RecognitionTaskListAdapter.kt @@ -20,7 +20,7 @@ class RecognitionTaskListAdapter( val preloader = ImagePreloader( manager, getItem = { - getItem(it)?.images?.firstOrNull()?.path + getItem(it)?.images?.firstOrNull() }, getImageLoader = getImageLoader ) @@ -33,7 +33,7 @@ class RecognitionTaskListAdapter( override fun areContentsTheSame(oldItem: RecognitionTask, newItem: RecognitionTask): Boolean = oldItem.owner?.id == newItem.owner?.id && oldItem.owner?.name == newItem.owner?.name && - oldItem.images?.firstOrNull()?.id == newItem.images?.firstOrNull()?.id + oldItem.images?.firstOrNull() == newItem.images?.firstOrNull() } } @@ -52,7 +52,7 @@ class RecognitionTaskListAdapter( override fun onBindViewHolder(holder: TaskViewHolder, position: Int) = with(holder.binding) { val item = getItem(position) taskOwnerName.text = item?.owner?.name ?: "" - item?.images?.firstOrNull()?.path?.let { + item?.images?.firstOrNull()?.let { getImageLoader(manager, it).into(taskImage) } ?: manager.clear(taskImage) root.setOnClickListener { v -> diff --git a/app/src/main/java/lab/maxb/dark/presentation/viewModel/RecognitionTaskListViewModel.kt b/app/src/main/java/lab/maxb/dark/presentation/viewModel/RecognitionTaskListViewModel.kt index 21521a7..7eb7055 100644 --- a/app/src/main/java/lab/maxb/dark/presentation/viewModel/RecognitionTaskListViewModel.kt +++ b/app/src/main/java/lab/maxb/dark/presentation/viewModel/RecognitionTaskListViewModel.kt @@ -8,7 +8,6 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.flow.mapLatest import lab.maxb.dark.domain.model.isUser -import lab.maxb.dark.presentation.repository.interfaces.ImagesRepository import lab.maxb.dark.presentation.repository.interfaces.ProfileRepository import lab.maxb.dark.presentation.repository.interfaces.RecognitionTasksRepository import lab.maxb.dark.presentation.viewModel.utils.stateIn @@ -16,9 +15,8 @@ import org.koin.android.annotation.KoinViewModel @KoinViewModel class RecognitionTaskListViewModel( - recognitionTasksRepository: RecognitionTasksRepository, + private val recognitionTasksRepository: RecognitionTasksRepository, profileRepository: ProfileRepository, - private val imagesRepository: ImagesRepository, ) : ViewModel() { private val profile = profileRepository.profileState @@ -35,5 +33,5 @@ class RecognitionTaskListViewModel( it?.role?.isUser ?: false }.stateIn(false) - fun getImage(path: String) = imagesRepository.getUri(path) + fun getImage(path: String) = recognitionTasksRepository.getRecognitionTaskImage(path) } \ No newline at end of file diff --git a/app/src/main/java/lab/maxb/dark/presentation/viewModel/SolveRecognitionTaskViewModel.kt b/app/src/main/java/lab/maxb/dark/presentation/viewModel/SolveRecognitionTaskViewModel.kt index e2c1f2a..b7c48a1 100644 --- a/app/src/main/java/lab/maxb/dark/presentation/viewModel/SolveRecognitionTaskViewModel.kt +++ b/app/src/main/java/lab/maxb/dark/presentation/viewModel/SolveRecognitionTaskViewModel.kt @@ -4,7 +4,6 @@ import androidx.lifecycle.ViewModel import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.* import lab.maxb.dark.domain.model.Role -import lab.maxb.dark.presentation.repository.interfaces.ImagesRepository import lab.maxb.dark.presentation.repository.interfaces.ProfileRepository import lab.maxb.dark.presentation.repository.interfaces.RecognitionTasksRepository import lab.maxb.dark.presentation.repository.interfaces.UsersRepository @@ -17,7 +16,6 @@ import org.koin.android.annotation.KoinViewModel @KoinViewModel class SolveRecognitionTaskViewModel( private val recognitionTasksRepository: RecognitionTasksRepository, - private val imagesRepository: ImagesRepository, private val usersRepository: UsersRepository, profileRepository: ProfileRepository, ) : ViewModel() { @@ -68,5 +66,5 @@ class SolveRecognitionTaskViewModel( } } ?: false - fun getImage(path: String) = imagesRepository.getUri(path) + fun getImage(path: String) = recognitionTasksRepository.getRecognitionTaskImage(path) } \ No newline at end of file From 4a4da84da1fefba7b8502ba504615c65de7123a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=91=D0=B0=D0=B1=D1=8B=D1=88=D0=B5=D0=B2=20=D0=9C=D0=B0?= =?UTF-8?q?=D0=BA=D1=81=D0=B8=D0=BC?= Date: Sun, 29 May 2022 20:24:32 +0300 Subject: [PATCH 05/22] Move names into task entity There are no reasons to keep it separate --- .../RecognitionTasksRepositoryImpl.kt | 17 ++------- .../repository/room/LocalDatabase.kt | 8 ++--- .../repository/room/converters/Collections.kt | 24 ------------- .../room/converters/CollectionsConverter.kt | 35 +++++++++++++++++++ .../repository/room/dao/RecognitionTaskDAO.kt | 30 +++++++--------- .../room/model/RecognitionTaskDTO.kt | 6 +++- .../room/model/RecognitionTaskName.kt | 22 ------------ .../RecognitionTaskWithNamesAndImages.kt | 19 ---------- ...ndImage.kt => RecognitionTaskWithOwner.kt} | 2 +- .../pagination/RecognitionTasksMediator.kt | 14 ++++---- 10 files changed, 66 insertions(+), 111 deletions(-) delete mode 100644 app/src/main/java/lab/maxb/dark/presentation/repository/room/converters/Collections.kt create mode 100644 app/src/main/java/lab/maxb/dark/presentation/repository/room/converters/CollectionsConverter.kt delete mode 100644 app/src/main/java/lab/maxb/dark/presentation/repository/room/model/RecognitionTaskName.kt delete mode 100644 app/src/main/java/lab/maxb/dark/presentation/repository/room/relations/RecognitionTaskWithNamesAndImages.kt rename app/src/main/java/lab/maxb/dark/presentation/repository/room/relations/{RecognitionTaskWithOwnerAndImage.kt => RecognitionTaskWithOwner.kt} (93%) diff --git a/app/src/main/java/lab/maxb/dark/presentation/repository/implementation/RecognitionTasksRepositoryImpl.kt b/app/src/main/java/lab/maxb/dark/presentation/repository/implementation/RecognitionTasksRepositoryImpl.kt index adeacc1..975183c 100644 --- a/app/src/main/java/lab/maxb/dark/presentation/repository/implementation/RecognitionTasksRepositoryImpl.kt +++ b/app/src/main/java/lab/maxb/dark/presentation/repository/implementation/RecognitionTasksRepositoryImpl.kt @@ -19,7 +19,6 @@ import lab.maxb.dark.presentation.repository.network.dark.model.RecognitionTaskC import lab.maxb.dark.presentation.repository.room.LocalDatabase import lab.maxb.dark.presentation.repository.room.dao.RecognitionTaskDAO import lab.maxb.dark.presentation.repository.room.model.RecognitionTaskDTO -import lab.maxb.dark.presentation.repository.room.model.RecognitionTaskName import lab.maxb.dark.presentation.repository.utils.Resource import lab.maxb.dark.presentation.repository.utils.pagination.Page import lab.maxb.dark.presentation.repository.utils.pagination.RecognitionTaskMediator @@ -55,7 +54,7 @@ class RecognitionTasksRepositoryImpl( localStore = { db.withTransaction { it.forEach { task -> - mRecognitionTaskDao.addRecognitionTask( + mRecognitionTaskDao.save( RecognitionTaskDTO(task) ) } @@ -96,12 +95,7 @@ class RecognitionTasksRepositoryImpl( )!! } - mRecognitionTaskDao.addRecognitionTask( - taskLocal, - task.names!!.map { - RecognitionTaskName(taskLocal.id, it) - } - ) + mRecognitionTaskDao.save(taskLocal) } override suspend fun markRecognitionTask(task: RecognitionTask) { @@ -149,12 +143,7 @@ class RecognitionTasksRepositoryImpl( } } localStore = { task -> - mRecognitionTaskDao.addRecognitionTask( - RecognitionTaskDTO(task), - task.names!!.map { name -> - RecognitionTaskName(task.id, name) - }, - ) + mRecognitionTaskDao.save(RecognitionTaskDTO(task)) } clearLocalStore = { mRecognitionTaskDao.deleteRecognitionTask(it) diff --git a/app/src/main/java/lab/maxb/dark/presentation/repository/room/LocalDatabase.kt b/app/src/main/java/lab/maxb/dark/presentation/repository/room/LocalDatabase.kt index 6d9c868..e40f63f 100644 --- a/app/src/main/java/lab/maxb/dark/presentation/repository/room/LocalDatabase.kt +++ b/app/src/main/java/lab/maxb/dark/presentation/repository/room/LocalDatabase.kt @@ -6,24 +6,22 @@ import androidx.room.Room import androidx.room.RoomDatabase import androidx.room.TypeConverters import lab.maxb.dark.presentation.repository.room.Server.Model.ProfileDTO -import lab.maxb.dark.presentation.repository.room.converters.ListConverter +import lab.maxb.dark.presentation.repository.room.converters.CollectionsConverter import lab.maxb.dark.presentation.repository.room.dao.ProfileDAO import lab.maxb.dark.presentation.repository.room.dao.RecognitionTaskDAO import lab.maxb.dark.presentation.repository.room.dao.RemoteKeysDAO import lab.maxb.dark.presentation.repository.room.dao.UserDAO import lab.maxb.dark.presentation.repository.room.model.RecognitionTaskDTO -import lab.maxb.dark.presentation.repository.room.model.RecognitionTaskName import lab.maxb.dark.presentation.repository.room.model.RemoteKeys import lab.maxb.dark.presentation.repository.room.model.UserDTO @Database(entities = [ UserDTO::class, RecognitionTaskDTO::class, - RecognitionTaskName::class, ProfileDTO::class, RemoteKeys::class, - ], version = 6, exportSchema = false) -@TypeConverters(ListConverter::class) + ], version = 7, exportSchema = false) +@TypeConverters(CollectionsConverter::class) abstract class LocalDatabase : RoomDatabase() { abstract fun recognitionTaskDao(): RecognitionTaskDAO abstract fun userDao(): UserDAO diff --git a/app/src/main/java/lab/maxb/dark/presentation/repository/room/converters/Collections.kt b/app/src/main/java/lab/maxb/dark/presentation/repository/room/converters/Collections.kt deleted file mode 100644 index 185c178..0000000 --- a/app/src/main/java/lab/maxb/dark/presentation/repository/room/converters/Collections.kt +++ /dev/null @@ -1,24 +0,0 @@ -package lab.maxb.dark.presentation.repository.room.converters - -import androidx.room.TypeConverter -import com.google.gson.Gson -import com.google.gson.reflect.TypeToken -import java.lang.reflect.Type - - -class ListConverter { - @TypeConverter - fun fromList(list: List?) = list?.let { - gson.toJson(list, type) - } - - @TypeConverter - fun toList(list: String?) = list?.let { - gson.fromJson>(list, type) - } - - companion object { - private val gson = Gson() - private val type: Type = object : TypeToken?>() {}.type - } -} \ No newline at end of file diff --git a/app/src/main/java/lab/maxb/dark/presentation/repository/room/converters/CollectionsConverter.kt b/app/src/main/java/lab/maxb/dark/presentation/repository/room/converters/CollectionsConverter.kt new file mode 100644 index 0000000..fd8a83b --- /dev/null +++ b/app/src/main/java/lab/maxb/dark/presentation/repository/room/converters/CollectionsConverter.kt @@ -0,0 +1,35 @@ +package lab.maxb.dark.presentation.repository.room.converters + +import androidx.room.TypeConverter +import com.google.gson.Gson +import com.google.gson.reflect.TypeToken +import java.lang.reflect.Type + + +class CollectionsConverter { + @TypeConverter + fun fromList(list: List?) = list?.let { + gson.toJson(list, typeList) + } + + @TypeConverter + fun toList(list: String?) = list?.let { + gson.fromJson>(list, typeList) + } + + @TypeConverter + fun fromSet(list: Set?) = list?.let { + gson.toJson(list, typeSet) + } + + @TypeConverter + fun toSet(list: String?) = list?.let { + gson.fromJson>(list, typeSet) + } + + companion object { + private val gson = Gson() + private val typeList: Type = object : TypeToken?>() {}.type + private val typeSet: Type = object : TypeToken?>() {}.type + } +} \ No newline at end of file diff --git a/app/src/main/java/lab/maxb/dark/presentation/repository/room/dao/RecognitionTaskDAO.kt b/app/src/main/java/lab/maxb/dark/presentation/repository/room/dao/RecognitionTaskDAO.kt index d8c10b1..7211c92 100644 --- a/app/src/main/java/lab/maxb/dark/presentation/repository/room/dao/RecognitionTaskDAO.kt +++ b/app/src/main/java/lab/maxb/dark/presentation/repository/room/dao/RecognitionTaskDAO.kt @@ -5,29 +5,23 @@ import androidx.room.* import androidx.room.OnConflictStrategy.REPLACE import kotlinx.coroutines.flow.Flow import lab.maxb.dark.presentation.repository.room.model.RecognitionTaskDTO -import lab.maxb.dark.presentation.repository.room.model.RecognitionTaskName -import lab.maxb.dark.presentation.repository.room.relations.RecognitionTaskWithNamesAndImages -import lab.maxb.dark.presentation.repository.room.relations.RecognitionTaskWithOwnerAndImage +import lab.maxb.dark.presentation.repository.room.model.UserDTO +import lab.maxb.dark.presentation.repository.room.relations.RecognitionTaskWithOwner @Dao interface RecognitionTaskDAO { - @Insert(onConflict = REPLACE) - suspend fun addRecognitionTask(task: RecognitionTaskDTO) + @Insert(onConflict = OnConflictStrategy.IGNORE) + suspend fun addRecognitionTask(task: RecognitionTaskDTO): Long - @Insert(onConflict = REPLACE) - suspend fun addRecognitionTaskNames(names: List) + @Update + suspend fun updateRecognitionTask(task: RecognitionTaskDTO) @Transaction - suspend fun addRecognitionTask(task: RecognitionTaskDTO, - names: List) { - deleteRecognitionTask(task) - addRecognitionTask(task) - addRecognitionTaskNames(names) + suspend fun save(task: RecognitionTaskDTO) { + if (addRecognitionTask(task) == -1L) + updateRecognitionTask(task) } - @Update - suspend fun updateRecognitionTask(task: RecognitionTaskDTO) - @Delete suspend fun deleteRecognitionTask(task: RecognitionTaskDTO) @@ -36,15 +30,15 @@ interface RecognitionTaskDAO { @Transaction @Query("SELECT * FROM recognition_task ORDER BY reviewed") - fun getAllRecognitionTasks(): Flow?> + fun getAllRecognitionTasks(): Flow?> @Transaction @Query("SELECT * FROM recognition_task ORDER BY reviewed") - fun getAllRecognitionTasksPaged(): PagingSource + fun getAllRecognitionTasksPaged(): PagingSource @Transaction @Query("SELECT * FROM recognition_task WHERE id = :id") - fun getRecognitionTask(id: String): Flow + fun getRecognitionTask(id: String): Flow @Transaction @Query("DELETE FROM recognition_task") diff --git a/app/src/main/java/lab/maxb/dark/presentation/repository/room/model/RecognitionTaskDTO.kt b/app/src/main/java/lab/maxb/dark/presentation/repository/room/model/RecognitionTaskDTO.kt index 1e2c2fd..da2a288 100644 --- a/app/src/main/java/lab/maxb/dark/presentation/repository/room/model/RecognitionTaskDTO.kt +++ b/app/src/main/java/lab/maxb/dark/presentation/repository/room/model/RecognitionTaskDTO.kt @@ -6,7 +6,7 @@ import androidx.room.PrimaryKey import lab.maxb.dark.domain.model.RecognitionTask @Entity(tableName = "recognition_task", - ignoredColumns = ["owner", "names"], + ignoredColumns = ["owner"], foreignKeys = [ ForeignKey( entity = UserDTO::class, @@ -19,14 +19,18 @@ import lab.maxb.dark.domain.model.RecognitionTask data class RecognitionTaskDTO( @PrimaryKey override var id: String, + override var names: Set?, override var images: List?, val owner_id: String, override var reviewed: Boolean = false, ): RecognitionTask(id=id) { constructor(task: RecognitionTask) : this( task.id, + task.names, task.images, task.owner!!.id, task.reviewed, ) + + fun toRecognitionTask() = this as RecognitionTask } \ No newline at end of file diff --git a/app/src/main/java/lab/maxb/dark/presentation/repository/room/model/RecognitionTaskName.kt b/app/src/main/java/lab/maxb/dark/presentation/repository/room/model/RecognitionTaskName.kt deleted file mode 100644 index 8fe0bb9..0000000 --- a/app/src/main/java/lab/maxb/dark/presentation/repository/room/model/RecognitionTaskName.kt +++ /dev/null @@ -1,22 +0,0 @@ -package lab.maxb.dark.presentation.repository.room.model -import androidx.room.Entity -import androidx.room.ForeignKey -import androidx.room.ForeignKey.CASCADE - -@Entity(tableName = "recognition_task_name", - foreignKeys = [ - ForeignKey( - entity = RecognitionTaskDTO::class, - parentColumns = ["id"], - childColumns = ["recognition_task"], - onDelete = CASCADE, - onUpdate = CASCADE, - deferred = true - ) - ], - primaryKeys = ["recognition_task", "name"] -) -data class RecognitionTaskName( - val recognition_task: String, - val name: String, -) diff --git a/app/src/main/java/lab/maxb/dark/presentation/repository/room/relations/RecognitionTaskWithNamesAndImages.kt b/app/src/main/java/lab/maxb/dark/presentation/repository/room/relations/RecognitionTaskWithNamesAndImages.kt deleted file mode 100644 index 7a32aea..0000000 --- a/app/src/main/java/lab/maxb/dark/presentation/repository/room/relations/RecognitionTaskWithNamesAndImages.kt +++ /dev/null @@ -1,19 +0,0 @@ -package lab.maxb.dark.presentation.repository.room.relations -import androidx.room.Embedded -import androidx.room.Relation -import lab.maxb.dark.domain.model.RecognitionTask -import lab.maxb.dark.presentation.repository.room.model.RecognitionTaskDTO -import lab.maxb.dark.presentation.repository.room.model.RecognitionTaskName - -data class RecognitionTaskWithNamesAndImages( - @Embedded val recognition_task: RecognitionTaskDTO, - @Relation( - parentColumn = "id", - entityColumn = "recognition_task" - ) - val names: List, -) { - fun toRecognitionTask() = recognition_task.also { task -> - task.names = names.map { it.name }.toSet() - } as RecognitionTask -} diff --git a/app/src/main/java/lab/maxb/dark/presentation/repository/room/relations/RecognitionTaskWithOwnerAndImage.kt b/app/src/main/java/lab/maxb/dark/presentation/repository/room/relations/RecognitionTaskWithOwner.kt similarity index 93% rename from app/src/main/java/lab/maxb/dark/presentation/repository/room/relations/RecognitionTaskWithOwnerAndImage.kt rename to app/src/main/java/lab/maxb/dark/presentation/repository/room/relations/RecognitionTaskWithOwner.kt index 8bee814..8f2662b 100644 --- a/app/src/main/java/lab/maxb/dark/presentation/repository/room/relations/RecognitionTaskWithOwnerAndImage.kt +++ b/app/src/main/java/lab/maxb/dark/presentation/repository/room/relations/RecognitionTaskWithOwner.kt @@ -7,7 +7,7 @@ import lab.maxb.dark.domain.model.User import lab.maxb.dark.presentation.repository.room.model.RecognitionTaskDTO import lab.maxb.dark.presentation.repository.room.model.UserDTO -data class RecognitionTaskWithOwnerAndImage( +data class RecognitionTaskWithOwner( @Embedded val recognition_task: RecognitionTaskDTO, @Relation( parentColumn = "owner_id", diff --git a/app/src/main/java/lab/maxb/dark/presentation/repository/utils/pagination/RecognitionTasksMediator.kt b/app/src/main/java/lab/maxb/dark/presentation/repository/utils/pagination/RecognitionTasksMediator.kt index d3dddc4..d4c338f 100644 --- a/app/src/main/java/lab/maxb/dark/presentation/repository/utils/pagination/RecognitionTasksMediator.kt +++ b/app/src/main/java/lab/maxb/dark/presentation/repository/utils/pagination/RecognitionTasksMediator.kt @@ -8,7 +8,7 @@ import kotlinx.coroutines.flow.firstOrNull import lab.maxb.dark.domain.model.RecognitionTask import lab.maxb.dark.presentation.repository.room.dao.RemoteKeysDAO import lab.maxb.dark.presentation.repository.room.model.RemoteKeys -import lab.maxb.dark.presentation.repository.room.relations.RecognitionTaskWithOwnerAndImage +import lab.maxb.dark.presentation.repository.room.relations.RecognitionTaskWithOwner import lab.maxb.dark.presentation.repository.utils.BaseResource import retrofit2.HttpException import java.io.IOException @@ -19,7 +19,7 @@ import java.io.InvalidObjectException class RecognitionTaskMediator( private val resource: BaseResource>, private val remoteKeys: RemoteKeysDAO, -) : RemoteMediator() { +) : RemoteMediator() { override suspend fun initialize() = if (resource.isFresh(Page(0, 1))) InitializeAction.SKIP_INITIAL_REFRESH @@ -28,7 +28,7 @@ class RecognitionTaskMediator( override suspend fun load( loadType: LoadType, - state: PagingState + state: PagingState ): MediatorResult { return try { val pageKeyData = getKeyPageData(loadType, state) @@ -61,21 +61,21 @@ class RecognitionTaskMediator( } } - private suspend fun getFirstRemoteKey(state: PagingState): RemoteKeys? { + private suspend fun getFirstRemoteKey(state: PagingState): RemoteKeys? { return state.pages .firstOrNull { it.data.isNotEmpty() } ?.data?.firstOrNull() ?.let { doggo -> remoteKeys.getById(doggo.recognition_task.id) } } - private suspend fun getLastRemoteKey(state: PagingState): RemoteKeys? { + private suspend fun getLastRemoteKey(state: PagingState): RemoteKeys? { return state.pages .lastOrNull { it.data.isNotEmpty() } ?.data?.lastOrNull() ?.let { doggo -> remoteKeys.getById(doggo.recognition_task.id) } } - private suspend fun getClosestRemoteKey(state: PagingState): RemoteKeys? { + private suspend fun getClosestRemoteKey(state: PagingState): RemoteKeys? { return state.anchorPosition?.let { position -> state.closestItemToPosition(position)?.recognition_task?.id?.let { id -> remoteKeys.getById(id) @@ -83,7 +83,7 @@ class RecognitionTaskMediator( } } - private suspend fun getKeyPageData(loadType: LoadType, state: PagingState): Any? { + private suspend fun getKeyPageData(loadType: LoadType, state: PagingState): Any? { return when (loadType) { LoadType.REFRESH -> { val remoteKeys = getClosestRemoteKey(state) From 72b94f73033ca5d93210e3ca29f6fd6fec0b50eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=91=D0=B0=D0=B1=D1=8B=D1=88=D0=B5=D0=B2=20=D0=9C=D0=B0?= =?UTF-8?q?=D0=BA=D1=81=D0=B8=D0=BC?= Date: Sun, 29 May 2022 22:43:56 +0300 Subject: [PATCH 06/22] + Image loading animation --- .../view/AddRecognitionTaskFragment.kt | 5 +- .../view/RecognitionTaskListFragment.kt | 14 +++++- .../view/SolveRecognitionTaskFragment.kt | 12 ++++- app/src/main/res/drawable/ic_error.xml | 8 ++++ app/src/main/res/drawable/loading_vector.xml | 47 +++++++++++++++++++ app/src/main/res/layout/main_fragment.xml | 1 - .../res/values/string_darwable_consts.xml | 4 ++ 7 files changed, 87 insertions(+), 4 deletions(-) create mode 100644 app/src/main/res/drawable/ic_error.xml create mode 100644 app/src/main/res/drawable/loading_vector.xml create mode 100644 app/src/main/res/values/string_darwable_consts.xml diff --git a/app/src/main/java/lab/maxb/dark/presentation/view/AddRecognitionTaskFragment.kt b/app/src/main/java/lab/maxb/dark/presentation/view/AddRecognitionTaskFragment.kt index 7092856..9ccc66a 100644 --- a/app/src/main/java/lab/maxb/dark/presentation/view/AddRecognitionTaskFragment.kt +++ b/app/src/main/java/lab/maxb/dark/presentation/view/AddRecognitionTaskFragment.kt @@ -15,6 +15,7 @@ import androidx.core.net.toUri import androidx.core.view.isVisible import androidx.fragment.app.Fragment import com.bumptech.glide.Glide +import com.bumptech.glide.RequestManager import lab.maxb.dark.R import lab.maxb.dark.databinding.AddRecognitionTaskFragmentBinding import lab.maxb.dark.domain.operations.unicname @@ -32,6 +33,7 @@ class AddRecognitionTaskFragment : Fragment(R.layout.add_recognition_task_fragme private val mViewModel: AddRecognitionTaskViewModel by sharedViewModel() private val mBinding: AddRecognitionTaskFragmentBinding by viewBinding() private var mInputsAdapter: InputListAdapter by autoCleaned() + private var mGlide: RequestManager by autoCleaned() private var mImagesAdapter: ImageSliderAdapter by autoCleaned() override fun onViewCreated(view: View, savedInstanceState: Bundle?) { @@ -45,9 +47,10 @@ class AddRecognitionTaskFragment : Fragment(R.layout.add_recognition_task_fragme } private fun setupImageUploadPanel() = with (mBinding) { - val mGlide = Glide.with(this@AddRecognitionTaskFragment) + mGlide = Glide.with(this@AddRecognitionTaskFragment) mImagesAdapter = ImageSliderAdapter { mGlide.load(it.toUri()) + .error(R.drawable.ic_error) } imageSlider.adapter = mImagesAdapter diff --git a/app/src/main/java/lab/maxb/dark/presentation/view/RecognitionTaskListFragment.kt b/app/src/main/java/lab/maxb/dark/presentation/view/RecognitionTaskListFragment.kt index 40d22e4..2e1c8aa 100644 --- a/app/src/main/java/lab/maxb/dark/presentation/view/RecognitionTaskListFragment.kt +++ b/app/src/main/java/lab/maxb/dark/presentation/view/RecognitionTaskListFragment.kt @@ -1,10 +1,13 @@ package lab.maxb.dark.presentation.view +import android.graphics.drawable.AnimatedVectorDrawable import android.os.Bundle import android.view.View +import androidx.appcompat.content.res.AppCompatResources import androidx.core.view.isVisible import androidx.fragment.app.Fragment import com.bumptech.glide.Glide +import com.bumptech.glide.RequestManager import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions.withCrossFade import lab.maxb.dark.R import lab.maxb.dark.databinding.RecognitionTaskListFragmentBinding @@ -22,6 +25,7 @@ class RecognitionTaskListFragment : Fragment(R.layout.recognition_task_list_frag private val mViewModel: RecognitionTaskListViewModel by sharedViewModel() private val mBinding: RecognitionTaskListFragmentBinding by viewBinding() private var mAdapter: RecognitionTaskListAdapter by autoCleaned() + private var mPlaceholder: AnimatedVectorDrawable by autoCleaned() override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) @@ -31,8 +35,16 @@ class RecognitionTaskListFragment : Fragment(R.layout.recognition_task_list_frag mBinding.fab.setOnClickListener { RecognitionTaskListFragmentDirections.navToAddTaskFragment().navigate() } + mPlaceholder = AppCompatResources.getDrawable( + requireContext(), + R.drawable.loading_vector + ) as AnimatedVectorDrawable + mPlaceholder.start() mAdapter = RecognitionTaskListAdapter(Glide.with(this)) { - load(mViewModel.getImage(it)).transition(withCrossFade()) + load(mViewModel.getImage(it)) + .transition(withCrossFade()) + .placeholder(mPlaceholder) + .error(R.drawable.ic_error) } mBinding.recognitionTaskListRecycler.adapter = mAdapter mBinding.recognitionTaskListRecycler.addOnScrollListener(mAdapter.preloader.raw) diff --git a/app/src/main/java/lab/maxb/dark/presentation/view/SolveRecognitionTaskFragment.kt b/app/src/main/java/lab/maxb/dark/presentation/view/SolveRecognitionTaskFragment.kt index 1de4077..d27a4cf 100644 --- a/app/src/main/java/lab/maxb/dark/presentation/view/SolveRecognitionTaskFragment.kt +++ b/app/src/main/java/lab/maxb/dark/presentation/view/SolveRecognitionTaskFragment.kt @@ -1,16 +1,20 @@ package lab.maxb.dark.presentation.view import android.content.Intent +import android.graphics.drawable.AnimatedVectorDrawable import android.os.Bundle import android.view.Menu import android.view.MenuInflater import android.view.MenuItem import android.view.View import android.widget.Toast +import androidx.appcompat.content.res.AppCompatResources.getDrawable +import androidx.appcompat.widget.AppCompatDrawableManager import androidx.core.view.isVisible import androidx.fragment.app.Fragment import androidx.navigation.fragment.navArgs import com.bumptech.glide.Glide +import com.bumptech.glide.RequestManager import com.wada811.databinding.dataBinding import lab.maxb.dark.R import lab.maxb.dark.databinding.SolveRecognitionTaskFragmentBinding @@ -27,15 +31,21 @@ class SolveRecognitionTaskFragment : Fragment(R.layout.solve_recognition_task_fr private val mViewModel: SolveRecognitionTaskViewModel by sharedViewModel() private val mBinding: SolveRecognitionTaskFragmentBinding by dataBinding() private var mAdapter: ImageSliderAdapter by autoCleaned() + private var mGlide: RequestManager by autoCleaned() + private var mPlaceholder: AnimatedVectorDrawable by autoCleaned() private val args: SolveRecognitionTaskFragmentArgs by navArgs() override fun onViewCreated(view: View, savedInstanceState: Bundle?): Unit = with(mBinding) { super.onViewCreated(view, savedInstanceState) mViewModel.init(args.id) data = mViewModel - val mGlide = Glide.with(this@SolveRecognitionTaskFragment) + mGlide = Glide.with(this@SolveRecognitionTaskFragment) + mPlaceholder = getDrawable(requireContext(), R.drawable.loading_vector) as AnimatedVectorDrawable + mPlaceholder.start() mAdapter = ImageSliderAdapter { mGlide.load(mViewModel.getImage(it)) + .placeholder(mPlaceholder) + .error(R.drawable.ic_error) } imageSlider.adapter = mAdapter checkAnswer.setOnClickListener { diff --git a/app/src/main/res/drawable/ic_error.xml b/app/src/main/res/drawable/ic_error.xml new file mode 100644 index 0000000..cab3486 --- /dev/null +++ b/app/src/main/res/drawable/ic_error.xml @@ -0,0 +1,8 @@ + + + diff --git a/app/src/main/res/drawable/loading_vector.xml b/app/src/main/res/drawable/loading_vector.xml new file mode 100644 index 0000000..96fea0c --- /dev/null +++ b/app/src/main/res/drawable/loading_vector.xml @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/main_fragment.xml b/app/src/main/res/layout/main_fragment.xml index c7c77ee..c2aec14 100644 --- a/app/src/main/res/layout/main_fragment.xml +++ b/app/src/main/res/layout/main_fragment.xml @@ -31,5 +31,4 @@ app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/welcome_label" /> - \ No newline at end of file diff --git a/app/src/main/res/values/string_darwable_consts.xml b/app/src/main/res/values/string_darwable_consts.xml new file mode 100644 index 0000000..f6091c8 --- /dev/null +++ b/app/src/main/res/values/string_darwable_consts.xml @@ -0,0 +1,4 @@ + + + M0,0h12v12h-12z + \ No newline at end of file From b183ecd3c71bc8efb2535ab5fa52698c4e7ef872 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=91=D0=B0=D0=B1=D1=8B=D1=88=D0=B5=D0=B2=20=D0=9C=D0=B0?= =?UTF-8?q?=D0=BA=D1=81=D0=B8=D0=BC?= Date: Sun, 29 May 2022 23:01:05 +0300 Subject: [PATCH 07/22] Use Glide generated API --- app/build.gradle | 3 ++- .../java/lab/maxb/dark/presentation/extra/ImageHelper.kt | 6 ++++++ .../presentation/repository/room/dao/RecognitionTaskDAO.kt | 2 -- .../dark/presentation/view/AddRecognitionTaskFragment.kt | 4 ++-- .../dark/presentation/view/RecognitionTaskListFragment.kt | 5 ++--- .../dark/presentation/view/SolveRecognitionTaskFragment.kt | 5 ++--- 6 files changed, 14 insertions(+), 11 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 440a8d0..17fb2f2 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -137,7 +137,7 @@ dependencies { // Desugaring (Time-related features support for API 21+) coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5' - // Images + // Images with Glide def glide_version = "4.13.0" implementation "com.github.bumptech.glide:glide:$glide_version" annotationProcessor "com.github.bumptech.glide:compiler:$glide_version" @@ -146,6 +146,7 @@ dependencies { // Excludes the support library because it's already included by Glide. transitive = false } + kapt "com.github.bumptech.glide:compiler:$glide_version" // Tests testImplementation 'junit:junit:4.13.2' diff --git a/app/src/main/java/lab/maxb/dark/presentation/extra/ImageHelper.kt b/app/src/main/java/lab/maxb/dark/presentation/extra/ImageHelper.kt index 329cd5c..350941a 100644 --- a/app/src/main/java/lab/maxb/dark/presentation/extra/ImageHelper.kt +++ b/app/src/main/java/lab/maxb/dark/presentation/extra/ImageHelper.kt @@ -3,6 +3,8 @@ package lab.maxb.dark.presentation.extra import android.content.Context import android.content.Intent import android.net.Uri +import com.bumptech.glide.annotation.GlideModule +import com.bumptech.glide.module.AppGlideModule import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import okhttp3.MediaType.Companion.toMediaTypeOrNull @@ -10,6 +12,10 @@ import okhttp3.MultipartBody import okhttp3.RequestBody.Companion.toRequestBody import org.koin.core.annotation.Single + +@GlideModule +class MyAppGlideModule : AppGlideModule() + fun Uri.takePersistablePermission(context: Context) { context.applicationContext.contentResolver.takePersistableUriPermission(this, Intent.FLAG_GRANT_READ_URI_PERMISSION) diff --git a/app/src/main/java/lab/maxb/dark/presentation/repository/room/dao/RecognitionTaskDAO.kt b/app/src/main/java/lab/maxb/dark/presentation/repository/room/dao/RecognitionTaskDAO.kt index 7211c92..5b7d2b6 100644 --- a/app/src/main/java/lab/maxb/dark/presentation/repository/room/dao/RecognitionTaskDAO.kt +++ b/app/src/main/java/lab/maxb/dark/presentation/repository/room/dao/RecognitionTaskDAO.kt @@ -2,10 +2,8 @@ package lab.maxb.dark.presentation.repository.room.dao import androidx.paging.PagingSource import androidx.room.* -import androidx.room.OnConflictStrategy.REPLACE import kotlinx.coroutines.flow.Flow import lab.maxb.dark.presentation.repository.room.model.RecognitionTaskDTO -import lab.maxb.dark.presentation.repository.room.model.UserDTO import lab.maxb.dark.presentation.repository.room.relations.RecognitionTaskWithOwner @Dao diff --git a/app/src/main/java/lab/maxb/dark/presentation/view/AddRecognitionTaskFragment.kt b/app/src/main/java/lab/maxb/dark/presentation/view/AddRecognitionTaskFragment.kt index 9ccc66a..feabdca 100644 --- a/app/src/main/java/lab/maxb/dark/presentation/view/AddRecognitionTaskFragment.kt +++ b/app/src/main/java/lab/maxb/dark/presentation/view/AddRecognitionTaskFragment.kt @@ -14,11 +14,11 @@ import androidx.activity.result.contract.ActivityResultContracts import androidx.core.net.toUri import androidx.core.view.isVisible import androidx.fragment.app.Fragment -import com.bumptech.glide.Glide import com.bumptech.glide.RequestManager import lab.maxb.dark.R import lab.maxb.dark.databinding.AddRecognitionTaskFragmentBinding import lab.maxb.dark.domain.operations.unicname +import lab.maxb.dark.presentation.extra.GlideApp import lab.maxb.dark.presentation.extra.delegates.autoCleaned import lab.maxb.dark.presentation.extra.delegates.viewBinding import lab.maxb.dark.presentation.extra.goBack @@ -47,7 +47,7 @@ class AddRecognitionTaskFragment : Fragment(R.layout.add_recognition_task_fragme } private fun setupImageUploadPanel() = with (mBinding) { - mGlide = Glide.with(this@AddRecognitionTaskFragment) + mGlide = GlideApp.with(this@AddRecognitionTaskFragment) mImagesAdapter = ImageSliderAdapter { mGlide.load(it.toUri()) .error(R.drawable.ic_error) diff --git a/app/src/main/java/lab/maxb/dark/presentation/view/RecognitionTaskListFragment.kt b/app/src/main/java/lab/maxb/dark/presentation/view/RecognitionTaskListFragment.kt index 2e1c8aa..ae5a105 100644 --- a/app/src/main/java/lab/maxb/dark/presentation/view/RecognitionTaskListFragment.kt +++ b/app/src/main/java/lab/maxb/dark/presentation/view/RecognitionTaskListFragment.kt @@ -6,12 +6,11 @@ import android.view.View import androidx.appcompat.content.res.AppCompatResources import androidx.core.view.isVisible import androidx.fragment.app.Fragment -import com.bumptech.glide.Glide -import com.bumptech.glide.RequestManager import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions.withCrossFade import lab.maxb.dark.R import lab.maxb.dark.databinding.RecognitionTaskListFragmentBinding import lab.maxb.dark.domain.model.RecognitionTask +import lab.maxb.dark.presentation.extra.GlideApp import lab.maxb.dark.presentation.extra.delegates.autoCleaned import lab.maxb.dark.presentation.extra.delegates.viewBinding import lab.maxb.dark.presentation.extra.navigate @@ -40,7 +39,7 @@ class RecognitionTaskListFragment : Fragment(R.layout.recognition_task_list_frag R.drawable.loading_vector ) as AnimatedVectorDrawable mPlaceholder.start() - mAdapter = RecognitionTaskListAdapter(Glide.with(this)) { + mAdapter = RecognitionTaskListAdapter(GlideApp.with(this)) { load(mViewModel.getImage(it)) .transition(withCrossFade()) .placeholder(mPlaceholder) diff --git a/app/src/main/java/lab/maxb/dark/presentation/view/SolveRecognitionTaskFragment.kt b/app/src/main/java/lab/maxb/dark/presentation/view/SolveRecognitionTaskFragment.kt index d27a4cf..83a8895 100644 --- a/app/src/main/java/lab/maxb/dark/presentation/view/SolveRecognitionTaskFragment.kt +++ b/app/src/main/java/lab/maxb/dark/presentation/view/SolveRecognitionTaskFragment.kt @@ -9,15 +9,14 @@ import android.view.MenuItem import android.view.View import android.widget.Toast import androidx.appcompat.content.res.AppCompatResources.getDrawable -import androidx.appcompat.widget.AppCompatDrawableManager import androidx.core.view.isVisible import androidx.fragment.app.Fragment import androidx.navigation.fragment.navArgs -import com.bumptech.glide.Glide import com.bumptech.glide.RequestManager import com.wada811.databinding.dataBinding import lab.maxb.dark.R import lab.maxb.dark.databinding.SolveRecognitionTaskFragmentBinding +import lab.maxb.dark.presentation.extra.GlideApp import lab.maxb.dark.presentation.extra.delegates.autoCleaned import lab.maxb.dark.presentation.extra.goBack import lab.maxb.dark.presentation.extra.launchRepeatingOnLifecycle @@ -39,7 +38,7 @@ class SolveRecognitionTaskFragment : Fragment(R.layout.solve_recognition_task_fr super.onViewCreated(view, savedInstanceState) mViewModel.init(args.id) data = mViewModel - mGlide = Glide.with(this@SolveRecognitionTaskFragment) + mGlide = GlideApp.with(this@SolveRecognitionTaskFragment) mPlaceholder = getDrawable(requireContext(), R.drawable.loading_vector) as AnimatedVectorDrawable mPlaceholder.start() mAdapter = ImageSliderAdapter { From cc2d264cb69d265025457b419c05778bc5869173 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=91=D0=B0=D0=B1=D1=8B=D1=88=D0=B5=D0=B2=20=D0=9C=D0=B0?= =?UTF-8?q?=D0=BA=D1=81=D0=B8=D0=BC?= Date: Mon, 30 May 2022 15:19:51 +0300 Subject: [PATCH 08/22] Use hidden master key instead of default one --- app/build.gradle | 3 +++ .../java/lab/maxb/dark/presentation/extra/UserSettings.kt | 5 +++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 17fb2f2..d8bdc9d 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -26,6 +26,9 @@ android { // Google Client ID buildConfigField "String", "GOOGLE_CLIENT_ID", keystoreProperties["GOOGLE_CLIENT_ID"] + // MasterKey for encrypted SharedPreference + buildConfigField "String", "SECURE_PREFS_MASTER_KEY", keystoreProperties["SECURE_PREFS_MASTER_KEY"] + // URL for main API buildConfigField "String", "DARK_API_URL", keystoreProperties["DARK_API_URL"] diff --git a/app/src/main/java/lab/maxb/dark/presentation/extra/UserSettings.kt b/app/src/main/java/lab/maxb/dark/presentation/extra/UserSettings.kt index 3e9f9c4..783db0e 100644 --- a/app/src/main/java/lab/maxb/dark/presentation/extra/UserSettings.kt +++ b/app/src/main/java/lab/maxb/dark/presentation/extra/UserSettings.kt @@ -5,6 +5,7 @@ import android.content.SharedPreferences import androidx.preference.PreferenceManager import androidx.security.crypto.EncryptedSharedPreferences import androidx.security.crypto.MasterKey +import lab.maxb.dark.BuildConfig import lab.maxb.dark.presentation.extra.delegates.property import org.koin.core.annotation.Single @@ -13,8 +14,8 @@ class UserSettings(context: Context) { private val pref = PreferenceManager.getDefaultSharedPreferences(context) private val securePref: SharedPreferences = EncryptedSharedPreferences.create( context, - "secure_dark_preferences", - MasterKey.Builder(context).setKeyScheme(MasterKey.KeyScheme.AES256_GCM).build(), + "secure_dark.preferences", + MasterKey.Builder(context, BuildConfig.SECURE_PREFS_MASTER_KEY).setKeyScheme(MasterKey.KeyScheme.AES256_GCM).build(), EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV, EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM ) From 696395632fc0021d44cce5b51439ce9d8d641d98 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=91=D0=B0=D0=B1=D1=8B=D1=88=D0=B5=D0=B2=20=D0=9C=D0=B0?= =?UTF-8?q?=D0=BA=D1=81=D0=B8=D0=BC?= Date: Mon, 30 May 2022 16:52:38 +0300 Subject: [PATCH 09/22] Refactor: DTOs & DAOs now have clear API --- .../lab/maxb/dark/domain/operations/Source.kt | 3 + .../implementation/ProfileRepositoryImpl.kt | 21 ++-- .../RecognitionTasksRepositoryImpl.kt | 100 +++++++----------- .../implementation/UsersRepositoryImpl.kt | 21 ++-- .../interfaces/ProfileRepository.kt | 1 - .../interfaces/RecognitionTasksRepository.kt | 1 - .../repository/network/dark/DarkService.kt | 6 +- .../network/dark/DarkServiceImpl.kt | 4 +- .../network/dark/model/RecognitionTask.kt | 34 +++++- .../network/dark/routes/RecognitionTask.kt | 12 +-- .../repository/room/LocalDatabase.kt | 30 +++--- .../repository/room/dao/AdvancedDAO.kt | 47 ++++++++ .../repository/room/dao/ProfileDAO.kt | 18 ---- .../repository/room/dao/ProfilesDAO.kt | 13 +++ .../repository/room/dao/RecognitionTaskDAO.kt | 44 -------- .../room/dao/RecognitionTasksDAO.kt | 25 +++++ .../repository/room/dao/RemoteKeysDAO.kt | 16 +-- .../repository/room/dao/UserDAO.kt | 27 ----- .../repository/room/dao/UsersDAO.kt | 13 +++ .../repository/room/model/BaseLocalDTO.kt | 5 + .../repository/room/model/ProfileDTO.kt | 36 ------- .../repository/room/model/ProfileLocalDTO.kt | 36 +++++++ .../room/model/RecognitionTaskDTO.kt | 36 ------- .../room/model/RecognitionTaskLocalDTO.kt | 43 ++++++++ .../model/{RemoteKeys.kt => RemoteKey.kt} | 9 +- .../repository/room/model/UserDTO.kt | 19 ---- .../repository/room/model/UserLocalDTO.kt | 27 +++++ .../repository/room/relations/FullProfile.kt | 22 ---- .../room/relations/FullProfileLocalDTO.kt | 26 +++++ .../room/relations/FullRecognitionTaskDTO.kt | 26 +++++ .../relations/RecognitionTaskWithOwner.kt | 22 ---- .../pagination/RecognitionTasksMediator.kt | 22 ++-- 32 files changed, 401 insertions(+), 364 deletions(-) create mode 100644 app/src/main/java/lab/maxb/dark/domain/operations/Source.kt create mode 100644 app/src/main/java/lab/maxb/dark/presentation/repository/room/dao/AdvancedDAO.kt delete mode 100644 app/src/main/java/lab/maxb/dark/presentation/repository/room/dao/ProfileDAO.kt create mode 100644 app/src/main/java/lab/maxb/dark/presentation/repository/room/dao/ProfilesDAO.kt delete mode 100644 app/src/main/java/lab/maxb/dark/presentation/repository/room/dao/RecognitionTaskDAO.kt create mode 100644 app/src/main/java/lab/maxb/dark/presentation/repository/room/dao/RecognitionTasksDAO.kt delete mode 100644 app/src/main/java/lab/maxb/dark/presentation/repository/room/dao/UserDAO.kt create mode 100644 app/src/main/java/lab/maxb/dark/presentation/repository/room/dao/UsersDAO.kt create mode 100644 app/src/main/java/lab/maxb/dark/presentation/repository/room/model/BaseLocalDTO.kt delete mode 100644 app/src/main/java/lab/maxb/dark/presentation/repository/room/model/ProfileDTO.kt create mode 100644 app/src/main/java/lab/maxb/dark/presentation/repository/room/model/ProfileLocalDTO.kt delete mode 100644 app/src/main/java/lab/maxb/dark/presentation/repository/room/model/RecognitionTaskDTO.kt create mode 100644 app/src/main/java/lab/maxb/dark/presentation/repository/room/model/RecognitionTaskLocalDTO.kt rename app/src/main/java/lab/maxb/dark/presentation/repository/room/model/{RemoteKeys.kt => RemoteKey.kt} (57%) delete mode 100644 app/src/main/java/lab/maxb/dark/presentation/repository/room/model/UserDTO.kt create mode 100644 app/src/main/java/lab/maxb/dark/presentation/repository/room/model/UserLocalDTO.kt delete mode 100644 app/src/main/java/lab/maxb/dark/presentation/repository/room/relations/FullProfile.kt create mode 100644 app/src/main/java/lab/maxb/dark/presentation/repository/room/relations/FullProfileLocalDTO.kt create mode 100644 app/src/main/java/lab/maxb/dark/presentation/repository/room/relations/FullRecognitionTaskDTO.kt delete mode 100644 app/src/main/java/lab/maxb/dark/presentation/repository/room/relations/RecognitionTaskWithOwner.kt diff --git a/app/src/main/java/lab/maxb/dark/domain/operations/Source.kt b/app/src/main/java/lab/maxb/dark/domain/operations/Source.kt new file mode 100644 index 0000000..48310ab --- /dev/null +++ b/app/src/main/java/lab/maxb/dark/domain/operations/Source.kt @@ -0,0 +1,3 @@ +package lab.maxb.dark.domain.operations + +typealias ValSource = (String) -> T? diff --git a/app/src/main/java/lab/maxb/dark/presentation/repository/implementation/ProfileRepositoryImpl.kt b/app/src/main/java/lab/maxb/dark/presentation/repository/implementation/ProfileRepositoryImpl.kt index 7bdd517..85d5f61 100644 --- a/app/src/main/java/lab/maxb/dark/presentation/repository/implementation/ProfileRepositoryImpl.kt +++ b/app/src/main/java/lab/maxb/dark/presentation/repository/implementation/ProfileRepositoryImpl.kt @@ -9,44 +9,43 @@ import lab.maxb.dark.presentation.repository.interfaces.UsersRepository import lab.maxb.dark.presentation.repository.network.dark.DarkService import lab.maxb.dark.presentation.repository.network.dark.model.AuthRequest import lab.maxb.dark.presentation.repository.room.LocalDatabase -import lab.maxb.dark.presentation.repository.room.Server.Model.ProfileDTO +import lab.maxb.dark.presentation.repository.room.model.toLocalDTO +import lab.maxb.dark.presentation.repository.room.relations.toDomain import org.koin.core.annotation.Single @Single class ProfileRepositoryImpl( db: LocalDatabase, - private val darkService: DarkService, + private val networkDataSource: DarkService, private val userSettings: UserSettings, private val usersRepository: UsersRepository, ) : ProfileRepository { - private val profileDAO = db.profileDao() + private val localDataSource = db.profiles() private val _login = MutableStateFlow(userSettings.login) @OptIn(ExperimentalCoroutinesApi::class) override val profile = _login.flatMapLatest { login -> - profileDAO.getByLogin(login).distinctUntilChanged().mapLatest { fullProfile -> - fullProfile?.toProfile() + localDataSource.getByLogin(login).distinctUntilChanged().mapLatest { fullProfile -> + fullProfile?.toDomain() } } override suspend fun sendCredentials(login: String, password: String, initial: Boolean) { val request = AuthRequest(login, password) val response = if (initial) - darkService.signup(request) + networkDataSource.signup(request) else - darkService.login(request) + networkDataSource.login(request) userSettings.token = response.token userSettings.login = login - save( + localDataSource.save( Profile( login, usersRepository.getUser(response.id).firstOrNull()!!, response.token, role = response.role - ) + ).toLocalDTO() ) _login.value = login } - - override suspend fun save(profile: Profile) = profileDAO.save(ProfileDTO(profile)) } \ No newline at end of file diff --git a/app/src/main/java/lab/maxb/dark/presentation/repository/implementation/RecognitionTasksRepositoryImpl.kt b/app/src/main/java/lab/maxb/dark/presentation/repository/implementation/RecognitionTasksRepositoryImpl.kt index 975183c..71fdd0d 100644 --- a/app/src/main/java/lab/maxb/dark/presentation/repository/implementation/RecognitionTasksRepositoryImpl.kt +++ b/app/src/main/java/lab/maxb/dark/presentation/repository/implementation/RecognitionTasksRepositoryImpl.kt @@ -5,7 +5,6 @@ import androidx.paging.ExperimentalPagingApi import androidx.paging.Pager import androidx.paging.PagingConfig import androidx.paging.map -import androidx.room.withTransaction import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.flow.map @@ -15,10 +14,12 @@ import lab.maxb.dark.presentation.extra.ImageLoader import lab.maxb.dark.presentation.repository.interfaces.RecognitionTasksRepository import lab.maxb.dark.presentation.repository.interfaces.UsersRepository import lab.maxb.dark.presentation.repository.network.dark.DarkService -import lab.maxb.dark.presentation.repository.network.dark.model.RecognitionTaskCreationDTO +import lab.maxb.dark.presentation.repository.network.dark.model.toDomain +import lab.maxb.dark.presentation.repository.network.dark.model.toNetworkDTO import lab.maxb.dark.presentation.repository.room.LocalDatabase -import lab.maxb.dark.presentation.repository.room.dao.RecognitionTaskDAO -import lab.maxb.dark.presentation.repository.room.model.RecognitionTaskDTO +import lab.maxb.dark.presentation.repository.room.model.toDomain +import lab.maxb.dark.presentation.repository.room.model.toLocalDTO +import lab.maxb.dark.presentation.repository.room.relations.toDomain import lab.maxb.dark.presentation.repository.utils.Resource import lab.maxb.dark.presentation.repository.utils.pagination.Page import lab.maxb.dark.presentation.repository.utils.pagination.RecognitionTaskMediator @@ -26,82 +27,70 @@ import org.koin.core.annotation.Single @Single class RecognitionTasksRepositoryImpl( - private val db: LocalDatabase, - private val mDarkService: DarkService, + db: LocalDatabase, + private val networkDataSource: DarkService, private val usersRepository: UsersRepository, private val imageLoader: ImageLoader, ) : RecognitionTasksRepository { - private val mRecognitionTaskDao: RecognitionTaskDAO = db.recognitionTaskDao() + private val localDataSource = db.recognitionTasks() @OptIn(ExperimentalCoroutinesApi::class) private val tasksResource = Resource>().apply { fetchLocal = { page -> - mRecognitionTaskDao.getAllRecognitionTasks().mapLatest { - data -> data?.map { it.toRecognitionTask() } + localDataSource.getAll().mapLatest { + data -> data?.map { it.toDomain() } } } fetchRemote = { page -> - mDarkService.getAllTasks(page.page, page.size)?.map { - RecognitionTask( - setOf(), - it.image?.let { x -> listOf(x) }, - usersRepository.getUser(it.owner_id).firstOrNull()!!, - it.reviewed, - it.id, - ) + networkDataSource.getAllTasks(page.page, page.size)?.map { + it.toDomain { getUser(it.owner_id) } } } - localStore = { - db.withTransaction { - it.forEach { task -> - mRecognitionTaskDao.save( - RecognitionTaskDTO(task) - ) - } + localStore = { tasks -> + tasks.map { + it.toLocalDTO() + }.toTypedArray().let { + localDataSource.save(*it) } } clearLocalStore = { page -> if (page.page == 0) - mRecognitionTaskDao.clear() + localDataSource.clear() } } @OptIn(ExperimentalPagingApi::class) private val pager = Pager( config = PagingConfig(pageSize = 5), - remoteMediator = RecognitionTaskMediator(tasksResource, db.remoteKeysDao()), + remoteMediator = RecognitionTaskMediator(tasksResource, db.remoteKeys()), ) { - mRecognitionTaskDao.getAllRecognitionTasksPaged() + localDataSource.getAllPaged() }.flow.map { page -> - page.map { - it.toRecognitionTask() - } + page.map { it.toDomain() } } override fun getAllRecognitionTasks() = pager override suspend fun addRecognitionTask(task: RecognitionTask) { - val taskLocal = RecognitionTaskDTO(task) - mDarkService.addTask( - RecognitionTaskCreationDTO( - task.names!! - ) + val taskLocal = task.toLocalDTO() + networkDataSource.addTask( + task.toNetworkDTO() )?.also { taskLocal.id = it } taskLocal.images = task.images!!.map { - mDarkService.addImage( + networkDataSource.addImage( taskLocal.id, imageLoader.fromUri(it.toUri()) )!! } - mRecognitionTaskDao.save(taskLocal) + localDataSource.save(taskLocal) } override suspend fun markRecognitionTask(task: RecognitionTask) { - mRecognitionTaskDao.updateRecognitionTask(task as RecognitionTaskDTO) + localDataSource.update(task.toLocalDTO()) try { - if (mDarkService.markTask(task.id, task.reviewed)) + if (networkDataSource.markTask(task.id, task.reviewed)) getRecognitionTask(task.id, true).firstOrNull() } catch (e: Throwable) { e.printStackTrace() @@ -110,43 +99,29 @@ class RecognitionTasksRepositoryImpl( override suspend fun solveRecognitionTask(id: String, answer: String) = try { - mDarkService.solveTask(id, answer) + networkDataSource.solveTask(id, answer) } catch (e: Throwable) { e.printStackTrace() false } - override suspend fun deleteRecognitionTask(task: RecognitionTask) { - mRecognitionTaskDao.deleteRecognitionTask( - task as RecognitionTaskDTO - ) - } - @OptIn(ExperimentalCoroutinesApi::class) private val taskResource = Resource().apply { fetchRemote = { id -> - mDarkService.getTask(id)?.let { task -> - RecognitionTask( - task.names, - task.images, - usersRepository.getUser( - task.owner_id - ).firstOrNull()!!, - task.reviewed, - task.id, - ) + networkDataSource.getTask(id)?.let { + it.toDomain { getUser(it.owner_id) } } } fetchLocal = { id -> - mRecognitionTaskDao.getRecognitionTask(id).mapLatest { - it?.toRecognitionTask() + localDataSource.get(id).mapLatest { + it?.toDomain() } } localStore = { task -> - mRecognitionTaskDao.save(RecognitionTaskDTO(task)) + localDataSource.save(task.toLocalDTO()) } clearLocalStore = { - mRecognitionTaskDao.deleteRecognitionTask(it) + localDataSource.delete(it) } } @@ -154,5 +129,8 @@ class RecognitionTasksRepositoryImpl( = taskResource.query(id, forceUpdate) override fun getRecognitionTaskImage(path: String) - = mDarkService.getImageSource(path) + = networkDataSource.getImageSource(path) + + private suspend fun getUser(id: String) = + usersRepository.getUser(id).firstOrNull()!! } \ No newline at end of file diff --git a/app/src/main/java/lab/maxb/dark/presentation/repository/implementation/UsersRepositoryImpl.kt b/app/src/main/java/lab/maxb/dark/presentation/repository/implementation/UsersRepositoryImpl.kt index 33c9a53..84e9175 100644 --- a/app/src/main/java/lab/maxb/dark/presentation/repository/implementation/UsersRepositoryImpl.kt +++ b/app/src/main/java/lab/maxb/dark/presentation/repository/implementation/UsersRepositoryImpl.kt @@ -1,26 +1,31 @@ package lab.maxb.dark.presentation.repository.implementation +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.mapLatest import lab.maxb.dark.domain.model.User import lab.maxb.dark.presentation.repository.interfaces.UsersRepository import lab.maxb.dark.presentation.repository.network.dark.DarkService import lab.maxb.dark.presentation.repository.room.LocalDatabase -import lab.maxb.dark.presentation.repository.room.dao.UserDAO -import lab.maxb.dark.presentation.repository.room.model.UserDTO +import lab.maxb.dark.presentation.repository.room.dao.UsersDAO +import lab.maxb.dark.presentation.repository.room.model.toDomain +import lab.maxb.dark.presentation.repository.room.model.toLocalDTO import lab.maxb.dark.presentation.repository.utils.Resource import org.koin.core.annotation.Single @Single class UsersRepositoryImpl( db: LocalDatabase, - private val darkService: DarkService + private val networkDataSource: DarkService ) : UsersRepository { - private val mUserDao: UserDAO = db.userDao() + private val localDataSource: UsersDAO = db.users() + + @OptIn(ExperimentalCoroutinesApi::class) private val userResource = Resource().apply { - fetchLocal = { mUserDao.getUser(it) } - fetchRemote = { darkService.getUser(it) } - localStore = { mUserDao.save(UserDTO(it)) } - clearLocalStore = { mUserDao.deleteUser(it) } + fetchLocal = { localDataSource.get(it).mapLatest { x -> x?.toDomain() } } + fetchRemote = { networkDataSource.getUser(it) } + localStore = { localDataSource.save(it.toLocalDTO()) } + clearLocalStore = { localDataSource.delete(it) } } override suspend fun getUser(id: String, fresh: Boolean): Flow diff --git a/app/src/main/java/lab/maxb/dark/presentation/repository/interfaces/ProfileRepository.kt b/app/src/main/java/lab/maxb/dark/presentation/repository/interfaces/ProfileRepository.kt index d07cd61..80a123d 100644 --- a/app/src/main/java/lab/maxb/dark/presentation/repository/interfaces/ProfileRepository.kt +++ b/app/src/main/java/lab/maxb/dark/presentation/repository/interfaces/ProfileRepository.kt @@ -6,5 +6,4 @@ import lab.maxb.dark.domain.model.Profile interface ProfileRepository { suspend fun sendCredentials(login: String, password: String, initial: Boolean = false) val profile: Flow - suspend fun save(profile: Profile) } diff --git a/app/src/main/java/lab/maxb/dark/presentation/repository/interfaces/RecognitionTasksRepository.kt b/app/src/main/java/lab/maxb/dark/presentation/repository/interfaces/RecognitionTasksRepository.kt index f5175dc..d25a37d 100644 --- a/app/src/main/java/lab/maxb/dark/presentation/repository/interfaces/RecognitionTasksRepository.kt +++ b/app/src/main/java/lab/maxb/dark/presentation/repository/interfaces/RecognitionTasksRepository.kt @@ -12,5 +12,4 @@ interface RecognitionTasksRepository { suspend fun addRecognitionTask(task: RecognitionTask) suspend fun markRecognitionTask(task: RecognitionTask) suspend fun solveRecognitionTask(id: String, answer: String): Boolean - suspend fun deleteRecognitionTask(task: RecognitionTask) } \ No newline at end of file diff --git a/app/src/main/java/lab/maxb/dark/presentation/repository/network/dark/DarkService.kt b/app/src/main/java/lab/maxb/dark/presentation/repository/network/dark/DarkService.kt index b2d7a38..fafe4f3 100644 --- a/app/src/main/java/lab/maxb/dark/presentation/repository/network/dark/DarkService.kt +++ b/app/src/main/java/lab/maxb/dark/presentation/repository/network/dark/DarkService.kt @@ -7,9 +7,9 @@ import okhttp3.MultipartBody interface DarkService { - suspend fun getAllTasks(page: Int, size: Int): List? - suspend fun getTask(id: String): RecognitionTaskFullViewDTO? - suspend fun addTask(task: RecognitionTaskCreationDTO): String? + suspend fun getAllTasks(page: Int, size: Int): List? + suspend fun getTask(id: String): RecognitionTaskFullViewNetworkDTO? + suspend fun addTask(task: RecognitionTaskCreationNetworkDTO): String? suspend fun markTask(id: String, isAllowed: Boolean): Boolean suspend fun solveTask(id: String, answer: String): Boolean suspend fun addImage(id: String, filePart: MultipartBody.Part): String? diff --git a/app/src/main/java/lab/maxb/dark/presentation/repository/network/dark/DarkServiceImpl.kt b/app/src/main/java/lab/maxb/dark/presentation/repository/network/dark/DarkServiceImpl.kt index de5a4ee..7d225ca 100644 --- a/app/src/main/java/lab/maxb/dark/presentation/repository/network/dark/DarkServiceImpl.kt +++ b/app/src/main/java/lab/maxb/dark/presentation/repository/network/dark/DarkServiceImpl.kt @@ -5,7 +5,7 @@ import com.bumptech.glide.load.model.LazyHeaders import com.google.gson.GsonBuilder import lab.maxb.dark.BuildConfig import lab.maxb.dark.presentation.repository.network.dark.model.AuthRequest -import lab.maxb.dark.presentation.repository.network.dark.model.RecognitionTaskCreationDTO +import lab.maxb.dark.presentation.repository.network.dark.model.RecognitionTaskCreationNetworkDTO import lab.maxb.dark.presentation.repository.network.logger import okhttp3.MultipartBody import okhttp3.OkHttpClient @@ -28,7 +28,7 @@ class DarkServiceImpl( api.getTask(id) } - override suspend fun addTask(task: RecognitionTaskCreationDTO) = catchAll { + override suspend fun addTask(task: RecognitionTaskCreationNetworkDTO) = catchAll { api.addTask(task) } diff --git a/app/src/main/java/lab/maxb/dark/presentation/repository/network/dark/model/RecognitionTask.kt b/app/src/main/java/lab/maxb/dark/presentation/repository/network/dark/model/RecognitionTask.kt index 5cdd1b0..541fcd8 100644 --- a/app/src/main/java/lab/maxb/dark/presentation/repository/network/dark/model/RecognitionTask.kt +++ b/app/src/main/java/lab/maxb/dark/presentation/repository/network/dark/model/RecognitionTask.kt @@ -1,13 +1,17 @@ package lab.maxb.dark.presentation.repository.network.dark.model -class RecognitionTaskListViewDTO ( +import lab.maxb.dark.domain.model.RecognitionTask +import lab.maxb.dark.domain.model.User + + +class RecognitionTaskListViewNetworkDTO ( var image: String?, val owner_id: String, val reviewed: Boolean, var id: String, ) -class RecognitionTaskFullViewDTO( +class RecognitionTaskFullViewNetworkDTO( var names: Set?, var images: List?, val owner_id: String, @@ -15,6 +19,30 @@ class RecognitionTaskFullViewDTO( var id: String, ) -class RecognitionTaskCreationDTO( +class RecognitionTaskCreationNetworkDTO( var names: Set +) + +fun RecognitionTask.toNetworkDTO() = RecognitionTaskCreationNetworkDTO( + names!! +) + +inline fun RecognitionTaskListViewNetworkDTO.toDomain( + user: () -> User? = { null }, +) = RecognitionTask( + setOf(), + image?.let { listOf(it) }, + user(), + reviewed, + id, +) + +inline fun RecognitionTaskFullViewNetworkDTO.toDomain( + user: () -> User? = { null } +) = RecognitionTask( + names, + images, + user(), + reviewed, + id, ) \ No newline at end of file diff --git a/app/src/main/java/lab/maxb/dark/presentation/repository/network/dark/routes/RecognitionTask.kt b/app/src/main/java/lab/maxb/dark/presentation/repository/network/dark/routes/RecognitionTask.kt index 0d7392d..00b03b6 100644 --- a/app/src/main/java/lab/maxb/dark/presentation/repository/network/dark/routes/RecognitionTask.kt +++ b/app/src/main/java/lab/maxb/dark/presentation/repository/network/dark/routes/RecognitionTask.kt @@ -1,8 +1,8 @@ package lab.maxb.dark.presentation.repository.network.dark.routes -import lab.maxb.dark.presentation.repository.network.dark.model.RecognitionTaskCreationDTO -import lab.maxb.dark.presentation.repository.network.dark.model.RecognitionTaskFullViewDTO -import lab.maxb.dark.presentation.repository.network.dark.model.RecognitionTaskListViewDTO +import lab.maxb.dark.presentation.repository.network.dark.model.RecognitionTaskCreationNetworkDTO +import lab.maxb.dark.presentation.repository.network.dark.model.RecognitionTaskFullViewNetworkDTO +import lab.maxb.dark.presentation.repository.network.dark.model.RecognitionTaskListViewNetworkDTO import okhttp3.MultipartBody import retrofit2.http.* @@ -11,13 +11,13 @@ interface RecognitionTask { suspend fun getAllTasks( @Query("page") page: Int = 0, @Query("size") size: Int = 2, - ): List? + ): List? @GET("$path/{id}") - suspend fun getTask(@Path("id") id: String): RecognitionTaskFullViewDTO? + suspend fun getTask(@Path("id") id: String): RecognitionTaskFullViewNetworkDTO? @POST("$path/add") - suspend fun addTask(@Body task: RecognitionTaskCreationDTO): String? + suspend fun addTask(@Body task: RecognitionTaskCreationNetworkDTO): String? @PATCH("$path/mark/{id}/{isAllowed}") suspend fun markTask( diff --git a/app/src/main/java/lab/maxb/dark/presentation/repository/room/LocalDatabase.kt b/app/src/main/java/lab/maxb/dark/presentation/repository/room/LocalDatabase.kt index e40f63f..7a64c6e 100644 --- a/app/src/main/java/lab/maxb/dark/presentation/repository/room/LocalDatabase.kt +++ b/app/src/main/java/lab/maxb/dark/presentation/repository/room/LocalDatabase.kt @@ -5,28 +5,28 @@ import androidx.room.Database import androidx.room.Room import androidx.room.RoomDatabase import androidx.room.TypeConverters -import lab.maxb.dark.presentation.repository.room.Server.Model.ProfileDTO import lab.maxb.dark.presentation.repository.room.converters.CollectionsConverter -import lab.maxb.dark.presentation.repository.room.dao.ProfileDAO -import lab.maxb.dark.presentation.repository.room.dao.RecognitionTaskDAO +import lab.maxb.dark.presentation.repository.room.dao.ProfilesDAO +import lab.maxb.dark.presentation.repository.room.dao.RecognitionTasksDAO import lab.maxb.dark.presentation.repository.room.dao.RemoteKeysDAO -import lab.maxb.dark.presentation.repository.room.dao.UserDAO -import lab.maxb.dark.presentation.repository.room.model.RecognitionTaskDTO -import lab.maxb.dark.presentation.repository.room.model.RemoteKeys -import lab.maxb.dark.presentation.repository.room.model.UserDTO +import lab.maxb.dark.presentation.repository.room.dao.UsersDAO +import lab.maxb.dark.presentation.repository.room.model.ProfileLocalDTO +import lab.maxb.dark.presentation.repository.room.model.RecognitionTaskLocalDTO +import lab.maxb.dark.presentation.repository.room.model.RemoteKey +import lab.maxb.dark.presentation.repository.room.model.UserLocalDTO @Database(entities = [ - UserDTO::class, - RecognitionTaskDTO::class, - ProfileDTO::class, - RemoteKeys::class, + UserLocalDTO::class, + RecognitionTaskLocalDTO::class, + ProfileLocalDTO::class, + RemoteKey::class, ], version = 7, exportSchema = false) @TypeConverters(CollectionsConverter::class) abstract class LocalDatabase : RoomDatabase() { - abstract fun recognitionTaskDao(): RecognitionTaskDAO - abstract fun userDao(): UserDAO - abstract fun profileDao(): ProfileDAO - abstract fun remoteKeysDao(): RemoteKeysDAO + abstract fun recognitionTasks(): RecognitionTasksDAO + abstract fun users(): UsersDAO + abstract fun profiles(): ProfilesDAO + abstract fun remoteKeys(): RemoteKeysDAO companion object { internal fun build(app: Application) = Room.databaseBuilder( diff --git a/app/src/main/java/lab/maxb/dark/presentation/repository/room/dao/AdvancedDAO.kt b/app/src/main/java/lab/maxb/dark/presentation/repository/room/dao/AdvancedDAO.kt new file mode 100644 index 0000000..6e6a940 --- /dev/null +++ b/app/src/main/java/lab/maxb/dark/presentation/repository/room/dao/AdvancedDAO.kt @@ -0,0 +1,47 @@ +package lab.maxb.dark.presentation.repository.room.dao + +import androidx.room.* +import androidx.room.OnConflictStrategy.IGNORE +import androidx.sqlite.db.SimpleSQLiteQuery +import androidx.sqlite.db.SupportSQLiteQuery +import lab.maxb.dark.presentation.repository.room.model.BaseLocalDTO + +interface BaseDAO { + @Insert(onConflict = IGNORE) + suspend fun add(value: DTO): Long + + @Update + suspend fun update(value: DTO) + + @Delete + suspend fun delete(vararg value: DTO) +} + +abstract class AdvancedDAO( + private val tableName: String, +) : BaseDAO { + suspend fun clear() = run(SimpleSQLiteQuery("DELETE FROM $tableName")) + + suspend fun getById(id: String) = runForResult(SimpleSQLiteQuery( + "SELECT * FROM $tableName WHERE id = :id", + arrayOf(id), + )) + + suspend fun delete(id: String) = run(SimpleSQLiteQuery( + "DELETE FROM $tableName WHERE id = :id", + arrayOf(id), + )) + + @Transaction + open suspend fun save(vararg value: DTO) { + for (it in value) + if (add(it) == -1L) + update(it) + } + + @RawQuery + protected abstract suspend fun run(query: SupportSQLiteQuery): Long + + @RawQuery + protected abstract suspend fun runForResult(query: SupportSQLiteQuery): DTO? +} diff --git a/app/src/main/java/lab/maxb/dark/presentation/repository/room/dao/ProfileDAO.kt b/app/src/main/java/lab/maxb/dark/presentation/repository/room/dao/ProfileDAO.kt deleted file mode 100644 index 655b0af..0000000 --- a/app/src/main/java/lab/maxb/dark/presentation/repository/room/dao/ProfileDAO.kt +++ /dev/null @@ -1,18 +0,0 @@ -package lab.maxb.dark.presentation.repository.room.dao - -import androidx.room.Dao -import androidx.room.Insert -import androidx.room.OnConflictStrategy.REPLACE -import androidx.room.Query -import kotlinx.coroutines.flow.Flow -import lab.maxb.dark.presentation.repository.room.Server.Model.ProfileDTO -import lab.maxb.dark.presentation.repository.room.relations.FullProfile - -@Dao -interface ProfileDAO { - @Insert(onConflict = REPLACE) - suspend fun save(profile: ProfileDTO) - - @Query("SELECT * FROM profile WHERE login = :login") - fun getByLogin(login: String): Flow -} \ No newline at end of file diff --git a/app/src/main/java/lab/maxb/dark/presentation/repository/room/dao/ProfilesDAO.kt b/app/src/main/java/lab/maxb/dark/presentation/repository/room/dao/ProfilesDAO.kt new file mode 100644 index 0000000..2dd0848 --- /dev/null +++ b/app/src/main/java/lab/maxb/dark/presentation/repository/room/dao/ProfilesDAO.kt @@ -0,0 +1,13 @@ +package lab.maxb.dark.presentation.repository.room.dao + +import androidx.room.Dao +import androidx.room.Query +import kotlinx.coroutines.flow.Flow +import lab.maxb.dark.presentation.repository.room.model.ProfileLocalDTO +import lab.maxb.dark.presentation.repository.room.relations.FullProfileLocalDTO + +@Dao +abstract class ProfilesDAO: AdvancedDAO("profile") { + @Query("SELECT * FROM profile WHERE id = :login") + abstract fun getByLogin(login: String): Flow +} \ No newline at end of file diff --git a/app/src/main/java/lab/maxb/dark/presentation/repository/room/dao/RecognitionTaskDAO.kt b/app/src/main/java/lab/maxb/dark/presentation/repository/room/dao/RecognitionTaskDAO.kt deleted file mode 100644 index 5b7d2b6..0000000 --- a/app/src/main/java/lab/maxb/dark/presentation/repository/room/dao/RecognitionTaskDAO.kt +++ /dev/null @@ -1,44 +0,0 @@ -package lab.maxb.dark.presentation.repository.room.dao - -import androidx.paging.PagingSource -import androidx.room.* -import kotlinx.coroutines.flow.Flow -import lab.maxb.dark.presentation.repository.room.model.RecognitionTaskDTO -import lab.maxb.dark.presentation.repository.room.relations.RecognitionTaskWithOwner - -@Dao -interface RecognitionTaskDAO { - @Insert(onConflict = OnConflictStrategy.IGNORE) - suspend fun addRecognitionTask(task: RecognitionTaskDTO): Long - - @Update - suspend fun updateRecognitionTask(task: RecognitionTaskDTO) - - @Transaction - suspend fun save(task: RecognitionTaskDTO) { - if (addRecognitionTask(task) == -1L) - updateRecognitionTask(task) - } - - @Delete - suspend fun deleteRecognitionTask(task: RecognitionTaskDTO) - - @Query("DELETE FROM recognition_task WHERE id=:id") - suspend fun deleteRecognitionTask(id: String) - - @Transaction - @Query("SELECT * FROM recognition_task ORDER BY reviewed") - fun getAllRecognitionTasks(): Flow?> - - @Transaction - @Query("SELECT * FROM recognition_task ORDER BY reviewed") - fun getAllRecognitionTasksPaged(): PagingSource - - @Transaction - @Query("SELECT * FROM recognition_task WHERE id = :id") - fun getRecognitionTask(id: String): Flow - - @Transaction - @Query("DELETE FROM recognition_task") - suspend fun clear() -} \ No newline at end of file diff --git a/app/src/main/java/lab/maxb/dark/presentation/repository/room/dao/RecognitionTasksDAO.kt b/app/src/main/java/lab/maxb/dark/presentation/repository/room/dao/RecognitionTasksDAO.kt new file mode 100644 index 0000000..777717c --- /dev/null +++ b/app/src/main/java/lab/maxb/dark/presentation/repository/room/dao/RecognitionTasksDAO.kt @@ -0,0 +1,25 @@ +package lab.maxb.dark.presentation.repository.room.dao + +import androidx.paging.PagingSource +import androidx.room.Dao +import androidx.room.Query +import androidx.room.Transaction +import kotlinx.coroutines.flow.Flow +import lab.maxb.dark.presentation.repository.room.model.RecognitionTaskLocalDTO +import lab.maxb.dark.presentation.repository.room.relations.FullRecognitionTaskDTO + +@Dao +abstract class RecognitionTasksDAO: AdvancedDAO( + "recognition_task" +) { + @Transaction + @Query("SELECT * FROM recognition_task ORDER BY reviewed") + abstract fun getAll(): Flow?> + + @Transaction + @Query("SELECT * FROM recognition_task ORDER BY reviewed") + abstract fun getAllPaged(): PagingSource + + @Query("SELECT * FROM recognition_task WHERE id = :id") + abstract fun get(id: String): Flow +} \ No newline at end of file diff --git a/app/src/main/java/lab/maxb/dark/presentation/repository/room/dao/RemoteKeysDAO.kt b/app/src/main/java/lab/maxb/dark/presentation/repository/room/dao/RemoteKeysDAO.kt index a0492b2..c52d09d 100644 --- a/app/src/main/java/lab/maxb/dark/presentation/repository/room/dao/RemoteKeysDAO.kt +++ b/app/src/main/java/lab/maxb/dark/presentation/repository/room/dao/RemoteKeysDAO.kt @@ -1,19 +1,7 @@ package lab.maxb.dark.presentation.repository.room.dao import androidx.room.Dao -import androidx.room.Insert -import androidx.room.OnConflictStrategy -import androidx.room.Query -import lab.maxb.dark.presentation.repository.room.model.RemoteKeys +import lab.maxb.dark.presentation.repository.room.model.RemoteKey @Dao -interface RemoteKeysDAO { - @Insert(onConflict = OnConflictStrategy.REPLACE) - suspend fun save(remoteKey: List) - - @Query("SELECT * FROM remotekeys WHERE id = :id") - suspend fun getById(id: String): RemoteKeys? - - @Query("DELETE FROM remotekeys") - suspend fun clear() -} \ No newline at end of file +abstract class RemoteKeysDAO: AdvancedDAO("remote_keys") \ No newline at end of file diff --git a/app/src/main/java/lab/maxb/dark/presentation/repository/room/dao/UserDAO.kt b/app/src/main/java/lab/maxb/dark/presentation/repository/room/dao/UserDAO.kt deleted file mode 100644 index fc2d972..0000000 --- a/app/src/main/java/lab/maxb/dark/presentation/repository/room/dao/UserDAO.kt +++ /dev/null @@ -1,27 +0,0 @@ -package lab.maxb.dark.presentation.repository.room.dao - -import androidx.room.* -import androidx.room.OnConflictStrategy.IGNORE -import kotlinx.coroutines.flow.Flow -import lab.maxb.dark.presentation.repository.room.model.UserDTO - -@Dao -interface UserDAO { - @Insert(onConflict = IGNORE) - suspend fun addUser(user: UserDTO): Long - - @Update - suspend fun updateUser(user: UserDTO) - - @Transaction - suspend fun save(user: UserDTO) { - if (addUser(user) == -1L) - updateUser(user) - } - - @Query("DELETE FROM user WHERE id = :id") - suspend fun deleteUser(id: String) - - @Query("SELECT * FROM user WHERE id = :id") - fun getUser(id: String): Flow -} \ No newline at end of file diff --git a/app/src/main/java/lab/maxb/dark/presentation/repository/room/dao/UsersDAO.kt b/app/src/main/java/lab/maxb/dark/presentation/repository/room/dao/UsersDAO.kt new file mode 100644 index 0000000..cadc5fa --- /dev/null +++ b/app/src/main/java/lab/maxb/dark/presentation/repository/room/dao/UsersDAO.kt @@ -0,0 +1,13 @@ +package lab.maxb.dark.presentation.repository.room.dao + +import androidx.room.Dao +import androidx.room.Query +import kotlinx.coroutines.flow.Flow +import lab.maxb.dark.presentation.repository.room.model.UserLocalDTO + + +@Dao +abstract class UsersDAO : AdvancedDAO("user") { + @Query("SELECT * FROM user WHERE id = :id") + abstract fun get(id: String): Flow +} \ No newline at end of file diff --git a/app/src/main/java/lab/maxb/dark/presentation/repository/room/model/BaseLocalDTO.kt b/app/src/main/java/lab/maxb/dark/presentation/repository/room/model/BaseLocalDTO.kt new file mode 100644 index 0000000..d766ca3 --- /dev/null +++ b/app/src/main/java/lab/maxb/dark/presentation/repository/room/model/BaseLocalDTO.kt @@ -0,0 +1,5 @@ +package lab.maxb.dark.presentation.repository.room.model + +abstract class BaseLocalDTO { + abstract val id: String +} diff --git a/app/src/main/java/lab/maxb/dark/presentation/repository/room/model/ProfileDTO.kt b/app/src/main/java/lab/maxb/dark/presentation/repository/room/model/ProfileDTO.kt deleted file mode 100644 index 4a23d41..0000000 --- a/app/src/main/java/lab/maxb/dark/presentation/repository/room/model/ProfileDTO.kt +++ /dev/null @@ -1,36 +0,0 @@ -package lab.maxb.dark.presentation.repository.room.Server.Model - -import androidx.room.Entity -import androidx.room.ForeignKey -import androidx.room.PrimaryKey -import lab.maxb.dark.domain.model.Profile -import lab.maxb.dark.domain.model.Role -import lab.maxb.dark.presentation.repository.room.model.UserDTO - -@Entity(tableName = "profile", - ignoredColumns = ["user"], - foreignKeys = [ - ForeignKey( - entity = UserDTO::class, - parentColumns = ["id"], - childColumns = ["user_id"], - onDelete = ForeignKey.CASCADE - ) - ] -) -data class ProfileDTO( - @PrimaryKey - override var login: String, - var user_id: String, - override var token: String, - override var type: AuthType, - override var role: Role, -): Profile(login, null, token, type, role) { - constructor(profile: Profile) : this( - profile.login, - profile.user!!.id, - profile.token, - profile.type, - profile.role - ) -} diff --git a/app/src/main/java/lab/maxb/dark/presentation/repository/room/model/ProfileLocalDTO.kt b/app/src/main/java/lab/maxb/dark/presentation/repository/room/model/ProfileLocalDTO.kt new file mode 100644 index 0000000..70780d7 --- /dev/null +++ b/app/src/main/java/lab/maxb/dark/presentation/repository/room/model/ProfileLocalDTO.kt @@ -0,0 +1,36 @@ +package lab.maxb.dark.presentation.repository.room.model + +import androidx.room.Entity +import androidx.room.ForeignKey +import androidx.room.PrimaryKey +import lab.maxb.dark.domain.model.Profile +import lab.maxb.dark.domain.model.Role +import lab.maxb.dark.domain.operations.randomUUID + +@Entity(tableName = "profile", + foreignKeys = [ + ForeignKey( + entity = UserLocalDTO::class, + parentColumns = ["id"], + childColumns = ["user_id"], + onDelete = ForeignKey.CASCADE + ) + ] +) +data class ProfileLocalDTO( + var user_id: String, + var token: String, + var type: Profile.AuthType, + var role: Role, + + @PrimaryKey + override var id: String = randomUUID, +): BaseLocalDTO() + +fun Profile.toLocalDTO() = ProfileLocalDTO( + user!!.id, + token, + type, + role, + login, +) diff --git a/app/src/main/java/lab/maxb/dark/presentation/repository/room/model/RecognitionTaskDTO.kt b/app/src/main/java/lab/maxb/dark/presentation/repository/room/model/RecognitionTaskDTO.kt deleted file mode 100644 index da2a288..0000000 --- a/app/src/main/java/lab/maxb/dark/presentation/repository/room/model/RecognitionTaskDTO.kt +++ /dev/null @@ -1,36 +0,0 @@ -package lab.maxb.dark.presentation.repository.room.model - -import androidx.room.Entity -import androidx.room.ForeignKey -import androidx.room.PrimaryKey -import lab.maxb.dark.domain.model.RecognitionTask - -@Entity(tableName = "recognition_task", - ignoredColumns = ["owner"], - foreignKeys = [ - ForeignKey( - entity = UserDTO::class, - parentColumns = ["id"], - childColumns = ["owner_id"], - onDelete = ForeignKey.CASCADE - ) - ] -) -data class RecognitionTaskDTO( - @PrimaryKey - override var id: String, - override var names: Set?, - override var images: List?, - val owner_id: String, - override var reviewed: Boolean = false, -): RecognitionTask(id=id) { - constructor(task: RecognitionTask) : this( - task.id, - task.names, - task.images, - task.owner!!.id, - task.reviewed, - ) - - fun toRecognitionTask() = this as RecognitionTask -} \ No newline at end of file diff --git a/app/src/main/java/lab/maxb/dark/presentation/repository/room/model/RecognitionTaskLocalDTO.kt b/app/src/main/java/lab/maxb/dark/presentation/repository/room/model/RecognitionTaskLocalDTO.kt new file mode 100644 index 0000000..95a0632 --- /dev/null +++ b/app/src/main/java/lab/maxb/dark/presentation/repository/room/model/RecognitionTaskLocalDTO.kt @@ -0,0 +1,43 @@ +package lab.maxb.dark.presentation.repository.room.model + +import androidx.room.Entity +import androidx.room.ForeignKey +import androidx.room.PrimaryKey +import lab.maxb.dark.domain.model.RecognitionTask +import lab.maxb.dark.domain.operations.randomUUID + +@Entity(tableName = "recognition_task", + foreignKeys = [ + ForeignKey( + entity = UserLocalDTO::class, + parentColumns = ["id"], + childColumns = ["owner_id"], + onDelete = ForeignKey.CASCADE + ) + ] +) +data class RecognitionTaskLocalDTO( + var names: Set?, + var images: List?, + val owner_id: String, + var reviewed: Boolean = false, + + @PrimaryKey + override var id: String = randomUUID, +): BaseLocalDTO() + +fun RecognitionTask.toLocalDTO() = RecognitionTaskLocalDTO( + names, + images, + owner!!.id, + reviewed, + id, +) + +fun RecognitionTaskLocalDTO.toDomain() = RecognitionTask( + names, + images, + null, + reviewed, + id, +) \ No newline at end of file diff --git a/app/src/main/java/lab/maxb/dark/presentation/repository/room/model/RemoteKeys.kt b/app/src/main/java/lab/maxb/dark/presentation/repository/room/model/RemoteKey.kt similarity index 57% rename from app/src/main/java/lab/maxb/dark/presentation/repository/room/model/RemoteKeys.kt rename to app/src/main/java/lab/maxb/dark/presentation/repository/room/model/RemoteKey.kt index e12e255..33a957b 100644 --- a/app/src/main/java/lab/maxb/dark/presentation/repository/room/model/RemoteKeys.kt +++ b/app/src/main/java/lab/maxb/dark/presentation/repository/room/model/RemoteKey.kt @@ -3,9 +3,10 @@ package lab.maxb.dark.presentation.repository.room.model import androidx.room.Entity import androidx.room.PrimaryKey -@Entity -data class RemoteKeys( - @PrimaryKey val id: String, +@Entity(tableName = "remote_keys") +data class RemoteKey( + @PrimaryKey + override var id: String, val prevKey: Int?, val nextKey: Int? -) \ No newline at end of file +): BaseLocalDTO() \ No newline at end of file diff --git a/app/src/main/java/lab/maxb/dark/presentation/repository/room/model/UserDTO.kt b/app/src/main/java/lab/maxb/dark/presentation/repository/room/model/UserDTO.kt deleted file mode 100644 index cfde4aa..0000000 --- a/app/src/main/java/lab/maxb/dark/presentation/repository/room/model/UserDTO.kt +++ /dev/null @@ -1,19 +0,0 @@ -package lab.maxb.dark.presentation.repository.room.model - -import androidx.room.Entity -import androidx.room.PrimaryKey -import lab.maxb.dark.domain.model.User - -@Entity(tableName = "user") -data class UserDTO( - @PrimaryKey - override var id: String, - override var name: String, - override var rating: Int, -): User(name=name, rating=rating) { - constructor(user: User) : this( - user.id, - user.name, - user.rating, - ) -} diff --git a/app/src/main/java/lab/maxb/dark/presentation/repository/room/model/UserLocalDTO.kt b/app/src/main/java/lab/maxb/dark/presentation/repository/room/model/UserLocalDTO.kt new file mode 100644 index 0000000..3f0d389 --- /dev/null +++ b/app/src/main/java/lab/maxb/dark/presentation/repository/room/model/UserLocalDTO.kt @@ -0,0 +1,27 @@ +package lab.maxb.dark.presentation.repository.room.model + +import androidx.room.Entity +import androidx.room.PrimaryKey +import lab.maxb.dark.domain.model.User +import lab.maxb.dark.domain.operations.randomUUID + +@Entity(tableName = "user") +data class UserLocalDTO( + var name: String, + var rating: Int, + + @PrimaryKey + override var id: String = randomUUID, +): BaseLocalDTO() + +fun User.toLocalDTO() = UserLocalDTO( + name, + rating, + id, +) + +fun UserLocalDTO.toDomain() = User( + name, + rating, + id, +) diff --git a/app/src/main/java/lab/maxb/dark/presentation/repository/room/relations/FullProfile.kt b/app/src/main/java/lab/maxb/dark/presentation/repository/room/relations/FullProfile.kt deleted file mode 100644 index b65cf34..0000000 --- a/app/src/main/java/lab/maxb/dark/presentation/repository/room/relations/FullProfile.kt +++ /dev/null @@ -1,22 +0,0 @@ -package lab.maxb.dark.presentation.repository.room.relations - -import androidx.room.Embedded -import androidx.room.Relation -import lab.maxb.dark.domain.model.Profile -import lab.maxb.dark.domain.model.User -import lab.maxb.dark.presentation.repository.room.Server.Model.ProfileDTO -import lab.maxb.dark.presentation.repository.room.model.UserDTO - -data class FullProfile( - @Embedded val profile: ProfileDTO, - @Relation( - parentColumn = "user_id", - entityColumn = "id", - entity = UserDTO::class - ) - val user: User? -) { - fun toProfile() = profile.also { - it.user = user - } as Profile -} diff --git a/app/src/main/java/lab/maxb/dark/presentation/repository/room/relations/FullProfileLocalDTO.kt b/app/src/main/java/lab/maxb/dark/presentation/repository/room/relations/FullProfileLocalDTO.kt new file mode 100644 index 0000000..a36e9ee --- /dev/null +++ b/app/src/main/java/lab/maxb/dark/presentation/repository/room/relations/FullProfileLocalDTO.kt @@ -0,0 +1,26 @@ +package lab.maxb.dark.presentation.repository.room.relations + +import androidx.room.Embedded +import androidx.room.Relation +import lab.maxb.dark.domain.model.Profile +import lab.maxb.dark.domain.model.User +import lab.maxb.dark.presentation.repository.room.model.ProfileLocalDTO +import lab.maxb.dark.presentation.repository.room.model.UserLocalDTO + +data class FullProfileLocalDTO( + @Embedded val profile: ProfileLocalDTO, + @Relation( + parentColumn = "user_id", + entityColumn = "id", + entity = UserLocalDTO::class + ) + val user: User? +) + +fun FullProfileLocalDTO.toDomain() = Profile( + profile.id, + user, + profile.token, + profile.type, + profile.role, +) diff --git a/app/src/main/java/lab/maxb/dark/presentation/repository/room/relations/FullRecognitionTaskDTO.kt b/app/src/main/java/lab/maxb/dark/presentation/repository/room/relations/FullRecognitionTaskDTO.kt new file mode 100644 index 0000000..aa214a4 --- /dev/null +++ b/app/src/main/java/lab/maxb/dark/presentation/repository/room/relations/FullRecognitionTaskDTO.kt @@ -0,0 +1,26 @@ +package lab.maxb.dark.presentation.repository.room.relations + +import androidx.room.Embedded +import androidx.room.Relation +import lab.maxb.dark.domain.model.RecognitionTask +import lab.maxb.dark.domain.model.User +import lab.maxb.dark.presentation.repository.room.model.RecognitionTaskLocalDTO +import lab.maxb.dark.presentation.repository.room.model.UserLocalDTO + +data class FullRecognitionTaskDTO( + @Embedded val recognition_task: RecognitionTaskLocalDTO, + @Relation( + parentColumn = "owner_id", + entityColumn = "id", + entity = UserLocalDTO::class + ) + val owner: User? +) + +fun FullRecognitionTaskDTO.toDomain() = RecognitionTask( + recognition_task.names, + recognition_task.images, + owner, + recognition_task.reviewed, + recognition_task.id, +) diff --git a/app/src/main/java/lab/maxb/dark/presentation/repository/room/relations/RecognitionTaskWithOwner.kt b/app/src/main/java/lab/maxb/dark/presentation/repository/room/relations/RecognitionTaskWithOwner.kt deleted file mode 100644 index 8f2662b..0000000 --- a/app/src/main/java/lab/maxb/dark/presentation/repository/room/relations/RecognitionTaskWithOwner.kt +++ /dev/null @@ -1,22 +0,0 @@ -package lab.maxb.dark.presentation.repository.room.relations - -import androidx.room.Embedded -import androidx.room.Relation -import lab.maxb.dark.domain.model.RecognitionTask -import lab.maxb.dark.domain.model.User -import lab.maxb.dark.presentation.repository.room.model.RecognitionTaskDTO -import lab.maxb.dark.presentation.repository.room.model.UserDTO - -data class RecognitionTaskWithOwner( - @Embedded val recognition_task: RecognitionTaskDTO, - @Relation( - parentColumn = "owner_id", - entityColumn = "id", - entity = UserDTO::class - ) - val owner: User? -) { - fun toRecognitionTask() = recognition_task.also { task -> - task.owner = owner - } as RecognitionTask -} diff --git a/app/src/main/java/lab/maxb/dark/presentation/repository/utils/pagination/RecognitionTasksMediator.kt b/app/src/main/java/lab/maxb/dark/presentation/repository/utils/pagination/RecognitionTasksMediator.kt index d4c338f..2b3b24b 100644 --- a/app/src/main/java/lab/maxb/dark/presentation/repository/utils/pagination/RecognitionTasksMediator.kt +++ b/app/src/main/java/lab/maxb/dark/presentation/repository/utils/pagination/RecognitionTasksMediator.kt @@ -7,8 +7,8 @@ import androidx.paging.RemoteMediator import kotlinx.coroutines.flow.firstOrNull import lab.maxb.dark.domain.model.RecognitionTask import lab.maxb.dark.presentation.repository.room.dao.RemoteKeysDAO -import lab.maxb.dark.presentation.repository.room.model.RemoteKeys -import lab.maxb.dark.presentation.repository.room.relations.RecognitionTaskWithOwner +import lab.maxb.dark.presentation.repository.room.model.RemoteKey +import lab.maxb.dark.presentation.repository.room.relations.FullRecognitionTaskDTO import lab.maxb.dark.presentation.repository.utils.BaseResource import retrofit2.HttpException import java.io.IOException @@ -19,7 +19,7 @@ import java.io.InvalidObjectException class RecognitionTaskMediator( private val resource: BaseResource>, private val remoteKeys: RemoteKeysDAO, -) : RemoteMediator() { +) : RemoteMediator() { override suspend fun initialize() = if (resource.isFresh(Page(0, 1))) InitializeAction.SKIP_INITIAL_REFRESH @@ -28,7 +28,7 @@ class RecognitionTaskMediator( override suspend fun load( loadType: LoadType, - state: PagingState + state: PagingState ): MediatorResult { return try { val pageKeyData = getKeyPageData(loadType, state) @@ -42,13 +42,13 @@ class RecognitionTaskMediator( remoteKeys.clear() response?.map { - RemoteKeys( + RemoteKey( it.id, if (page == 0) null else page - 1, if (isEndOfList) null else page + 1 ) - }?.let { - remoteKeys.save(it) + }?.toTypedArray()?.let { + remoteKeys.save(*it) } MediatorResult.Success( @@ -61,21 +61,21 @@ class RecognitionTaskMediator( } } - private suspend fun getFirstRemoteKey(state: PagingState): RemoteKeys? { + private suspend fun getFirstRemoteKey(state: PagingState): RemoteKey? { return state.pages .firstOrNull { it.data.isNotEmpty() } ?.data?.firstOrNull() ?.let { doggo -> remoteKeys.getById(doggo.recognition_task.id) } } - private suspend fun getLastRemoteKey(state: PagingState): RemoteKeys? { + private suspend fun getLastRemoteKey(state: PagingState): RemoteKey? { return state.pages .lastOrNull { it.data.isNotEmpty() } ?.data?.lastOrNull() ?.let { doggo -> remoteKeys.getById(doggo.recognition_task.id) } } - private suspend fun getClosestRemoteKey(state: PagingState): RemoteKeys? { + private suspend fun getClosestRemoteKey(state: PagingState): RemoteKey? { return state.anchorPosition?.let { position -> state.closestItemToPosition(position)?.recognition_task?.id?.let { id -> remoteKeys.getById(id) @@ -83,7 +83,7 @@ class RecognitionTaskMediator( } } - private suspend fun getKeyPageData(loadType: LoadType, state: PagingState): Any? { + private suspend fun getKeyPageData(loadType: LoadType, state: PagingState): Any? { return when (loadType) { LoadType.REFRESH -> { val remoteKeys = getClosestRemoteKey(state) From 1701d411f241a88888d19642f17aa96dc91f082c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=91=D0=B0=D0=B1=D1=8B=D1=88=D0=B5=D0=B2=20=D0=9C=D0=B0?= =?UTF-8?q?=D0=BA=D1=81=D0=B8=D0=BC?= Date: Mon, 30 May 2022 22:07:10 +0300 Subject: [PATCH 10/22] Network requests can now be retried on timeout --- .../network/dark/DarkServiceImpl.kt | 28 +++++++++++++++++-- .../dark/presentation/view/MainActivity.kt | 8 ++---- 2 files changed, 28 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/lab/maxb/dark/presentation/repository/network/dark/DarkServiceImpl.kt b/app/src/main/java/lab/maxb/dark/presentation/repository/network/dark/DarkServiceImpl.kt index 7d225ca..a52aca2 100644 --- a/app/src/main/java/lab/maxb/dark/presentation/repository/network/dark/DarkServiceImpl.kt +++ b/app/src/main/java/lab/maxb/dark/presentation/repository/network/dark/DarkServiceImpl.kt @@ -3,6 +3,7 @@ package lab.maxb.dark.presentation.repository.network.dark import com.bumptech.glide.load.model.GlideUrl import com.bumptech.glide.load.model.LazyHeaders import com.google.gson.GsonBuilder +import kotlinx.coroutines.delay import lab.maxb.dark.BuildConfig import lab.maxb.dark.presentation.repository.network.dark.model.AuthRequest import lab.maxb.dark.presentation.repository.network.dark.model.RecognitionTaskCreationNetworkDTO @@ -13,6 +14,8 @@ import org.koin.core.annotation.Single import retrofit2.Retrofit import retrofit2.converter.gson.GsonConverterFactory import java.io.EOFException +import java.net.SocketTimeoutException +import java.time.Duration @Single class DarkServiceImpl( @@ -64,9 +67,9 @@ class DarkServiceImpl( } private suspend inline fun catchAll( - crossinline block: suspend () -> T + crossinline block: suspend () -> T, ): T = try { - block() + retry(block) } catch (e: EOFException) { null as T } catch (e: Throwable) { @@ -74,6 +77,22 @@ class DarkServiceImpl( throw e } + private suspend inline fun retry( + crossinline block: suspend () -> T, + ): T { + lateinit var lastException: SocketTimeoutException + repeat(MAX_RETRY_COUNT) { + try { + return block() + } catch (e: SocketTimeoutException) { + lastException = e + if ((it + 1) != MAX_RETRY_COUNT) + delay(RETRY_DELAY) + } + } + throw lastException + } + // Initialization private fun buildDarkService() = Retrofit.Builder() @@ -93,4 +112,9 @@ class DarkServiceImpl( }.create().run { GsonConverterFactory.create(this) } + + companion object { + const val MAX_RETRY_COUNT = 6 + val RETRY_DELAY = Duration.ofSeconds(1).toMillis() + } } \ No newline at end of file diff --git a/app/src/main/java/lab/maxb/dark/presentation/view/MainActivity.kt b/app/src/main/java/lab/maxb/dark/presentation/view/MainActivity.kt index 071e60e..3954857 100644 --- a/app/src/main/java/lab/maxb/dark/presentation/view/MainActivity.kt +++ b/app/src/main/java/lab/maxb/dark/presentation/view/MainActivity.kt @@ -77,8 +77,8 @@ class MainActivity : AppCompatActivity(R.layout.main_activity), binding.navView.setNavigationItemSelectedListener(this) // Logic - navController.addOnDestinationChangedListener { _, destionation, _ -> - val inAuthZone = destionation.id in authDestinations + navController.addOnDestinationChangedListener { _, destination, _ -> + val inAuthZone = destination.id in authDestinations binding.drawerLayout.setDrawerLockMode( if (!inAuthZone) DrawerLayout.LOCK_MODE_UNLOCKED else DrawerLayout.LOCK_MODE_LOCKED_CLOSED @@ -94,10 +94,6 @@ class MainActivity : AppCompatActivity(R.layout.main_activity), } } - override fun onSupportNavigateUp(): Boolean { - return super.onSupportNavigateUp() - } - override fun onBackPressed(): Unit = with(binding.drawerLayout) { if (isDrawerOpen(GravityCompat.START)) closeDrawer(GravityCompat.START) From 9ce8ee997627ffb47917ad9988a424e161018631 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=91=D0=B0=D0=B1=D1=8B=D1=88=D0=B5=D0=B2=20=D0=9C=D0=B0?= =?UTF-8?q?=D0=BA=D1=81=D0=B8=D0=BC?= Date: Mon, 30 May 2022 22:43:17 +0300 Subject: [PATCH 11/22] Resource now use cache if unable to get fresh version from network --- .../repository/network/dark/DarkServiceImpl.kt | 15 +++++++++------ .../presentation/repository/utils/BaseResource.kt | 13 ++++++++----- .../dark/presentation/viewModel/AuthViewModel.kt | 4 +--- 3 files changed, 18 insertions(+), 14 deletions(-) diff --git a/app/src/main/java/lab/maxb/dark/presentation/repository/network/dark/DarkServiceImpl.kt b/app/src/main/java/lab/maxb/dark/presentation/repository/network/dark/DarkServiceImpl.kt index a52aca2..38892a5 100644 --- a/app/src/main/java/lab/maxb/dark/presentation/repository/network/dark/DarkServiceImpl.kt +++ b/app/src/main/java/lab/maxb/dark/presentation/repository/network/dark/DarkServiceImpl.kt @@ -15,6 +15,7 @@ import retrofit2.Retrofit import retrofit2.converter.gson.GsonConverterFactory import java.io.EOFException import java.net.SocketTimeoutException +import java.net.UnknownHostException import java.time.Duration @Single @@ -72,6 +73,8 @@ class DarkServiceImpl( retry(block) } catch (e: EOFException) { null as T + } catch (e: UnknownHostException) { + throw UnableToObtainResource() } catch (e: Throwable) { e.printStackTrace() throw e @@ -80,17 +83,14 @@ class DarkServiceImpl( private suspend inline fun retry( crossinline block: suspend () -> T, ): T { - lateinit var lastException: SocketTimeoutException repeat(MAX_RETRY_COUNT) { try { return block() } catch (e: SocketTimeoutException) { - lastException = e - if ((it + 1) != MAX_RETRY_COUNT) - delay(RETRY_DELAY) + // Ignore } } - throw lastException + throw UnableToObtainResource() } // Initialization @@ -117,4 +117,7 @@ class DarkServiceImpl( const val MAX_RETRY_COUNT = 6 val RETRY_DELAY = Duration.ofSeconds(1).toMillis() } -} \ No newline at end of file +} + + +class UnableToObtainResource: Exception() \ No newline at end of file diff --git a/app/src/main/java/lab/maxb/dark/presentation/repository/utils/BaseResource.kt b/app/src/main/java/lab/maxb/dark/presentation/repository/utils/BaseResource.kt index 597a2ce..8e25e03 100644 --- a/app/src/main/java/lab/maxb/dark/presentation/repository/utils/BaseResource.kt +++ b/app/src/main/java/lab/maxb/dark/presentation/repository/utils/BaseResource.kt @@ -3,6 +3,7 @@ package lab.maxb.dark.presentation.repository.utils import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.emitAll import kotlinx.coroutines.flow.flow +import lab.maxb.dark.presentation.repository.network.dark.UnableToObtainResource open class BaseResource { open lateinit var fetchLocal: suspend (Input) -> Flow @@ -16,11 +17,13 @@ open class BaseResource { suspend fun query(args: Input, force: Boolean = false) = flow { if (force || !isFresh(args)) { - fetchRemote(args)?.run { - localStore?.invoke(this) - } ?: clearLocalStore?.invoke(args) - onRefresh?.invoke() + try { + fetchRemote(args)?.run { + localStore?.invoke(this) + } ?: clearLocalStore?.invoke(args) + onRefresh?.invoke() + } catch (e: UnableToObtainResource) {} } emitAll(getCache(args)) } -} \ No newline at end of file +} diff --git a/app/src/main/java/lab/maxb/dark/presentation/viewModel/AuthViewModel.kt b/app/src/main/java/lab/maxb/dark/presentation/viewModel/AuthViewModel.kt index bcc4bf6..b21bc36 100644 --- a/app/src/main/java/lab/maxb/dark/presentation/viewModel/AuthViewModel.kt +++ b/app/src/main/java/lab/maxb/dark/presentation/viewModel/AuthViewModel.kt @@ -58,9 +58,7 @@ class AuthViewModel( suspend fun authorize() { try { _profile.value = UiState.Loading - withTimeout(Duration.ofSeconds(10).toMillis()) { - profileRepository.sendCredentials(login.value, password.value, isAccountNew.value) - } + profileRepository.sendCredentials(login.value, password.value, isAccountNew.value) } catch (e: Throwable) { _profile.value = UiState.Error(e) println(e) From 207c91ef4a5f06ba49571f063bab234ad6992d5c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=91=D0=B0=D0=B1=D1=8B=D1=88=D0=B5=D0=B2=20=D0=9C=D0=B0?= =?UTF-8?q?=D0=BA=D1=81=D0=B8=D0=BC?= Date: Mon, 30 May 2022 23:21:03 +0300 Subject: [PATCH 12/22] Fix: App crash when suggestions requested with no internet --- .../repository/network/synonymizer/SynonymFounder.kt | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/lab/maxb/dark/presentation/repository/network/synonymizer/SynonymFounder.kt b/app/src/main/java/lab/maxb/dark/presentation/repository/network/synonymizer/SynonymFounder.kt index 891717d..935ee62 100644 --- a/app/src/main/java/lab/maxb/dark/presentation/repository/network/synonymizer/SynonymFounder.kt +++ b/app/src/main/java/lab/maxb/dark/presentation/repository/network/synonymizer/SynonymFounder.kt @@ -9,12 +9,16 @@ import retrofit2.converter.gson.GsonConverterFactory @Single class SynonymFounder : SynonymsRepository { private val api: RusTxtAPI - private suspend fun getSynonym(text: String): String? { - return api.getSynonym(MultipartBody.Builder().setType(MultipartBody.FORM) + private suspend fun getSynonym(text: String) + = try { + api.getSynonym(MultipartBody.Builder().setType(MultipartBody.FORM) .addFormDataPart("method","getSynText") .addFormDataPart("text", text) .build() )?.modified_text?.let { fixText(it) } + } catch (e: Throwable) { + e.printStackTrace() + null } private fun fixText(text: String) From 388cf4595979932a89dda636df12e19393abbef0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=91=D0=B0=D0=B1=D1=8B=D1=88=D0=B5=D0=B2=20=D0=9C=D0=B0?= =?UTF-8?q?=D0=BA=D1=81=D0=B8=D0=BC?= Date: Mon, 30 May 2022 23:22:02 +0300 Subject: [PATCH 13/22] Fix: App crash when task.mark requested with no internet --- .../RecognitionTasksRepositoryImpl.kt | 13 +++++++------ .../interfaces/RecognitionTasksRepository.kt | 2 +- .../repository/room/dao/RecognitionTasksDAO.kt | 2 +- .../view/SolveRecognitionTaskFragment.kt | 4 ++-- .../viewModel/SolveRecognitionTaskViewModel.kt | 10 ++++------ 5 files changed, 15 insertions(+), 16 deletions(-) diff --git a/app/src/main/java/lab/maxb/dark/presentation/repository/implementation/RecognitionTasksRepositoryImpl.kt b/app/src/main/java/lab/maxb/dark/presentation/repository/implementation/RecognitionTasksRepositoryImpl.kt index 71fdd0d..1808b00 100644 --- a/app/src/main/java/lab/maxb/dark/presentation/repository/implementation/RecognitionTasksRepositoryImpl.kt +++ b/app/src/main/java/lab/maxb/dark/presentation/repository/implementation/RecognitionTasksRepositoryImpl.kt @@ -87,15 +87,16 @@ class RecognitionTasksRepositoryImpl( localDataSource.save(taskLocal) } - override suspend fun markRecognitionTask(task: RecognitionTask) { - localDataSource.update(task.toLocalDTO()) - try { - if (networkDataSource.markTask(task.id, task.reviewed)) - getRecognitionTask(task.id, true).firstOrNull() + override suspend fun markRecognitionTask(task: RecognitionTask) + = try { + networkDataSource.markTask(task.id, task.reviewed).also { + if (it) + getRecognitionTask(task.id, true).firstOrNull() + } } catch (e: Throwable) { e.printStackTrace() + false } - } override suspend fun solveRecognitionTask(id: String, answer: String) = try { diff --git a/app/src/main/java/lab/maxb/dark/presentation/repository/interfaces/RecognitionTasksRepository.kt b/app/src/main/java/lab/maxb/dark/presentation/repository/interfaces/RecognitionTasksRepository.kt index d25a37d..0f510d9 100644 --- a/app/src/main/java/lab/maxb/dark/presentation/repository/interfaces/RecognitionTasksRepository.kt +++ b/app/src/main/java/lab/maxb/dark/presentation/repository/interfaces/RecognitionTasksRepository.kt @@ -10,6 +10,6 @@ interface RecognitionTasksRepository { suspend fun getRecognitionTask(id: String, forceUpdate: Boolean = false): Flow fun getRecognitionTaskImage(path: String): GlideUrl suspend fun addRecognitionTask(task: RecognitionTask) - suspend fun markRecognitionTask(task: RecognitionTask) + suspend fun markRecognitionTask(task: RecognitionTask): Boolean suspend fun solveRecognitionTask(id: String, answer: String): Boolean } \ No newline at end of file diff --git a/app/src/main/java/lab/maxb/dark/presentation/repository/room/dao/RecognitionTasksDAO.kt b/app/src/main/java/lab/maxb/dark/presentation/repository/room/dao/RecognitionTasksDAO.kt index 777717c..f2910e7 100644 --- a/app/src/main/java/lab/maxb/dark/presentation/repository/room/dao/RecognitionTasksDAO.kt +++ b/app/src/main/java/lab/maxb/dark/presentation/repository/room/dao/RecognitionTasksDAO.kt @@ -21,5 +21,5 @@ abstract class RecognitionTasksDAO: AdvancedDAO( abstract fun getAllPaged(): PagingSource @Query("SELECT * FROM recognition_task WHERE id = :id") - abstract fun get(id: String): Flow + abstract fun get(id: String): Flow } \ No newline at end of file diff --git a/app/src/main/java/lab/maxb/dark/presentation/view/SolveRecognitionTaskFragment.kt b/app/src/main/java/lab/maxb/dark/presentation/view/SolveRecognitionTaskFragment.kt index 83a8895..5744abe 100644 --- a/app/src/main/java/lab/maxb/dark/presentation/view/SolveRecognitionTaskFragment.kt +++ b/app/src/main/java/lab/maxb/dark/presentation/view/SolveRecognitionTaskFragment.kt @@ -89,8 +89,8 @@ class SolveRecognitionTaskFragment : Fragment(R.layout.solve_recognition_task_fr } private fun mark(isAllowed: Boolean) = launchRepeatingOnLifecycle { - mViewModel.mark(isAllowed) - goBack() + if (mViewModel.mark(isAllowed)) + goBack() } override fun onCreate(savedInstanceState: Bundle?) { diff --git a/app/src/main/java/lab/maxb/dark/presentation/viewModel/SolveRecognitionTaskViewModel.kt b/app/src/main/java/lab/maxb/dark/presentation/viewModel/SolveRecognitionTaskViewModel.kt index b7c48a1..6d7ffe0 100644 --- a/app/src/main/java/lab/maxb/dark/presentation/viewModel/SolveRecognitionTaskViewModel.kt +++ b/app/src/main/java/lab/maxb/dark/presentation/viewModel/SolveRecognitionTaskViewModel.kt @@ -47,13 +47,11 @@ class SolveRecognitionTaskViewModel( } }.stateIn(false) - suspend fun mark(isAllowed: Boolean) { - (getCurrentTask() ?: return).apply { - reviewed = isAllowed - }.also { + suspend fun mark(isAllowed: Boolean) = + getCurrentTask()?.let { + it.reviewed = isAllowed recognitionTasksRepository.markRecognitionTask(it) - } - } + } ?: false suspend fun solveRecognitionTask() = getCurrentTask()?.let { recognitionTasksRepository.solveRecognitionTask( From 81b3a729e25ad22d031dd323fe2bfd39c59672d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=91=D0=B0=D0=B1=D1=8B=D1=88=D0=B5=D0=B2=20=D0=9C=D0=B0?= =?UTF-8?q?=D0=BA=D1=81=D0=B8=D0=BC?= Date: Thu, 2 Jun 2022 00:17:12 +0300 Subject: [PATCH 14/22] Handle paging in "replace cache" way --- .../implementation/RecognitionTasksRepositoryImpl.kt | 8 +++++--- .../repository/network/dark/DarkServiceImpl.kt | 1 - .../repository/room/dao/RecognitionTasksDAO.kt | 9 +++++++++ .../dark/presentation/repository/utils/BaseResource.kt | 10 +++++++--- .../maxb/dark/presentation/viewModel/AuthViewModel.kt | 3 +-- 5 files changed, 22 insertions(+), 9 deletions(-) diff --git a/app/src/main/java/lab/maxb/dark/presentation/repository/implementation/RecognitionTasksRepositoryImpl.kt b/app/src/main/java/lab/maxb/dark/presentation/repository/implementation/RecognitionTasksRepositoryImpl.kt index 1808b00..d9a58cc 100644 --- a/app/src/main/java/lab/maxb/dark/presentation/repository/implementation/RecognitionTasksRepositoryImpl.kt +++ b/app/src/main/java/lab/maxb/dark/presentation/repository/implementation/RecognitionTasksRepositoryImpl.kt @@ -17,7 +17,6 @@ import lab.maxb.dark.presentation.repository.network.dark.DarkService import lab.maxb.dark.presentation.repository.network.dark.model.toDomain import lab.maxb.dark.presentation.repository.network.dark.model.toNetworkDTO import lab.maxb.dark.presentation.repository.room.LocalDatabase -import lab.maxb.dark.presentation.repository.room.model.toDomain import lab.maxb.dark.presentation.repository.room.model.toLocalDTO import lab.maxb.dark.presentation.repository.room.relations.toDomain import lab.maxb.dark.presentation.repository.utils.Resource @@ -46,11 +45,14 @@ class RecognitionTasksRepositoryImpl( it.toDomain { getUser(it.owner_id) } } } + isEmptyResponse = { + it.isNullOrEmpty() + } localStore = { tasks -> tasks.map { it.toLocalDTO() }.toTypedArray().let { - localDataSource.save(*it) + localDataSource.saveOnly(*it) } } clearLocalStore = { page -> @@ -61,7 +63,7 @@ class RecognitionTasksRepositoryImpl( @OptIn(ExperimentalPagingApi::class) private val pager = Pager( - config = PagingConfig(pageSize = 5), + config = PagingConfig(pageSize = 50), remoteMediator = RecognitionTaskMediator(tasksResource, db.remoteKeys()), ) { localDataSource.getAllPaged() diff --git a/app/src/main/java/lab/maxb/dark/presentation/repository/network/dark/DarkServiceImpl.kt b/app/src/main/java/lab/maxb/dark/presentation/repository/network/dark/DarkServiceImpl.kt index 38892a5..af268a1 100644 --- a/app/src/main/java/lab/maxb/dark/presentation/repository/network/dark/DarkServiceImpl.kt +++ b/app/src/main/java/lab/maxb/dark/presentation/repository/network/dark/DarkServiceImpl.kt @@ -3,7 +3,6 @@ package lab.maxb.dark.presentation.repository.network.dark import com.bumptech.glide.load.model.GlideUrl import com.bumptech.glide.load.model.LazyHeaders import com.google.gson.GsonBuilder -import kotlinx.coroutines.delay import lab.maxb.dark.BuildConfig import lab.maxb.dark.presentation.repository.network.dark.model.AuthRequest import lab.maxb.dark.presentation.repository.network.dark.model.RecognitionTaskCreationNetworkDTO diff --git a/app/src/main/java/lab/maxb/dark/presentation/repository/room/dao/RecognitionTasksDAO.kt b/app/src/main/java/lab/maxb/dark/presentation/repository/room/dao/RecognitionTasksDAO.kt index f2910e7..bfbaf0d 100644 --- a/app/src/main/java/lab/maxb/dark/presentation/repository/room/dao/RecognitionTasksDAO.kt +++ b/app/src/main/java/lab/maxb/dark/presentation/repository/room/dao/RecognitionTasksDAO.kt @@ -22,4 +22,13 @@ abstract class RecognitionTasksDAO: AdvancedDAO( @Query("SELECT * FROM recognition_task WHERE id = :id") abstract fun get(id: String): Flow + + @Query("DELETE FROM recognition_task WHERE NOT (id in (:id))") + abstract fun deleteOther(id: List) + + @Transaction + open suspend fun saveOnly(vararg value: RecognitionTaskLocalDTO) { + save(*value) + deleteOther(value.map { it.id }) + } } \ No newline at end of file diff --git a/app/src/main/java/lab/maxb/dark/presentation/repository/utils/BaseResource.kt b/app/src/main/java/lab/maxb/dark/presentation/repository/utils/BaseResource.kt index 8e25e03..86ad076 100644 --- a/app/src/main/java/lab/maxb/dark/presentation/repository/utils/BaseResource.kt +++ b/app/src/main/java/lab/maxb/dark/presentation/repository/utils/BaseResource.kt @@ -9,6 +9,7 @@ open class BaseResource { open lateinit var fetchLocal: suspend (Input) -> Flow open lateinit var fetchRemote: suspend (Input) -> Output? open lateinit var isFresh: (suspend (Input) -> Boolean) + open var isEmptyResponse: (suspend (Output?) -> Boolean) = { it == null } open var onRefresh: (suspend () -> Unit)? = null open var localStore: (suspend (Output) -> Unit)? = null open var clearLocalStore: (suspend (Input) -> Unit)? = null @@ -18,9 +19,12 @@ open class BaseResource { suspend fun query(args: Input, force: Boolean = false) = flow { if (force || !isFresh(args)) { try { - fetchRemote(args)?.run { - localStore?.invoke(this) - } ?: clearLocalStore?.invoke(args) + fetchRemote(args).also { + if (isEmptyResponse(it)) + clearLocalStore?.invoke(args) + else + localStore?.invoke(it!!) + } onRefresh?.invoke() } catch (e: UnableToObtainResource) {} } diff --git a/app/src/main/java/lab/maxb/dark/presentation/viewModel/AuthViewModel.kt b/app/src/main/java/lab/maxb/dark/presentation/viewModel/AuthViewModel.kt index b21bc36..07778c4 100644 --- a/app/src/main/java/lab/maxb/dark/presentation/viewModel/AuthViewModel.kt +++ b/app/src/main/java/lab/maxb/dark/presentation/viewModel/AuthViewModel.kt @@ -4,7 +4,6 @@ import androidx.lifecycle.ViewModel import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.* -import kotlinx.coroutines.withTimeout import lab.maxb.dark.domain.model.Profile import lab.maxb.dark.presentation.extra.UserSettings import lab.maxb.dark.presentation.extra.launch @@ -15,7 +14,6 @@ import lab.maxb.dark.presentation.viewModel.utils.UiState import lab.maxb.dark.presentation.viewModel.utils.stateIn import lab.maxb.dark.presentation.viewModel.utils.valueOrNull import org.koin.android.annotation.KoinViewModel -import java.time.Duration @OptIn(ExperimentalCoroutinesApi::class) @@ -65,6 +63,7 @@ class AuthViewModel( } } + @Suppress("unused", "UNUSED_PARAMETER") fun authorizeByOAUTHProvider(login: String, name: String, authCode: String) { TODO() } From ad9eb3bfc25a113f35d30f481e4ee199dc979ba8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=91=D0=B0=D0=B1=D1=8B=D1=88=D0=B5=D0=B2=20=D0=9C=D0=B0?= =?UTF-8?q?=D0=BA=D1=81=D0=B8=D0=BC?= Date: Thu, 2 Jun 2022 00:19:54 +0300 Subject: [PATCH 15/22] Extract UserSettings Interface --- .../dark/presentation/extra/UserSettings.kt | 26 +++---------------- .../presentation/extra/UserSettingsImpl.kt | 25 ++++++++++++++++++ 2 files changed, 29 insertions(+), 22 deletions(-) create mode 100644 app/src/main/java/lab/maxb/dark/presentation/extra/UserSettingsImpl.kt diff --git a/app/src/main/java/lab/maxb/dark/presentation/extra/UserSettings.kt b/app/src/main/java/lab/maxb/dark/presentation/extra/UserSettings.kt index 783db0e..ac9f015 100644 --- a/app/src/main/java/lab/maxb/dark/presentation/extra/UserSettings.kt +++ b/app/src/main/java/lab/maxb/dark/presentation/extra/UserSettings.kt @@ -1,24 +1,6 @@ package lab.maxb.dark.presentation.extra -import android.content.Context -import android.content.SharedPreferences -import androidx.preference.PreferenceManager -import androidx.security.crypto.EncryptedSharedPreferences -import androidx.security.crypto.MasterKey -import lab.maxb.dark.BuildConfig -import lab.maxb.dark.presentation.extra.delegates.property -import org.koin.core.annotation.Single - -@Single -class UserSettings(context: Context) { - private val pref = PreferenceManager.getDefaultSharedPreferences(context) - private val securePref: SharedPreferences = EncryptedSharedPreferences.create( - context, - "secure_dark.preferences", - MasterKey.Builder(context, BuildConfig.SECURE_PREFS_MASTER_KEY).setKeyScheme(MasterKey.KeyScheme.AES256_GCM).build(), - EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV, - EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM - ) - var token: String by securePref.property() - var login: String by pref.property() -} \ No newline at end of file +interface UserSettings { + var token: String + var login: String +} diff --git a/app/src/main/java/lab/maxb/dark/presentation/extra/UserSettingsImpl.kt b/app/src/main/java/lab/maxb/dark/presentation/extra/UserSettingsImpl.kt new file mode 100644 index 0000000..a0a1488 --- /dev/null +++ b/app/src/main/java/lab/maxb/dark/presentation/extra/UserSettingsImpl.kt @@ -0,0 +1,25 @@ +package lab.maxb.dark.presentation.extra + +import android.content.Context +import android.content.SharedPreferences +import androidx.preference.PreferenceManager +import androidx.security.crypto.EncryptedSharedPreferences +import androidx.security.crypto.MasterKey +import lab.maxb.dark.BuildConfig +import lab.maxb.dark.presentation.extra.delegates.property +import org.koin.core.annotation.Single + +@Single +class UserSettingsImpl(context: Context) : UserSettings { + private val pref = PreferenceManager.getDefaultSharedPreferences(context) + private val securePref: SharedPreferences = EncryptedSharedPreferences.create( + context, + "secure_dark.preferences", + MasterKey.Builder(context, BuildConfig.SECURE_PREFS_MASTER_KEY) + .setKeyScheme(MasterKey.KeyScheme.AES256_GCM).build(), + EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV, + EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM + ) + override var token: String by securePref.property() + override var login: String by pref.property() +} \ No newline at end of file From 9644df2862b718ac3b89c77bb8ec795c50bb5667 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=91=D0=B0=D0=B1=D1=8B=D1=88=D0=B5=D0=B2=20=D0=9C=D0=B0?= =?UTF-8?q?=D0=BA=D1=81=D0=B8=D0=BC?= Date: Thu, 2 Jun 2022 02:31:35 +0300 Subject: [PATCH 16/22] Handle unauthorized state & token expiration --- .../implementation/ProfileRepositoryImpl.kt | 74 ++++++++++++++----- .../repository/network/dark/DarkService.kt | 2 + .../network/dark/DarkServiceImpl.kt | 17 ++++- .../repository/network/dark/model/Auth.kt | 13 ++++ .../repository/room/dao/ProfilesDAO.kt | 3 + .../dark/presentation/view/MainActivity.kt | 4 +- 6 files changed, 90 insertions(+), 23 deletions(-) diff --git a/app/src/main/java/lab/maxb/dark/presentation/repository/implementation/ProfileRepositoryImpl.kt b/app/src/main/java/lab/maxb/dark/presentation/repository/implementation/ProfileRepositoryImpl.kt index 85d5f61..a9bb4f4 100644 --- a/app/src/main/java/lab/maxb/dark/presentation/repository/implementation/ProfileRepositoryImpl.kt +++ b/app/src/main/java/lab/maxb/dark/presentation/repository/implementation/ProfileRepositoryImpl.kt @@ -3,15 +3,21 @@ package lab.maxb.dark.presentation.repository.implementation import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.* import lab.maxb.dark.domain.model.Profile +import lab.maxb.dark.domain.model.User import lab.maxb.dark.presentation.extra.UserSettings import lab.maxb.dark.presentation.repository.interfaces.ProfileRepository import lab.maxb.dark.presentation.repository.interfaces.UsersRepository import lab.maxb.dark.presentation.repository.network.dark.DarkService import lab.maxb.dark.presentation.repository.network.dark.model.AuthRequest +import lab.maxb.dark.presentation.repository.network.dark.model.toProfile import lab.maxb.dark.presentation.repository.room.LocalDatabase +import lab.maxb.dark.presentation.repository.room.model.toDomain import lab.maxb.dark.presentation.repository.room.model.toLocalDTO import lab.maxb.dark.presentation.repository.room.relations.toDomain +import lab.maxb.dark.presentation.repository.utils.RefreshControllerImpl +import lab.maxb.dark.presentation.repository.utils.Resource import org.koin.core.annotation.Single +import java.time.Duration @Single class ProfileRepositoryImpl( @@ -21,31 +27,59 @@ class ProfileRepositoryImpl( private val usersRepository: UsersRepository, ) : ProfileRepository { private val localDataSource = db.profiles() - private val _login = MutableStateFlow(userSettings.login) + private val _credentials = MutableStateFlow(Credentials( + userSettings.login, + )) + + init { + networkDataSource.onAuthRequired = { + localDataSource.clear() + } + } + + @OptIn(ExperimentalCoroutinesApi::class) + override val profile = _credentials.flatMapLatest { + profileResource.query(it, true) + } @OptIn(ExperimentalCoroutinesApi::class) - override val profile = _login.flatMapLatest { login -> - localDataSource.getByLogin(login).distinctUntilChanged().mapLatest { fullProfile -> - fullProfile?.toDomain() + private val profileResource = Resource( + RefreshControllerImpl(Duration.ofHours(12).toMillis()) + ).apply { + fetchLocal = { + localDataSource.getByLogin(it.login).mapLatest { x -> x?.toDomain() } + } + fetchRemote = remote@ { + it.password ?: run { + localDataSource.getUserIdByLogin(it.login)?.let { id -> + usersRepository.getUser(id).firstOrNull() + } + return@remote null + } + + val request = it.toRequest() + val response = if (it.initial) + networkDataSource.signup(request) + else + networkDataSource.login(request) + userSettings.token = response.token + userSettings.login = it.login + response.toProfile(it.login) { id -> + usersRepository.getUser(id).firstOrNull() ?: return@remote null + } } + localStore = { localDataSource.save(it.toLocalDTO()) } } override suspend fun sendCredentials(login: String, password: String, initial: Boolean) { - val request = AuthRequest(login, password) - val response = if (initial) - networkDataSource.signup(request) - else - networkDataSource.login(request) - userSettings.token = response.token - userSettings.login = login - localDataSource.save( - Profile( - login, - usersRepository.getUser(response.id).firstOrNull()!!, - response.token, - role = response.role - ).toLocalDTO() - ) - _login.value = login + _credentials.value = Credentials(login, password, initial) } + + private data class Credentials( + val login: String, + val password: String? = null, + val initial: Boolean = false, + ) + + private fun Credentials.toRequest() = AuthRequest(login, password!!) } \ No newline at end of file diff --git a/app/src/main/java/lab/maxb/dark/presentation/repository/network/dark/DarkService.kt b/app/src/main/java/lab/maxb/dark/presentation/repository/network/dark/DarkService.kt index fafe4f3..f1935cb 100644 --- a/app/src/main/java/lab/maxb/dark/presentation/repository/network/dark/DarkService.kt +++ b/app/src/main/java/lab/maxb/dark/presentation/repository/network/dark/DarkService.kt @@ -17,4 +17,6 @@ interface DarkService { suspend fun getUser(id: String): User? suspend fun login(request: AuthRequest): AuthResponse suspend fun signup(request: AuthRequest): AuthResponse + + var onAuthRequired: (suspend () -> Unit)? } diff --git a/app/src/main/java/lab/maxb/dark/presentation/repository/network/dark/DarkServiceImpl.kt b/app/src/main/java/lab/maxb/dark/presentation/repository/network/dark/DarkServiceImpl.kt index af268a1..ae1b0c2 100644 --- a/app/src/main/java/lab/maxb/dark/presentation/repository/network/dark/DarkServiceImpl.kt +++ b/app/src/main/java/lab/maxb/dark/presentation/repository/network/dark/DarkServiceImpl.kt @@ -3,6 +3,7 @@ package lab.maxb.dark.presentation.repository.network.dark import com.bumptech.glide.load.model.GlideUrl import com.bumptech.glide.load.model.LazyHeaders import com.google.gson.GsonBuilder +import kotlinx.coroutines.delay import lab.maxb.dark.BuildConfig import lab.maxb.dark.presentation.repository.network.dark.model.AuthRequest import lab.maxb.dark.presentation.repository.network.dark.model.RecognitionTaskCreationNetworkDTO @@ -13,15 +14,17 @@ import org.koin.core.annotation.Single import retrofit2.Retrofit import retrofit2.converter.gson.GsonConverterFactory import java.io.EOFException +import java.net.ConnectException import java.net.SocketTimeoutException import java.net.UnknownHostException import java.time.Duration @Single class DarkServiceImpl( - private val authInterceptor: AuthInterceptor, + private val authInterceptor: AuthInterceptor ) : DarkService { private val api = buildDarkService() + override var onAuthRequired: (suspend () -> Unit)? = null override suspend fun getAllTasks(page: Int, size: Int) = catchAll { api.getAllTasks(page, size) @@ -74,6 +77,14 @@ class DarkServiceImpl( null as T } catch (e: UnknownHostException) { throw UnableToObtainResource() + } catch (e: ConnectException) { + throw UnableToObtainResource() + } catch (e: retrofit2.HttpException) { + when (e.code()) { + 401, 403 -> onAuthRequired?.invoke() + else -> e.printStackTrace() + } + throw UnableToObtainResource() } catch (e: Throwable) { e.printStackTrace() throw e @@ -86,7 +97,8 @@ class DarkServiceImpl( try { return block() } catch (e: SocketTimeoutException) { - // Ignore + if (it != LAST_RETRY) + delay(RETRY_DELAY) } } throw UnableToObtainResource() @@ -114,6 +126,7 @@ class DarkServiceImpl( companion object { const val MAX_RETRY_COUNT = 6 + private const val LAST_RETRY = MAX_RETRY_COUNT-1 val RETRY_DELAY = Duration.ofSeconds(1).toMillis() } } diff --git a/app/src/main/java/lab/maxb/dark/presentation/repository/network/dark/model/Auth.kt b/app/src/main/java/lab/maxb/dark/presentation/repository/network/dark/model/Auth.kt index ee925e7..f1b4f72 100644 --- a/app/src/main/java/lab/maxb/dark/presentation/repository/network/dark/model/Auth.kt +++ b/app/src/main/java/lab/maxb/dark/presentation/repository/network/dark/model/Auth.kt @@ -1,6 +1,9 @@ package lab.maxb.dark.presentation.repository.network.dark.model +import lab.maxb.dark.domain.model.Profile +import lab.maxb.dark.domain.model.RecognitionTask import lab.maxb.dark.domain.model.Role +import lab.maxb.dark.domain.model.User class AuthRequest( var login: String, @@ -12,3 +15,13 @@ class AuthResponse( var id: String, var role: Role, ) + +inline fun AuthResponse.toProfile( + login: String, + user: (String) -> User? = { null }, +) = Profile( + login, + user(id), + token, + role = role +) \ No newline at end of file diff --git a/app/src/main/java/lab/maxb/dark/presentation/repository/room/dao/ProfilesDAO.kt b/app/src/main/java/lab/maxb/dark/presentation/repository/room/dao/ProfilesDAO.kt index 2dd0848..884ca2e 100644 --- a/app/src/main/java/lab/maxb/dark/presentation/repository/room/dao/ProfilesDAO.kt +++ b/app/src/main/java/lab/maxb/dark/presentation/repository/room/dao/ProfilesDAO.kt @@ -10,4 +10,7 @@ import lab.maxb.dark.presentation.repository.room.relations.FullProfileLocalDTO abstract class ProfilesDAO: AdvancedDAO("profile") { @Query("SELECT * FROM profile WHERE id = :login") abstract fun getByLogin(login: String): Flow + + @Query("SELECT user_id FROM profile WHERE id = :login") + abstract suspend fun getUserIdByLogin(login: String): String? } \ No newline at end of file diff --git a/app/src/main/java/lab/maxb/dark/presentation/view/MainActivity.kt b/app/src/main/java/lab/maxb/dark/presentation/view/MainActivity.kt index 3954857..6ed2b19 100644 --- a/app/src/main/java/lab/maxb/dark/presentation/view/MainActivity.kt +++ b/app/src/main/java/lab/maxb/dark/presentation/view/MainActivity.kt @@ -88,8 +88,10 @@ class MainActivity : AppCompatActivity(R.layout.main_activity), authViewModel.profile observe { it.ifLoaded { profile -> - if (profile == null && navController.currentDestination?.id != R.id.nav_auth_fragment) + if (profile == null && navController.currentDestination?.id != R.id.nav_auth_fragment) { + authViewModel.signOut() navController.navigate(NavGraphDirections.navToAuthFragment()) + } } } } From b6ec0a443157201d0aa4a4b0994ae49a355b52fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=91=D0=B0=D0=B1=D1=8B=D1=88=D0=B5=D0=B2=20=D0=9C=D0=B0?= =?UTF-8?q?=D0=BA=D1=81=D0=B8=D0=BC?= Date: Thu, 2 Jun 2022 13:57:39 +0300 Subject: [PATCH 17/22] Simplify auth handling --- .../maxb/dark/domain/model/AuthCredentials.kt | 14 +++++++ .../dark/domain/operations/Credentials.kt | 14 +++++++ .../implementation/ProfileRepositoryImpl.kt | 39 ++++++++----------- .../interfaces/ProfileRepository.kt | 3 +- .../network/dark/DarkServiceImpl.kt | 9 ++--- .../repository/network/dark/model/Auth.kt | 18 ++++----- .../dark/presentation/view/AuthFragment.kt | 27 +++++++------ .../presentation/view/AuthHandleFragment.kt | 5 ++- .../dark/presentation/view/MainActivity.kt | 6 +-- .../presentation/viewModel/AuthViewModel.kt | 28 +++++++++---- 10 files changed, 97 insertions(+), 66 deletions(-) create mode 100644 app/src/main/java/lab/maxb/dark/domain/model/AuthCredentials.kt create mode 100644 app/src/main/java/lab/maxb/dark/domain/operations/Credentials.kt diff --git a/app/src/main/java/lab/maxb/dark/domain/model/AuthCredentials.kt b/app/src/main/java/lab/maxb/dark/domain/model/AuthCredentials.kt new file mode 100644 index 0000000..cf27cb8 --- /dev/null +++ b/app/src/main/java/lab/maxb/dark/domain/model/AuthCredentials.kt @@ -0,0 +1,14 @@ +package lab.maxb.dark.domain.model + +data class AuthCredentials( + val login: String, + val password: String? = null, + val initial: Boolean = false, +) + +data class ReceivedAuthCredentials( + val token: String, + val id: String, + val role: Role, + val request: AuthCredentials, +) \ No newline at end of file diff --git a/app/src/main/java/lab/maxb/dark/domain/operations/Credentials.kt b/app/src/main/java/lab/maxb/dark/domain/operations/Credentials.kt new file mode 100644 index 0000000..d4494d2 --- /dev/null +++ b/app/src/main/java/lab/maxb/dark/domain/operations/Credentials.kt @@ -0,0 +1,14 @@ +package lab.maxb.dark.domain.operations + +import lab.maxb.dark.domain.model.Profile +import lab.maxb.dark.domain.model.ReceivedAuthCredentials +import lab.maxb.dark.domain.model.User + +inline fun ReceivedAuthCredentials.toProfile( + user: (String) -> User? = { null }, +) = Profile( + request.login, + user(id), + token, + role = role +) \ No newline at end of file diff --git a/app/src/main/java/lab/maxb/dark/presentation/repository/implementation/ProfileRepositoryImpl.kt b/app/src/main/java/lab/maxb/dark/presentation/repository/implementation/ProfileRepositoryImpl.kt index a9bb4f4..fa76ba6 100644 --- a/app/src/main/java/lab/maxb/dark/presentation/repository/implementation/ProfileRepositoryImpl.kt +++ b/app/src/main/java/lab/maxb/dark/presentation/repository/implementation/ProfileRepositoryImpl.kt @@ -1,17 +1,20 @@ package lab.maxb.dark.presentation.repository.implementation import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.flow.* +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.firstOrNull +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.mapLatest +import lab.maxb.dark.domain.model.AuthCredentials import lab.maxb.dark.domain.model.Profile -import lab.maxb.dark.domain.model.User +import lab.maxb.dark.domain.operations.toProfile import lab.maxb.dark.presentation.extra.UserSettings import lab.maxb.dark.presentation.repository.interfaces.ProfileRepository import lab.maxb.dark.presentation.repository.interfaces.UsersRepository import lab.maxb.dark.presentation.repository.network.dark.DarkService -import lab.maxb.dark.presentation.repository.network.dark.model.AuthRequest -import lab.maxb.dark.presentation.repository.network.dark.model.toProfile +import lab.maxb.dark.presentation.repository.network.dark.model.toDomain +import lab.maxb.dark.presentation.repository.network.dark.model.toNetworkDTO import lab.maxb.dark.presentation.repository.room.LocalDatabase -import lab.maxb.dark.presentation.repository.room.model.toDomain import lab.maxb.dark.presentation.repository.room.model.toLocalDTO import lab.maxb.dark.presentation.repository.room.relations.toDomain import lab.maxb.dark.presentation.repository.utils.RefreshControllerImpl @@ -27,9 +30,7 @@ class ProfileRepositoryImpl( private val usersRepository: UsersRepository, ) : ProfileRepository { private val localDataSource = db.profiles() - private val _credentials = MutableStateFlow(Credentials( - userSettings.login, - )) + private val _credentials = MutableStateFlow(AuthCredentials(userSettings.login)) init { networkDataSource.onAuthRequired = { @@ -43,7 +44,7 @@ class ProfileRepositoryImpl( } @OptIn(ExperimentalCoroutinesApi::class) - private val profileResource = Resource( + private val profileResource = Resource( RefreshControllerImpl(Duration.ofHours(12).toMillis()) ).apply { fetchLocal = { @@ -57,29 +58,21 @@ class ProfileRepositoryImpl( return@remote null } - val request = it.toRequest() - val response = if (it.initial) + val request = it.toNetworkDTO() + val response = (if (it.initial) networkDataSource.signup(request) else - networkDataSource.login(request) + networkDataSource.login(request)).toDomain(it) userSettings.token = response.token userSettings.login = it.login - response.toProfile(it.login) { id -> + response.toProfile { id -> usersRepository.getUser(id).firstOrNull() ?: return@remote null } } localStore = { localDataSource.save(it.toLocalDTO()) } } - override suspend fun sendCredentials(login: String, password: String, initial: Boolean) { - _credentials.value = Credentials(login, password, initial) + override fun sendCredentials(credentials: AuthCredentials) { + _credentials.value = credentials } - - private data class Credentials( - val login: String, - val password: String? = null, - val initial: Boolean = false, - ) - - private fun Credentials.toRequest() = AuthRequest(login, password!!) } \ No newline at end of file diff --git a/app/src/main/java/lab/maxb/dark/presentation/repository/interfaces/ProfileRepository.kt b/app/src/main/java/lab/maxb/dark/presentation/repository/interfaces/ProfileRepository.kt index 80a123d..ef5dbe9 100644 --- a/app/src/main/java/lab/maxb/dark/presentation/repository/interfaces/ProfileRepository.kt +++ b/app/src/main/java/lab/maxb/dark/presentation/repository/interfaces/ProfileRepository.kt @@ -1,9 +1,10 @@ package lab.maxb.dark.presentation.repository.interfaces import kotlinx.coroutines.flow.Flow +import lab.maxb.dark.domain.model.AuthCredentials import lab.maxb.dark.domain.model.Profile interface ProfileRepository { - suspend fun sendCredentials(login: String, password: String, initial: Boolean = false) + fun sendCredentials(credentials: AuthCredentials) val profile: Flow } diff --git a/app/src/main/java/lab/maxb/dark/presentation/repository/network/dark/DarkServiceImpl.kt b/app/src/main/java/lab/maxb/dark/presentation/repository/network/dark/DarkServiceImpl.kt index ae1b0c2..41fd878 100644 --- a/app/src/main/java/lab/maxb/dark/presentation/repository/network/dark/DarkServiceImpl.kt +++ b/app/src/main/java/lab/maxb/dark/presentation/repository/network/dark/DarkServiceImpl.kt @@ -3,7 +3,6 @@ package lab.maxb.dark.presentation.repository.network.dark import com.bumptech.glide.load.model.GlideUrl import com.bumptech.glide.load.model.LazyHeaders import com.google.gson.GsonBuilder -import kotlinx.coroutines.delay import lab.maxb.dark.BuildConfig import lab.maxb.dark.presentation.repository.network.dark.model.AuthRequest import lab.maxb.dark.presentation.repository.network.dark.model.RecognitionTaskCreationNetworkDTO @@ -17,7 +16,6 @@ import java.io.EOFException import java.net.ConnectException import java.net.SocketTimeoutException import java.net.UnknownHostException -import java.time.Duration @Single class DarkServiceImpl( @@ -75,6 +73,8 @@ class DarkServiceImpl( retry(block) } catch (e: EOFException) { null as T + } catch (e: UnableToObtainResource) { + throw e } catch (e: UnknownHostException) { throw UnableToObtainResource() } catch (e: ConnectException) { @@ -97,8 +97,7 @@ class DarkServiceImpl( try { return block() } catch (e: SocketTimeoutException) { - if (it != LAST_RETRY) - delay(RETRY_DELAY) + // Ignore (delay included in timeout) } } throw UnableToObtainResource() @@ -126,8 +125,6 @@ class DarkServiceImpl( companion object { const val MAX_RETRY_COUNT = 6 - private const val LAST_RETRY = MAX_RETRY_COUNT-1 - val RETRY_DELAY = Duration.ofSeconds(1).toMillis() } } diff --git a/app/src/main/java/lab/maxb/dark/presentation/repository/network/dark/model/Auth.kt b/app/src/main/java/lab/maxb/dark/presentation/repository/network/dark/model/Auth.kt index f1b4f72..daf3337 100644 --- a/app/src/main/java/lab/maxb/dark/presentation/repository/network/dark/model/Auth.kt +++ b/app/src/main/java/lab/maxb/dark/presentation/repository/network/dark/model/Auth.kt @@ -1,9 +1,8 @@ package lab.maxb.dark.presentation.repository.network.dark.model -import lab.maxb.dark.domain.model.Profile -import lab.maxb.dark.domain.model.RecognitionTask +import lab.maxb.dark.domain.model.AuthCredentials +import lab.maxb.dark.domain.model.ReceivedAuthCredentials import lab.maxb.dark.domain.model.Role -import lab.maxb.dark.domain.model.User class AuthRequest( var login: String, @@ -16,12 +15,11 @@ class AuthResponse( var role: Role, ) -inline fun AuthResponse.toProfile( - login: String, - user: (String) -> User? = { null }, -) = Profile( +fun AuthCredentials.toNetworkDTO() = AuthRequest( login, - user(id), - token, - role = role + password!!, +) + +fun AuthResponse.toDomain(request: AuthCredentials) = ReceivedAuthCredentials( + token, id, role, request, ) \ No newline at end of file diff --git a/app/src/main/java/lab/maxb/dark/presentation/view/AuthFragment.kt b/app/src/main/java/lab/maxb/dark/presentation/view/AuthFragment.kt index 6ac7eb5..784d882 100644 --- a/app/src/main/java/lab/maxb/dark/presentation/view/AuthFragment.kt +++ b/app/src/main/java/lab/maxb/dark/presentation/view/AuthFragment.kt @@ -13,13 +13,11 @@ import androidx.fragment.app.Fragment import androidx.lifecycle.DefaultLifecycleObserver import androidx.lifecycle.LifecycleOwner import com.wada811.databinding.dataBinding -import kotlinx.coroutines.flow.collectLatest import lab.maxb.dark.NavGraphDirections import lab.maxb.dark.R import lab.maxb.dark.databinding.AuthFragmentBinding import lab.maxb.dark.domain.model.Profile import lab.maxb.dark.domain.operations.unicname -import lab.maxb.dark.presentation.extra.launchRepeatingOnLifecycle import lab.maxb.dark.presentation.extra.navigate import lab.maxb.dark.presentation.extra.observe import lab.maxb.dark.presentation.extra.setPasswordVisibility @@ -73,20 +71,21 @@ class AuthFragment : Fragment(R.layout.auth_fragment) { }?.let { return@setOnClickListener } mViewModel.isLoading.value = true - launchRepeatingOnLifecycle { - mViewModel.authorize() - mViewModel.profile.collectLatest { state -> - state.ifLoaded { - val message = if (mViewModel.isAccountNew.value) - R.string.auth_message_signup_incorrectCredentials - else - R.string.auth_message_login_incorrectCredentials - handleResult(message, it) - } - } - } + mViewModel.authorize() } + mViewModel.profile observe { state -> + if (!mViewModel.isLoading.value) + return@observe + + state.ifLoaded { + val message = if (mViewModel.isAccountNew.value) + R.string.auth_message_signup_incorrectCredentials + else + R.string.auth_message_login_incorrectCredentials + handleResult(message, it) + } + } // mBinding.googleSignIn.setOnClickListener { // mGoogleSignInPage.open(mGoogleSignInLogic.signInIntent) // } diff --git a/app/src/main/java/lab/maxb/dark/presentation/view/AuthHandleFragment.kt b/app/src/main/java/lab/maxb/dark/presentation/view/AuthHandleFragment.kt index 0867a30..c1965ed 100644 --- a/app/src/main/java/lab/maxb/dark/presentation/view/AuthHandleFragment.kt +++ b/app/src/main/java/lab/maxb/dark/presentation/view/AuthHandleFragment.kt @@ -21,9 +21,10 @@ class AuthHandleFragment : Fragment(R.layout.auth_handle_fragment) { super.onViewCreated(view, savedInstanceState) mViewModel.profile observe { it.ifLoaded { profile -> - if (profile == null) + if (profile == null) { + mViewModel.handleNotAuthorizedYet() NavGraphDirections.navToAuthFragment().navigate() - else + } else NavGraphDirections.navToMainFragment().navigate() } } diff --git a/app/src/main/java/lab/maxb/dark/presentation/view/MainActivity.kt b/app/src/main/java/lab/maxb/dark/presentation/view/MainActivity.kt index 6ed2b19..c0deab0 100644 --- a/app/src/main/java/lab/maxb/dark/presentation/view/MainActivity.kt +++ b/app/src/main/java/lab/maxb/dark/presentation/view/MainActivity.kt @@ -88,10 +88,10 @@ class MainActivity : AppCompatActivity(R.layout.main_activity), authViewModel.profile observe { it.ifLoaded { profile -> - if (profile == null && navController.currentDestination?.id != R.id.nav_auth_fragment) { - authViewModel.signOut() + if (profile == null && navController.currentDestination?.id != R.id.nav_auth_fragment) navController.navigate(NavGraphDirections.navToAuthFragment()) - } + + authViewModel.handleAuthorizedStateChanges() } } } diff --git a/app/src/main/java/lab/maxb/dark/presentation/viewModel/AuthViewModel.kt b/app/src/main/java/lab/maxb/dark/presentation/viewModel/AuthViewModel.kt index 07778c4..0dcf312 100644 --- a/app/src/main/java/lab/maxb/dark/presentation/viewModel/AuthViewModel.kt +++ b/app/src/main/java/lab/maxb/dark/presentation/viewModel/AuthViewModel.kt @@ -4,6 +4,7 @@ import androidx.lifecycle.ViewModel import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.* +import lab.maxb.dark.domain.model.AuthCredentials import lab.maxb.dark.domain.model.Profile import lab.maxb.dark.presentation.extra.UserSettings import lab.maxb.dark.presentation.extra.launch @@ -25,6 +26,7 @@ class AuthViewModel( private val userSettings: UserSettings, // private val mGoogleSignInLogic: GoogleSignInLogic, ) : ViewModel() { + private var _wasAuthorized = false private val _profile = MutableStateFlow(UiState.Loading as UiState) val profile = _profile.stateIn(UiState.Loading) @@ -53,16 +55,28 @@ class AuthViewModel( } } - suspend fun authorize() { - try { - _profile.value = UiState.Loading - profileRepository.sendCredentials(login.value, password.value, isAccountNew.value) - } catch (e: Throwable) { - _profile.value = UiState.Error(e) - println(e) + fun handleNotAuthorizedYet() { + _profile.value = UiState.Loading + } + + fun handleAuthorizedStateChanges() = _profile.value.ifLoaded { + if (it != null) + _wasAuthorized = true + else if (_wasAuthorized) { + _wasAuthorized = false + signOut() } } + fun authorize() { + _profile.value = UiState.Loading + profileRepository.sendCredentials(AuthCredentials( + login.value, + password.value, + isAccountNew.value, + )) + } + @Suppress("unused", "UNUSED_PARAMETER") fun authorizeByOAUTHProvider(login: String, name: String, authCode: String) { TODO() From 12ae046a6a88ea1343b71ff0d14e928b6c6e3a40 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=91=D0=B0=D0=B1=D1=8B=D1=88=D0=B5=D0=B2=20=D0=9C=D0=B0?= =?UTF-8?q?=D0=BA=D1=81=D0=B8=D0=BC?= Date: Thu, 2 Jun 2022 16:40:43 +0300 Subject: [PATCH 18/22] Improve caching logic --- .../implementation/ProfileRepositoryImpl.kt | 2 +- .../RecognitionTasksRepositoryImpl.kt | 13 +++-- .../implementation/UsersRepositoryImpl.kt | 6 ++- .../interfaces/RecognitionTasksRepository.kt | 1 + .../repository/interfaces/UsersRepository.kt | 1 + .../repository/utils/BaseResource.kt | 33 ------------ .../presentation/repository/utils/Resource.kt | 51 ++++++++++++++++--- .../repository/utils/StaticResource.kt | 9 ---- .../pagination/RecognitionTasksMediator.kt | 6 +-- .../SolveRecognitionTaskViewModel.kt | 4 +- 10 files changed, 65 insertions(+), 61 deletions(-) delete mode 100644 app/src/main/java/lab/maxb/dark/presentation/repository/utils/BaseResource.kt delete mode 100644 app/src/main/java/lab/maxb/dark/presentation/repository/utils/StaticResource.kt diff --git a/app/src/main/java/lab/maxb/dark/presentation/repository/implementation/ProfileRepositoryImpl.kt b/app/src/main/java/lab/maxb/dark/presentation/repository/implementation/ProfileRepositoryImpl.kt index fa76ba6..0dac312 100644 --- a/app/src/main/java/lab/maxb/dark/presentation/repository/implementation/ProfileRepositoryImpl.kt +++ b/app/src/main/java/lab/maxb/dark/presentation/repository/implementation/ProfileRepositoryImpl.kt @@ -53,7 +53,7 @@ class ProfileRepositoryImpl( fetchRemote = remote@ { it.password ?: run { localDataSource.getUserIdByLogin(it.login)?.let { id -> - usersRepository.getUser(id).firstOrNull() + usersRepository.refresh(id) } return@remote null } diff --git a/app/src/main/java/lab/maxb/dark/presentation/repository/implementation/RecognitionTasksRepositoryImpl.kt b/app/src/main/java/lab/maxb/dark/presentation/repository/implementation/RecognitionTasksRepositoryImpl.kt index d9a58cc..578f3cd 100644 --- a/app/src/main/java/lab/maxb/dark/presentation/repository/implementation/RecognitionTasksRepositoryImpl.kt +++ b/app/src/main/java/lab/maxb/dark/presentation/repository/implementation/RecognitionTasksRepositoryImpl.kt @@ -92,8 +92,7 @@ class RecognitionTasksRepositoryImpl( override suspend fun markRecognitionTask(task: RecognitionTask) = try { networkDataSource.markTask(task.id, task.reviewed).also { - if (it) - getRecognitionTask(task.id, true).firstOrNull() + if (it) refresh(task.id) } } catch (e: Throwable) { e.printStackTrace() @@ -129,11 +128,15 @@ class RecognitionTasksRepositoryImpl( } override suspend fun getRecognitionTask(id: String, forceUpdate: Boolean) - = taskResource.query(id, forceUpdate) + = taskResource.query(id, forceUpdate, true) + + override suspend fun refresh(id: String) { + taskResource.refresh(id) + } override fun getRecognitionTaskImage(path: String) = networkDataSource.getImageSource(path) - private suspend fun getUser(id: String) = - usersRepository.getUser(id).firstOrNull()!! + private suspend fun getUser(id: String) + = usersRepository.getUser(id).firstOrNull()!! } \ No newline at end of file diff --git a/app/src/main/java/lab/maxb/dark/presentation/repository/implementation/UsersRepositoryImpl.kt b/app/src/main/java/lab/maxb/dark/presentation/repository/implementation/UsersRepositoryImpl.kt index 84e9175..addd178 100644 --- a/app/src/main/java/lab/maxb/dark/presentation/repository/implementation/UsersRepositoryImpl.kt +++ b/app/src/main/java/lab/maxb/dark/presentation/repository/implementation/UsersRepositoryImpl.kt @@ -29,5 +29,9 @@ class UsersRepositoryImpl( } override suspend fun getUser(id: String, fresh: Boolean): Flow - = userResource.query(id, fresh) + = userResource.query(id, fresh, true) + + override suspend fun refresh(id: String) { + userResource.refresh(id) + } } \ No newline at end of file diff --git a/app/src/main/java/lab/maxb/dark/presentation/repository/interfaces/RecognitionTasksRepository.kt b/app/src/main/java/lab/maxb/dark/presentation/repository/interfaces/RecognitionTasksRepository.kt index 0f510d9..0739851 100644 --- a/app/src/main/java/lab/maxb/dark/presentation/repository/interfaces/RecognitionTasksRepository.kt +++ b/app/src/main/java/lab/maxb/dark/presentation/repository/interfaces/RecognitionTasksRepository.kt @@ -8,6 +8,7 @@ import lab.maxb.dark.domain.model.RecognitionTask interface RecognitionTasksRepository { fun getAllRecognitionTasks(): Flow> suspend fun getRecognitionTask(id: String, forceUpdate: Boolean = false): Flow + suspend fun refresh(id: String) fun getRecognitionTaskImage(path: String): GlideUrl suspend fun addRecognitionTask(task: RecognitionTask) suspend fun markRecognitionTask(task: RecognitionTask): Boolean diff --git a/app/src/main/java/lab/maxb/dark/presentation/repository/interfaces/UsersRepository.kt b/app/src/main/java/lab/maxb/dark/presentation/repository/interfaces/UsersRepository.kt index 9710e9e..0b21ccd 100644 --- a/app/src/main/java/lab/maxb/dark/presentation/repository/interfaces/UsersRepository.kt +++ b/app/src/main/java/lab/maxb/dark/presentation/repository/interfaces/UsersRepository.kt @@ -5,4 +5,5 @@ import lab.maxb.dark.domain.model.User interface UsersRepository { suspend fun getUser(id: String, fresh: Boolean = false): Flow + suspend fun refresh(id: String) } \ No newline at end of file diff --git a/app/src/main/java/lab/maxb/dark/presentation/repository/utils/BaseResource.kt b/app/src/main/java/lab/maxb/dark/presentation/repository/utils/BaseResource.kt deleted file mode 100644 index 86ad076..0000000 --- a/app/src/main/java/lab/maxb/dark/presentation/repository/utils/BaseResource.kt +++ /dev/null @@ -1,33 +0,0 @@ -package lab.maxb.dark.presentation.repository.utils - -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.emitAll -import kotlinx.coroutines.flow.flow -import lab.maxb.dark.presentation.repository.network.dark.UnableToObtainResource - -open class BaseResource { - open lateinit var fetchLocal: suspend (Input) -> Flow - open lateinit var fetchRemote: suspend (Input) -> Output? - open lateinit var isFresh: (suspend (Input) -> Boolean) - open var isEmptyResponse: (suspend (Output?) -> Boolean) = { it == null } - open var onRefresh: (suspend () -> Unit)? = null - open var localStore: (suspend (Output) -> Unit)? = null - open var clearLocalStore: (suspend (Input) -> Unit)? = null - - protected open suspend fun getCache(args: Input) = fetchLocal(args) - - suspend fun query(args: Input, force: Boolean = false) = flow { - if (force || !isFresh(args)) { - try { - fetchRemote(args).also { - if (isEmptyResponse(it)) - clearLocalStore?.invoke(args) - else - localStore?.invoke(it!!) - } - onRefresh?.invoke() - } catch (e: UnableToObtainResource) {} - } - emitAll(getCache(args)) - } -} diff --git a/app/src/main/java/lab/maxb/dark/presentation/repository/utils/Resource.kt b/app/src/main/java/lab/maxb/dark/presentation/repository/utils/Resource.kt index 45589d5..edac698 100644 --- a/app/src/main/java/lab/maxb/dark/presentation/repository/utils/Resource.kt +++ b/app/src/main/java/lab/maxb/dark/presentation/repository/utils/Resource.kt @@ -1,17 +1,54 @@ package lab.maxb.dark.presentation.repository.utils +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.emitAll import kotlinx.coroutines.flow.firstOrNull +import kotlinx.coroutines.flow.flow +import lab.maxb.dark.presentation.repository.network.dark.UnableToObtainResource -class Resource( +open class Resource( var refreshController: RefreshController = RefreshControllerImpl() -) : BaseResource() { +) { + open lateinit var fetchLocal: suspend (Input) -> Flow + open var fetchLocalSnapshot: suspend (Input) -> Output? = { + fetchLocal(it).firstOrNull() + } + open lateinit var fetchRemote: suspend (Input) -> Output? + open var isFresh: (suspend (Input, Output?) -> Boolean) = { it, cache -> + !refreshController.isExpired() && !isEmptyCache(cache) + } + open var isEmptyResponse: (suspend (Output?) -> Boolean) = { it == null } + open var isEmptyCache: (suspend (Output?) -> Boolean) = { it == null } + open var onRefresh: (suspend () -> Unit)? = { + refreshController.refresh() + } + open var localStore: (suspend (Output) -> Unit)? = null + open var clearLocalStore: (suspend (Input) -> Unit)? = null - override var isFresh: suspend (Input) -> Boolean = { - !refreshController.isExpired() && getCache(it).firstOrNull() != null + suspend fun query(args: Input, force: Boolean = false, useCache: Boolean = false) = flow { + val cache = fetchLocalSnapshot(args) + + if (useCache && !isEmptyCache(cache)) + emit(cache) + + if (force || !isFresh(args, cache)) + refresh(args) + + emitAll(fetchLocal(args)) } - override var onRefresh: (suspend () -> Unit)? = { - refreshController.refresh() + suspend fun refresh(args: Input) { + try { + fetchRemote(args).also { + if (isEmptyResponse(it)) + clearLocalStore?.invoke(args) + else + localStore?.invoke(it!!) + } + onRefresh?.invoke() + } catch (e: UnableToObtainResource) {} } -} + suspend fun checkIsFresh(args: Input) + = isFresh(args, fetchLocalSnapshot(args)) +} \ No newline at end of file diff --git a/app/src/main/java/lab/maxb/dark/presentation/repository/utils/StaticResource.kt b/app/src/main/java/lab/maxb/dark/presentation/repository/utils/StaticResource.kt deleted file mode 100644 index 5c1ef5e..0000000 --- a/app/src/main/java/lab/maxb/dark/presentation/repository/utils/StaticResource.kt +++ /dev/null @@ -1,9 +0,0 @@ -package lab.maxb.dark.presentation.repository.utils - -import kotlinx.coroutines.flow.firstOrNull - -open class StaticResource : BaseResource() { - override var isFresh: suspend (Input) -> Boolean = { - getCache(it).firstOrNull() != null - } -} \ No newline at end of file diff --git a/app/src/main/java/lab/maxb/dark/presentation/repository/utils/pagination/RecognitionTasksMediator.kt b/app/src/main/java/lab/maxb/dark/presentation/repository/utils/pagination/RecognitionTasksMediator.kt index 2b3b24b..684c9e3 100644 --- a/app/src/main/java/lab/maxb/dark/presentation/repository/utils/pagination/RecognitionTasksMediator.kt +++ b/app/src/main/java/lab/maxb/dark/presentation/repository/utils/pagination/RecognitionTasksMediator.kt @@ -9,7 +9,7 @@ import lab.maxb.dark.domain.model.RecognitionTask import lab.maxb.dark.presentation.repository.room.dao.RemoteKeysDAO import lab.maxb.dark.presentation.repository.room.model.RemoteKey import lab.maxb.dark.presentation.repository.room.relations.FullRecognitionTaskDTO -import lab.maxb.dark.presentation.repository.utils.BaseResource +import lab.maxb.dark.presentation.repository.utils.Resource import retrofit2.HttpException import java.io.IOException import java.io.InvalidObjectException @@ -17,11 +17,11 @@ import java.io.InvalidObjectException @OptIn(ExperimentalPagingApi::class) class RecognitionTaskMediator( - private val resource: BaseResource>, + private val resource: Resource>, private val remoteKeys: RemoteKeysDAO, ) : RemoteMediator() { - override suspend fun initialize() = if (resource.isFresh(Page(0, 1))) + override suspend fun initialize() = if (resource.checkIsFresh(Page(0, 1))) InitializeAction.SKIP_INITIAL_REFRESH else InitializeAction.LAUNCH_INITIAL_REFRESH diff --git a/app/src/main/java/lab/maxb/dark/presentation/viewModel/SolveRecognitionTaskViewModel.kt b/app/src/main/java/lab/maxb/dark/presentation/viewModel/SolveRecognitionTaskViewModel.kt index 6d7ffe0..3c60fcc 100644 --- a/app/src/main/java/lab/maxb/dark/presentation/viewModel/SolveRecognitionTaskViewModel.kt +++ b/app/src/main/java/lab/maxb/dark/presentation/viewModel/SolveRecognitionTaskViewModel.kt @@ -58,8 +58,8 @@ class SolveRecognitionTaskViewModel( it.id, answer.firstOrNull() ?: "" ).also { result -> if (result) { - usersRepository.getUser(profile.firstOrNull()!!.user!!.id, true).firstOrNull() - recognitionTasksRepository.getRecognitionTask(it.id, true).firstOrNull() + usersRepository.refresh(profile.firstOrNull()!!.user!!.id) + recognitionTasksRepository.refresh(it.id) } } } ?: false From f4b682f50746606194ce52573c82465248ba1feb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=91=D0=B0=D0=B1=D1=8B=D1=88=D0=B5=D0=B2=20=D0=9C=D0=B0?= =?UTF-8?q?=D0=BA=D1=81=D0=B8=D0=BC?= Date: Thu, 2 Jun 2022 17:54:31 +0300 Subject: [PATCH 19/22] Fix: Paging don't refresh on account changed --- .../implementation/RecognitionTasksRepositoryImpl.kt | 5 ++--- .../presentation/repository/room/dao/RemoteKeysDAO.kt | 6 +++++- .../utils/pagination/RecognitionTasksMediator.kt | 10 +++++++--- 3 files changed, 14 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/lab/maxb/dark/presentation/repository/implementation/RecognitionTasksRepositoryImpl.kt b/app/src/main/java/lab/maxb/dark/presentation/repository/implementation/RecognitionTasksRepositoryImpl.kt index 578f3cd..b4a24fc 100644 --- a/app/src/main/java/lab/maxb/dark/presentation/repository/implementation/RecognitionTasksRepositoryImpl.kt +++ b/app/src/main/java/lab/maxb/dark/presentation/repository/implementation/RecognitionTasksRepositoryImpl.kt @@ -45,9 +45,8 @@ class RecognitionTasksRepositoryImpl( it.toDomain { getUser(it.owner_id) } } } - isEmptyResponse = { - it.isNullOrEmpty() - } + isEmptyResponse = { it.isNullOrEmpty() } + isEmptyCache = { it.isNullOrEmpty() } localStore = { tasks -> tasks.map { it.toLocalDTO() diff --git a/app/src/main/java/lab/maxb/dark/presentation/repository/room/dao/RemoteKeysDAO.kt b/app/src/main/java/lab/maxb/dark/presentation/repository/room/dao/RemoteKeysDAO.kt index c52d09d..143bd00 100644 --- a/app/src/main/java/lab/maxb/dark/presentation/repository/room/dao/RemoteKeysDAO.kt +++ b/app/src/main/java/lab/maxb/dark/presentation/repository/room/dao/RemoteKeysDAO.kt @@ -1,7 +1,11 @@ package lab.maxb.dark.presentation.repository.room.dao import androidx.room.Dao +import androidx.room.Query import lab.maxb.dark.presentation.repository.room.model.RemoteKey @Dao -abstract class RemoteKeysDAO: AdvancedDAO("remote_keys") \ No newline at end of file +abstract class RemoteKeysDAO: AdvancedDAO("remote_keys") { + @Query("SELECT EXISTS(SELECT 1 FROM remote_keys)") + abstract suspend fun hasContent(): Boolean +} \ No newline at end of file diff --git a/app/src/main/java/lab/maxb/dark/presentation/repository/utils/pagination/RecognitionTasksMediator.kt b/app/src/main/java/lab/maxb/dark/presentation/repository/utils/pagination/RecognitionTasksMediator.kt index 684c9e3..4b7e902 100644 --- a/app/src/main/java/lab/maxb/dark/presentation/repository/utils/pagination/RecognitionTasksMediator.kt +++ b/app/src/main/java/lab/maxb/dark/presentation/repository/utils/pagination/RecognitionTasksMediator.kt @@ -21,7 +21,10 @@ class RecognitionTaskMediator( private val remoteKeys: RemoteKeysDAO, ) : RemoteMediator() { - override suspend fun initialize() = if (resource.checkIsFresh(Page(0, 1))) + override suspend fun initialize() = if ( + resource.checkIsFresh(Page(0, 1)) + && remoteKeys.hasContent() + ) InitializeAction.SKIP_INITIAL_REFRESH else InitializeAction.LAUNCH_INITIAL_REFRESH @@ -31,13 +34,14 @@ class RecognitionTaskMediator( state: PagingState ): MediatorResult { return try { - val pageKeyData = getKeyPageData(loadType, state) + val pageKeyData = getKeyPageData(if (remoteKeys.hasContent()) + loadType else LoadType.REFRESH, state) val page = when (pageKeyData) { is MediatorResult.Success -> return pageKeyData else -> pageKeyData as Int } val response = resource.query(Page(page, state.config.pageSize), true).firstOrNull() - val isEndOfList = response?.isEmpty() ?: true + val isEndOfList = response.isNullOrEmpty() if (loadType == LoadType.REFRESH) remoteKeys.clear() From ed9322e79a1eae0a8f515405d91688883ecaebe6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=91=D0=B0=D0=B1=D1=8B=D1=88=D0=B5=D0=B2=20=D0=9C=D0=B0?= =?UTF-8?q?=D0=BA=D1=81=D0=B8=D0=BC?= Date: Thu, 2 Jun 2022 18:32:58 +0300 Subject: [PATCH 20/22] + Reviewed tasks looks different for moderator --- .../repository/room/dao/RecognitionTasksDAO.kt | 4 ++-- .../dark/presentation/view/RecognitionTaskListFragment.kt | 4 +++- .../view/adapter/RecognitionTaskListAdapter.kt | 3 +++ .../viewModel/RecognitionTaskListViewModel.kt | 8 ++++++++ 4 files changed, 16 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/lab/maxb/dark/presentation/repository/room/dao/RecognitionTasksDAO.kt b/app/src/main/java/lab/maxb/dark/presentation/repository/room/dao/RecognitionTasksDAO.kt index bfbaf0d..2ba98eb 100644 --- a/app/src/main/java/lab/maxb/dark/presentation/repository/room/dao/RecognitionTasksDAO.kt +++ b/app/src/main/java/lab/maxb/dark/presentation/repository/room/dao/RecognitionTasksDAO.kt @@ -13,11 +13,11 @@ abstract class RecognitionTasksDAO: AdvancedDAO( "recognition_task" ) { @Transaction - @Query("SELECT * FROM recognition_task ORDER BY reviewed") + @Query("SELECT * FROM recognition_task ORDER BY reviewed ASC, id ASC") abstract fun getAll(): Flow?> @Transaction - @Query("SELECT * FROM recognition_task ORDER BY reviewed") + @Query("SELECT * FROM recognition_task ORDER BY reviewed ASC, id ASC") abstract fun getAllPaged(): PagingSource @Query("SELECT * FROM recognition_task WHERE id = :id") diff --git a/app/src/main/java/lab/maxb/dark/presentation/view/RecognitionTaskListFragment.kt b/app/src/main/java/lab/maxb/dark/presentation/view/RecognitionTaskListFragment.kt index ae5a105..b05ab3c 100644 --- a/app/src/main/java/lab/maxb/dark/presentation/view/RecognitionTaskListFragment.kt +++ b/app/src/main/java/lab/maxb/dark/presentation/view/RecognitionTaskListFragment.kt @@ -39,7 +39,9 @@ class RecognitionTaskListFragment : Fragment(R.layout.recognition_task_list_frag R.drawable.loading_vector ) as AnimatedVectorDrawable mPlaceholder.start() - mAdapter = RecognitionTaskListAdapter(GlideApp.with(this)) { + mAdapter = RecognitionTaskListAdapter(GlideApp.with(this), + !mViewModel.isTaskCreationAllowed.value + ) { load(mViewModel.getImage(it)) .transition(withCrossFade()) .placeholder(mPlaceholder) diff --git a/app/src/main/java/lab/maxb/dark/presentation/view/adapter/RecognitionTaskListAdapter.kt b/app/src/main/java/lab/maxb/dark/presentation/view/adapter/RecognitionTaskListAdapter.kt index c2362bf..ae71936 100644 --- a/app/src/main/java/lab/maxb/dark/presentation/view/adapter/RecognitionTaskListAdapter.kt +++ b/app/src/main/java/lab/maxb/dark/presentation/view/adapter/RecognitionTaskListAdapter.kt @@ -14,6 +14,7 @@ import lab.maxb.dark.presentation.view.adapter.RecognitionTaskListAdapter.TaskVi class RecognitionTaskListAdapter( private val manager: RequestManager, + private val accentNonReviewed: Boolean, private val getImageLoader: RequestManager.(String) -> RequestBuilder<*>, ): PagingDataAdapter(COMPARATOR) { @@ -52,6 +53,8 @@ class RecognitionTaskListAdapter( override fun onBindViewHolder(holder: TaskViewHolder, position: Int) = with(holder.binding) { val item = getItem(position) taskOwnerName.text = item?.owner?.name ?: "" + if (accentNonReviewed) + root.alpha = if (item?.reviewed != true) 1f else 0.75f item?.images?.firstOrNull()?.let { getImageLoader(manager, it).into(taskImage) } ?: manager.clear(taskImage) diff --git a/app/src/main/java/lab/maxb/dark/presentation/viewModel/RecognitionTaskListViewModel.kt b/app/src/main/java/lab/maxb/dark/presentation/viewModel/RecognitionTaskListViewModel.kt index 7eb7055..5fe6817 100644 --- a/app/src/main/java/lab/maxb/dark/presentation/viewModel/RecognitionTaskListViewModel.kt +++ b/app/src/main/java/lab/maxb/dark/presentation/viewModel/RecognitionTaskListViewModel.kt @@ -5,9 +5,11 @@ import androidx.lifecycle.viewModelScope import androidx.paging.cachedIn import androidx.paging.filter import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.buffer import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.flow.mapLatest import lab.maxb.dark.domain.model.isUser +import lab.maxb.dark.presentation.extra.launch import lab.maxb.dark.presentation.repository.interfaces.ProfileRepository import lab.maxb.dark.presentation.repository.interfaces.RecognitionTasksRepository import lab.maxb.dark.presentation.viewModel.utils.stateIn @@ -20,6 +22,12 @@ class RecognitionTaskListViewModel( ) : ViewModel() { private val profile = profileRepository.profileState + init { + launch { + profile.buffer() + } + } + @OptIn(ExperimentalCoroutinesApi::class) val recognitionTaskList = recognitionTasksRepository .getAllRecognitionTasks().mapLatest { page -> From 9e80a6341872311a60fdd86681618abfb9bfbb49 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=91=D0=B0=D0=B1=D1=8B=D1=88=D0=B5=D0=B2=20=D0=9C=D0=B0?= =?UTF-8?q?=D0=BA=D1=81=D0=B8=D0=BC?= Date: Thu, 2 Jun 2022 23:10:23 +0300 Subject: [PATCH 21/22] Fix: Images not updating on edition --- .../dark/presentation/view/adapter/StringHolderDiffchecker.kt | 4 ++-- .../presentation/viewModel/AddRecognitionTaskViewModel.kt | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/lab/maxb/dark/presentation/view/adapter/StringHolderDiffchecker.kt b/app/src/main/java/lab/maxb/dark/presentation/view/adapter/StringHolderDiffchecker.kt index 46ecf49..29d5560 100644 --- a/app/src/main/java/lab/maxb/dark/presentation/view/adapter/StringHolderDiffchecker.kt +++ b/app/src/main/java/lab/maxb/dark/presentation/view/adapter/StringHolderDiffchecker.kt @@ -6,8 +6,8 @@ import lab.maxb.dark.presentation.viewModel.utils.ItemHolder val stringHolderDiffCallback = object : DiffUtil.ItemCallback>() { override fun areItemsTheSame(oldItem: ItemHolder, newItem: ItemHolder) - = oldItem.id == newItem.id + = oldItem.id == newItem.id override fun areContentsTheSame(oldItem: ItemHolder, newItem: ItemHolder) - = oldItem.value == newItem.value + = oldItem.value == newItem.value }.let { AsyncDifferConfig.Builder(it).build() } diff --git a/app/src/main/java/lab/maxb/dark/presentation/viewModel/AddRecognitionTaskViewModel.kt b/app/src/main/java/lab/maxb/dark/presentation/viewModel/AddRecognitionTaskViewModel.kt index 39f27dd..0f1978f 100644 --- a/app/src/main/java/lab/maxb/dark/presentation/viewModel/AddRecognitionTaskViewModel.kt +++ b/app/src/main/java/lab/maxb/dark/presentation/viewModel/AddRecognitionTaskViewModel.kt @@ -16,6 +16,7 @@ import lab.maxb.dark.presentation.repository.interfaces.RecognitionTasksReposito import lab.maxb.dark.presentation.repository.interfaces.SynonymsRepository import lab.maxb.dark.presentation.viewModel.utils.ItemHolder import lab.maxb.dark.presentation.viewModel.utils.firstNotNull +import lab.maxb.dark.presentation.viewModel.utils.map import lab.maxb.dark.presentation.viewModel.utils.stateIn import org.koin.android.annotation.KoinViewModel @@ -114,7 +115,7 @@ class AddRecognitionTaskViewModel( fun updateImage(position: Int, uri: Uri) { uri.takePersistablePermission(getApplication()) - _imagesRaw[position].value = uri.toString() + _imagesRaw[position] = _imagesRaw[position].map(uri.toString()) updateImages() } } From 45d02df448e56bc545a97c094f7985c910463cb0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=91=D0=B0=D0=B1=D1=8B=D1=88=D0=B5=D0=B2=20=D0=9C=D0=B0?= =?UTF-8?q?=D0=BA=D1=81=D0=B8=D0=BC?= Date: Fri, 3 Jun 2022 00:31:07 +0300 Subject: [PATCH 22/22] Fix: Image holders updating on no changes performed --- .../dark/presentation/view/SolveRecognitionTaskFragment.kt | 3 ++- .../lab/maxb/dark/presentation/viewModel/utils/ItemHolder.kt | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/lab/maxb/dark/presentation/view/SolveRecognitionTaskFragment.kt b/app/src/main/java/lab/maxb/dark/presentation/view/SolveRecognitionTaskFragment.kt index 5744abe..5d76bfb 100644 --- a/app/src/main/java/lab/maxb/dark/presentation/view/SolveRecognitionTaskFragment.kt +++ b/app/src/main/java/lab/maxb/dark/presentation/view/SolveRecognitionTaskFragment.kt @@ -25,6 +25,7 @@ import lab.maxb.dark.presentation.view.adapter.ImageSliderAdapter import lab.maxb.dark.presentation.viewModel.SolveRecognitionTaskViewModel import lab.maxb.dark.presentation.viewModel.utils.ItemHolder import org.koin.androidx.viewmodel.ext.android.sharedViewModel +import java.util.* class SolveRecognitionTaskFragment : Fragment(R.layout.solve_recognition_task_fragment) { private val mViewModel: SolveRecognitionTaskViewModel by sharedViewModel() @@ -63,7 +64,7 @@ class SolveRecognitionTaskFragment : Fragment(R.layout.solve_recognition_task_fr } (task.images ?: listOf()).map { image -> - ItemHolder(image) + ItemHolder(image, image) }.run { mAdapter.submitList(this) } mViewModel.isReviewMode observe { diff --git a/app/src/main/java/lab/maxb/dark/presentation/viewModel/utils/ItemHolder.kt b/app/src/main/java/lab/maxb/dark/presentation/viewModel/utils/ItemHolder.kt index 2e2cf38..5b1def4 100644 --- a/app/src/main/java/lab/maxb/dark/presentation/viewModel/utils/ItemHolder.kt +++ b/app/src/main/java/lab/maxb/dark/presentation/viewModel/utils/ItemHolder.kt @@ -1,9 +1,9 @@ package lab.maxb.dark.presentation.viewModel.utils -import java.util.* +import lab.maxb.dark.domain.operations.randomUUID class ItemHolder( var value: T, - val id: UUID = UUID.randomUUID(), + val id: String = randomUUID, ) fun ItemHolder.map(value: R)