diff --git a/app/src/main/java/gq/kirmanak/mealient/data/auth/impl/AuthDataSourceImpl.kt b/app/src/main/java/gq/kirmanak/mealient/data/auth/impl/AuthDataSourceImpl.kt index b6606f6..0fe2951 100644 --- a/app/src/main/java/gq/kirmanak/mealient/data/auth/impl/AuthDataSourceImpl.kt +++ b/app/src/main/java/gq/kirmanak/mealient/data/auth/impl/AuthDataSourceImpl.kt @@ -2,11 +2,12 @@ package gq.kirmanak.mealient.data.auth.impl import gq.kirmanak.mealient.data.auth.AuthDataSource import gq.kirmanak.mealient.data.network.ErrorDetail -import gq.kirmanak.mealient.data.network.NetworkError.* +import gq.kirmanak.mealient.data.network.NetworkError.NotMealie +import gq.kirmanak.mealient.data.network.NetworkError.Unauthorized import gq.kirmanak.mealient.data.network.ServiceFactory import gq.kirmanak.mealient.extensions.decodeErrorBodyOrNull -import kotlinx.coroutines.CancellationException -import kotlinx.serialization.SerializationException +import gq.kirmanak.mealient.extensions.mapToNetworkError +import gq.kirmanak.mealient.extensions.runCatchingExceptCancel import kotlinx.serialization.json.Json import retrofit2.HttpException import retrofit2.Response @@ -22,12 +23,7 @@ class AuthDataSourceImpl @Inject constructor( override suspend fun authenticate(username: String, password: String): String { Timber.v("authenticate() called with: username = $username, password = $password") - val authService = try { - authServiceFactory.provideService() - } catch (e: Exception) { - Timber.e(e, "authenticate: can't create Retrofit service") - throw MalformedUrl(e) - } + val authService = authServiceFactory.provideService() val response = sendRequest(authService, username, password) val accessToken = parseToken(response) Timber.v("authenticate() returned: $accessToken") @@ -38,14 +34,11 @@ class AuthDataSourceImpl @Inject constructor( authService: AuthService, username: String, password: String - ): Response = try { + ): Response = runCatchingExceptCancel { authService.getToken(username, password) - } catch (e: Throwable) { - throw when (e) { - is CancellationException -> e - is SerializationException -> NotMealie(e) - else -> NoServerConnection(e) - } + }.getOrElse { + Timber.e(it, "sendRequest: can't request token") + throw it.mapToNetworkError() } private fun parseToken( diff --git a/app/src/main/java/gq/kirmanak/mealient/data/baseurl/VersionDataSourceImpl.kt b/app/src/main/java/gq/kirmanak/mealient/data/baseurl/VersionDataSourceImpl.kt index 6e25370..d779a03 100644 --- a/app/src/main/java/gq/kirmanak/mealient/data/baseurl/VersionDataSourceImpl.kt +++ b/app/src/main/java/gq/kirmanak/mealient/data/baseurl/VersionDataSourceImpl.kt @@ -1,10 +1,9 @@ package gq.kirmanak.mealient.data.baseurl -import gq.kirmanak.mealient.data.network.NetworkError import gq.kirmanak.mealient.data.network.ServiceFactory +import gq.kirmanak.mealient.extensions.mapToNetworkError +import gq.kirmanak.mealient.extensions.runCatchingExceptCancel import gq.kirmanak.mealient.extensions.versionInfo -import kotlinx.serialization.SerializationException -import retrofit2.HttpException import timber.log.Timber import javax.inject.Inject import javax.inject.Singleton @@ -17,21 +16,12 @@ class VersionDataSourceImpl @Inject constructor( override suspend fun getVersionInfo(baseUrl: String): VersionInfo { Timber.v("getVersionInfo() called with: baseUrl = $baseUrl") - val service = try { - serviceFactory.provideService(baseUrl) - } catch (e: Exception) { - Timber.e(e, "getVersionInfo: can't create service") - throw NetworkError.MalformedUrl(e) - } - - val response = try { + val service = serviceFactory.provideService(baseUrl) + val response = runCatchingExceptCancel { service.getVersion() - } catch (e: Exception) { - Timber.e(e, "getVersionInfo: can't request version") - when (e) { - is HttpException, is SerializationException -> throw NetworkError.NotMealie(e) - else -> throw NetworkError.NoServerConnection(e) - } + }.getOrElse { + Timber.e(it, "getVersionInfo: can't request version") + throw it.mapToNetworkError() } return response.versionInfo() diff --git a/app/src/main/java/gq/kirmanak/mealient/data/network/RetrofitServiceFactory.kt b/app/src/main/java/gq/kirmanak/mealient/data/network/RetrofitServiceFactory.kt index 7b99bac..7a7ea95 100644 --- a/app/src/main/java/gq/kirmanak/mealient/data/network/RetrofitServiceFactory.kt +++ b/app/src/main/java/gq/kirmanak/mealient/data/network/RetrofitServiceFactory.kt @@ -1,6 +1,7 @@ package gq.kirmanak.mealient.data.network import gq.kirmanak.mealient.data.baseurl.BaseURLStorage +import gq.kirmanak.mealient.extensions.runCatchingExceptCancel import timber.log.Timber inline fun RetrofitBuilder.createServiceFactory(baseURLStorage: BaseURLStorage) = @@ -14,10 +15,13 @@ class RetrofitServiceFactory( private val cache: MutableMap = mutableMapOf() - override suspend fun provideService(baseUrl: String?): T { + override suspend fun provideService(baseUrl: String?): T = runCatchingExceptCancel { Timber.v("provideService() called with: baseUrl = $baseUrl, class = ${serviceClass.simpleName}") val url = baseUrl ?: baseURLStorage.requireBaseURL() - return synchronized(cache) { cache[url] ?: createService(url, serviceClass) } + synchronized(cache) { cache[url] ?: createService(url, serviceClass) } + }.getOrElse { + Timber.e(it, "provideService: can't provide service for $baseUrl") + throw NetworkError.MalformedUrl(it) } private fun createService(url: String, serviceClass: Class): T { diff --git a/app/src/main/java/gq/kirmanak/mealient/data/recipes/impl/RecipeRepoImpl.kt b/app/src/main/java/gq/kirmanak/mealient/data/recipes/impl/RecipeRepoImpl.kt index d17bded..d0a199e 100644 --- a/app/src/main/java/gq/kirmanak/mealient/data/recipes/impl/RecipeRepoImpl.kt +++ b/app/src/main/java/gq/kirmanak/mealient/data/recipes/impl/RecipeRepoImpl.kt @@ -8,7 +8,7 @@ import gq.kirmanak.mealient.data.recipes.RecipeRepo import gq.kirmanak.mealient.data.recipes.db.RecipeStorage import gq.kirmanak.mealient.data.recipes.db.entity.RecipeSummaryEntity import gq.kirmanak.mealient.data.recipes.network.RecipeDataSource -import kotlinx.coroutines.CancellationException +import gq.kirmanak.mealient.extensions.runCatchingExceptCancel import timber.log.Timber import javax.inject.Inject import javax.inject.Singleton @@ -39,12 +39,10 @@ class RecipeRepoImpl @Inject constructor( override suspend fun loadRecipeInfo(recipeId: Long, recipeSlug: String): FullRecipeInfo { Timber.v("loadRecipeInfo() called with: recipeId = $recipeId, recipeSlug = $recipeSlug") - try { + runCatchingExceptCancel { storage.saveRecipeInfo(dataSource.requestRecipeInfo(recipeSlug)) - } catch (e: CancellationException) { - throw e - } catch (e: Throwable) { - Timber.e(e, "loadRecipeInfo: can't update full recipe info") + }.onFailure { + Timber.e(it, "loadRecipeInfo: can't update full recipe info") } return storage.queryRecipeInfo(recipeId) diff --git a/app/src/main/java/gq/kirmanak/mealient/data/recipes/impl/RecipesRemoteMediator.kt b/app/src/main/java/gq/kirmanak/mealient/data/recipes/impl/RecipesRemoteMediator.kt index 96cbfa4..a194206 100644 --- a/app/src/main/java/gq/kirmanak/mealient/data/recipes/impl/RecipesRemoteMediator.kt +++ b/app/src/main/java/gq/kirmanak/mealient/data/recipes/impl/RecipesRemoteMediator.kt @@ -7,7 +7,7 @@ import androidx.paging.LoadType.REFRESH import gq.kirmanak.mealient.data.recipes.db.RecipeStorage import gq.kirmanak.mealient.data.recipes.db.entity.RecipeSummaryEntity import gq.kirmanak.mealient.data.recipes.network.RecipeDataSource -import kotlinx.coroutines.CancellationException +import gq.kirmanak.mealient.extensions.runCatchingExceptCancel import timber.log.Timber import javax.inject.Inject import javax.inject.Singleton @@ -37,16 +37,14 @@ class RecipesRemoteMediator @Inject constructor( val start = if (loadType == REFRESH) 0 else lastRequestEnd val limit = if (loadType == REFRESH) state.config.initialLoadSize else state.config.pageSize - val count: Int = try { + val count: Int = runCatchingExceptCancel { val recipes = network.requestRecipes(start, limit) if (loadType == REFRESH) storage.refreshAll(recipes) else storage.saveRecipes(recipes) recipes.size - } catch (e: CancellationException) { - throw e - } catch (e: Throwable) { - Timber.e(e, "Can't load recipes") - return MediatorResult.Error(e) + }.getOrElse { + Timber.e(it, "load: can't load recipes") + return MediatorResult.Error(it) } // After something is inserted into DB the paging sources have to be invalidated diff --git a/app/src/main/java/gq/kirmanak/mealient/extensions/CoroutineExtensions.kt b/app/src/main/java/gq/kirmanak/mealient/extensions/CoroutineExtensions.kt new file mode 100644 index 0000000..c0b7379 --- /dev/null +++ b/app/src/main/java/gq/kirmanak/mealient/extensions/CoroutineExtensions.kt @@ -0,0 +1,15 @@ +package gq.kirmanak.mealient.extensions + +import kotlinx.coroutines.CancellationException + +/** + * Like [runCatching] but rethrows [CancellationException] to support + * cancellation of coroutines. + */ +inline fun runCatchingExceptCancel(block: () -> T): Result = try { + Result.success(block()) +} catch (e: CancellationException) { + throw e +} catch (e: Throwable) { + Result.failure(e) +} \ No newline at end of file diff --git a/app/src/main/java/gq/kirmanak/mealient/extensions/NetworkExtensions.kt b/app/src/main/java/gq/kirmanak/mealient/extensions/NetworkExtensions.kt index 3de9dd7..92bb769 100644 --- a/app/src/main/java/gq/kirmanak/mealient/extensions/NetworkExtensions.kt +++ b/app/src/main/java/gq/kirmanak/mealient/extensions/NetworkExtensions.kt @@ -1,8 +1,11 @@ package gq.kirmanak.mealient.extensions +import gq.kirmanak.mealient.data.network.NetworkError import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.SerializationException import kotlinx.serialization.json.Json import kotlinx.serialization.json.decodeFromStream +import retrofit2.HttpException import retrofit2.Response import timber.log.Timber import java.io.InputStream @@ -16,3 +19,7 @@ inline fun Json.decodeFromStreamOrNull(stream: InputStream): T? = .onFailure { Timber.e(it, "decodeFromStreamOrNull: can't decode") } .getOrNull() +fun Throwable.mapToNetworkError(): NetworkError = when (this) { + is HttpException, is SerializationException -> NetworkError.NotMealie(this) + else -> NetworkError.NoServerConnection(this) +} diff --git a/app/src/main/java/gq/kirmanak/mealient/ui/auth/AuthenticationViewModel.kt b/app/src/main/java/gq/kirmanak/mealient/ui/auth/AuthenticationViewModel.kt index d17b360..b411758 100644 --- a/app/src/main/java/gq/kirmanak/mealient/ui/auth/AuthenticationViewModel.kt +++ b/app/src/main/java/gq/kirmanak/mealient/ui/auth/AuthenticationViewModel.kt @@ -6,6 +6,7 @@ import androidx.lifecycle.asLiveData import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel import gq.kirmanak.mealient.data.auth.AuthRepo +import gq.kirmanak.mealient.extensions.runCatchingExceptCancel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.combine @@ -40,7 +41,7 @@ class AuthenticationViewModel @Inject constructor( } } - suspend fun authenticate(username: String, password: String): Result = runCatching { + suspend fun authenticate(username: String, password: String) = runCatchingExceptCancel { authRepo.authenticate(username, password) }.onFailure { Timber.e(it, "authenticate: can't authenticate") diff --git a/app/src/main/java/gq/kirmanak/mealient/ui/recipes/info/RecipeInfoViewModel.kt b/app/src/main/java/gq/kirmanak/mealient/ui/recipes/info/RecipeInfoViewModel.kt index 92fdec5..77a0a83 100644 --- a/app/src/main/java/gq/kirmanak/mealient/ui/recipes/info/RecipeInfoViewModel.kt +++ b/app/src/main/java/gq/kirmanak/mealient/ui/recipes/info/RecipeInfoViewModel.kt @@ -9,6 +9,7 @@ import dagger.hilt.android.lifecycle.HiltViewModel import gq.kirmanak.mealient.data.recipes.RecipeImageLoader import gq.kirmanak.mealient.data.recipes.RecipeRepo import gq.kirmanak.mealient.data.recipes.impl.FullRecipeInfo +import gq.kirmanak.mealient.extensions.runCatchingExceptCancel import kotlinx.coroutines.launch import timber.log.Timber import javax.inject.Inject @@ -41,7 +42,7 @@ constructor( recipeIngredientsAdapter.submitList(null) recipeInstructionsAdapter.submitList(null) viewModelScope.launch { - runCatching { recipeRepo.loadRecipeInfo(recipeId, recipeSlug) } + runCatchingExceptCancel { recipeRepo.loadRecipeInfo(recipeId, recipeSlug) } .onSuccess { Timber.d("loadRecipeInfo: received recipe info = $it") _recipeInfo.value = it diff --git a/app/src/test/java/gq/kirmanak/mealient/data/auth/impl/AuthDataSourceImplTest.kt b/app/src/test/java/gq/kirmanak/mealient/data/auth/impl/AuthDataSourceImplTest.kt index 2b6196e..5f414d4 100644 --- a/app/src/test/java/gq/kirmanak/mealient/data/auth/impl/AuthDataSourceImplTest.kt +++ b/app/src/test/java/gq/kirmanak/mealient/data/auth/impl/AuthDataSourceImplTest.kt @@ -71,7 +71,7 @@ class AuthDataSourceImplTest { @Test(expected = MalformedUrl::class) fun `when authenticate and provideService throws then MalformedUrl`() = runTest { - coEvery { authServiceFactory.provideService() } throws RuntimeException() + coEvery { authServiceFactory.provideService() } throws MalformedUrl(RuntimeException()) callAuthenticate() } diff --git a/app/src/test/java/gq/kirmanak/mealient/data/baseurl/VersionDataSourceImplTest.kt b/app/src/test/java/gq/kirmanak/mealient/data/baseurl/VersionDataSourceImplTest.kt index 122c1b0..eed1861 100644 --- a/app/src/test/java/gq/kirmanak/mealient/data/baseurl/VersionDataSourceImplTest.kt +++ b/app/src/test/java/gq/kirmanak/mealient/data/baseurl/VersionDataSourceImplTest.kt @@ -36,7 +36,9 @@ class VersionDataSourceImplTest { @Test(expected = NetworkError.MalformedUrl::class) fun `when getVersionInfo and provideService throws then MalformedUrl`() = runTest { - coEvery { versionServiceFactory.provideService(eq(TEST_BASE_URL)) } throws RuntimeException() + coEvery { + versionServiceFactory.provideService(eq(TEST_BASE_URL)) + } throws NetworkError.MalformedUrl(RuntimeException()) subject.getVersionInfo(TEST_BASE_URL) }