diff --git a/app/app/build.gradle b/app/app/build.gradle index 6449285c8..df2a0e2d3 100644 --- a/app/app/build.gradle +++ b/app/app/build.gradle @@ -128,24 +128,25 @@ dependencies { def nav_version = "2.3.0" def room_version = "2.2.5" def coroutines_version = "1.3.9" - def material_version = '1.3.0-alpha02' - def constraintlayout_version = '2.0.1' + def material_version = '1.3.0-alpha03' + def constraintlayout_version = '2.0.2' def preferences_version = '1.1.1' def retrofit_version = '2.9.0' def moshi_version = '1.11.0' def okhttp_version = '4.9.0' def lifecycle_version = "2.2.0" def hilt_viewmodel_version = '1.0.0-alpha02' - def fragment_version = '1.3.0-alpha08' + def fragment_version = '1.3.0-beta01' def glide_version="4.11.0" def swipe_version="1.1.0" def paging_version = '3.0.0-alpha07' def leakcanary_version = '2.4' + def arrow_version = "0.11.0" implementation fileTree(dir: "libs", include: ["*.jar"]) implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" - implementation 'androidx.core:core-ktx:1.3.1' + implementation 'androidx.core:core-ktx:1.3.2' implementation 'androidx.appcompat:appcompat:1.2.0' implementation "androidx.constraintlayout:constraintlayout:$constraintlayout_version" implementation "androidx.fragment:fragment-ktx:$fragment_version" @@ -213,6 +214,11 @@ dependencies { // paging implementation "androidx.paging:paging-runtime-ktx:$paging_version" + // arrow + implementation "io.arrow-kt:arrow-core:$arrow_version" + implementation "io.arrow-kt:arrow-syntax:$arrow_version" + kapt "io.arrow-kt:arrow-meta:$arrow_version" + // leak canary // debugImplementation "com.squareup.leakcanary:leakcanary-android:$leakcanary_version" diff --git a/app/app/src/main/java/com/github/livingwithhippos/unchained/data/model/APIError.kt b/app/app/src/main/java/com/github/livingwithhippos/unchained/data/model/APIError.kt index 795d359ca..b651970f9 100644 --- a/app/app/src/main/java/com/github/livingwithhippos/unchained/data/model/APIError.kt +++ b/app/app/src/main/java/com/github/livingwithhippos/unchained/data/model/APIError.kt @@ -12,9 +12,19 @@ data class APIError( val errorDetails: String?, @Json(name = "error_code") val errorCode: Int? -) +): UnchainedNetworkException -/** - * Manager the response error body from the retrofit calls. WIP. - */ -class APIException(val apiError: APIError) : Exception() \ No newline at end of file +data class EmptyBodyError( + val returnCode: Int +): UnchainedNetworkException + +data class NetworkError( + val error: Int, + val message: String +): UnchainedNetworkException + +data class ApiConversionError( + val error: Int +): UnchainedNetworkException + +interface UnchainedNetworkException \ No newline at end of file diff --git a/app/app/src/main/java/com/github/livingwithhippos/unchained/data/repositoy/BaseRepository.kt b/app/app/src/main/java/com/github/livingwithhippos/unchained/data/repositoy/BaseRepository.kt index 7cdaeb96c..072ad05a3 100644 --- a/app/app/src/main/java/com/github/livingwithhippos/unchained/data/repositoy/BaseRepository.kt +++ b/app/app/src/main/java/com/github/livingwithhippos/unchained/data/repositoy/BaseRepository.kt @@ -1,10 +1,15 @@ package com.github.livingwithhippos.unchained.data.repositoy import android.util.Log +import arrow.core.Either import com.github.livingwithhippos.unchained.BuildConfig import com.github.livingwithhippos.unchained.data.model.APIError +import com.github.livingwithhippos.unchained.data.model.ApiConversionError import com.github.livingwithhippos.unchained.data.model.CompleteNetworkResponse +import com.github.livingwithhippos.unchained.data.model.EmptyBodyError +import com.github.livingwithhippos.unchained.data.model.NetworkError import com.github.livingwithhippos.unchained.data.model.NetworkResponse +import com.github.livingwithhippos.unchained.data.model.UnchainedNetworkException import com.squareup.moshi.JsonAdapter import com.squareup.moshi.Moshi import okhttp3.ResponseBody @@ -102,4 +107,29 @@ open class BaseRepository { } } + + suspend fun eitherApiResult( + call: suspend () -> Response, + errorMessage: String + ): Either { + val response = call.invoke() + if (response.isSuccessful) { + val body = response.body() + return if (body != null) + Either.Right(body) + else + Either.Left(EmptyBodyError(response.code())) + } else { + try { + val error: APIError? = jsonAdapter.fromJson(response.errorBody()!!.string()) + return if (error!=null) + Either.Left(error) + else + Either.Left(ApiConversionError(-1)) + } catch (e: IOException) { + // todo: analyze error to return code + return Either.Left(NetworkError(-1, errorMessage)) + } + } + } } \ No newline at end of file diff --git a/app/app/src/main/java/com/github/livingwithhippos/unchained/data/repositoy/UnrestrictRepository.kt b/app/app/src/main/java/com/github/livingwithhippos/unchained/data/repositoy/UnrestrictRepository.kt index 1370a5c8c..633f49181 100644 --- a/app/app/src/main/java/com/github/livingwithhippos/unchained/data/repositoy/UnrestrictRepository.kt +++ b/app/app/src/main/java/com/github/livingwithhippos/unchained/data/repositoy/UnrestrictRepository.kt @@ -1,13 +1,38 @@ package com.github.livingwithhippos.unchained.data.repositoy +import arrow.core.Either import com.github.livingwithhippos.unchained.data.model.CompleteNetworkResponse import com.github.livingwithhippos.unchained.data.model.DownloadItem +import com.github.livingwithhippos.unchained.data.model.UnchainedNetworkException import com.github.livingwithhippos.unchained.data.remote.UnrestrictApiHelper import kotlinx.coroutines.delay import javax.inject.Inject class UnrestrictRepository @Inject constructor(private val unrestrictApiHelper: UnrestrictApiHelper) : BaseRepository() { + + suspend fun getEitherUnrestrictedLink( + token: String, + link: String, + password: String? = null, + remote: Int? = null + ): Either { + + val linkResponse = eitherApiResult( + call = { + unrestrictApiHelper.getUnrestrictedLink( + token = "Bearer $token", + link = link, + password = password, + remote = remote + ) + }, + errorMessage = "Error Fetching Unrestricted Link Info" + ) + + return linkResponse + } + suspend fun getUnrestrictedLink( token: String, link: String, diff --git a/app/app/src/main/java/com/github/livingwithhippos/unchained/newdownload/view/NewDownloadFragment.kt b/app/app/src/main/java/com/github/livingwithhippos/unchained/newdownload/view/NewDownloadFragment.kt index a3d66ad61..c0d9d30d9 100644 --- a/app/app/src/main/java/com/github/livingwithhippos/unchained/newdownload/view/NewDownloadFragment.kt +++ b/app/app/src/main/java/com/github/livingwithhippos/unchained/newdownload/view/NewDownloadFragment.kt @@ -25,6 +25,8 @@ import com.github.livingwithhippos.unchained.R import com.github.livingwithhippos.unchained.base.UnchainedFragment import com.github.livingwithhippos.unchained.data.model.APIError import com.github.livingwithhippos.unchained.data.model.AuthenticationState +import com.github.livingwithhippos.unchained.data.model.EmptyBodyError +import com.github.livingwithhippos.unchained.data.model.NetworkError import com.github.livingwithhippos.unchained.databinding.NewDownloadFragmentBinding import com.github.livingwithhippos.unchained.lists.view.ListsTabFragment import com.github.livingwithhippos.unchained.newdownload.viewmodel.NewDownloadViewModel @@ -32,6 +34,7 @@ import com.github.livingwithhippos.unchained.utilities.REMOTE_TRAFFIC_ON import com.github.livingwithhippos.unchained.utilities.SCHEME_HTTP import com.github.livingwithhippos.unchained.utilities.SCHEME_HTTPS import com.github.livingwithhippos.unchained.utilities.SCHEME_MAGNET +import com.github.livingwithhippos.unchained.utilities.extension.getApiErrorMessage import com.github.livingwithhippos.unchained.utilities.extension.getClipboardText import com.github.livingwithhippos.unchained.utilities.extension.isMagnet import com.github.livingwithhippos.unchained.utilities.extension.isWebUrl @@ -85,16 +88,6 @@ class NewDownloadFragment : UnchainedFragment(), NewDownloadListener { } }) - viewModel.apiErrorLiveData.observe(viewLifecycleOwner, { - it.getContentIfNotHandled()?.let { error -> - showErrorMessage(error) - // re enable buttons to let the user take other actions - //todo: this needs to be done also for other errors. maybe throw another error from the ViewModel - downloadBinding.bUnrestrict.isEnabled = true - downloadBinding.bLoadTorrent.isEnabled = true - } - }) - // add the unrestrict button listener downloadBinding.bUnrestrict.setOnClickListener { @@ -205,59 +198,54 @@ class NewDownloadFragment : UnchainedFragment(), NewDownloadListener { } }) - return downloadBinding.root - } + viewModel.networkExceptionLiveData.observe(viewLifecycleOwner, { e -> + val exception = e.getContentIfNotHandled() - private fun showErrorMessage(error: APIError) { - when (error.errorCode) { - -1 -> context?.showToast(R.string.internal_error) - 1 -> context?.showToast(R.string.missing_parameter) - 2 -> context?.showToast(R.string.bad_parameter_value) - 3 -> context?.showToast(R.string.unknown_method) - 4 -> context?.showToast(R.string.method_not_allowed) - // what is this error for? - 5 -> context?.showToast(R.string.slow_down) - 6 -> context?.showToast(R.string.resource_unreachable) - 7 -> context?.showToast(R.string.resource_not_found) - //todo: check these - 8 -> { - context?.showToast(R.string.bad_token) - activityViewModel.setUnauthenticated() - } - 9 -> context?.showToast(R.string.permission_denied) - 10 -> context?.showToast(R.string.tfa_needed) - 11 -> context?.showToast(R.string.tfa_pending) - 12 -> { - context?.showToast(R.string.invalid_login) - activityViewModel.setUnauthenticated() - } - 13 -> context?.showToast(R.string.invalid_password) - 14 -> { - context?.showToast(R.string.account_locked) - activityViewModel.setUnauthenticated() + // re-enable the buttons to allow the user to take new actions + downloadBinding.bUnrestrict.isEnabled = true + downloadBinding.bLoadTorrent.isEnabled = true + + when (exception) { + is APIError -> { + // error codes outside the known range will return unknown error + val errorCode = exception.errorCode ?: -2 + // manage the api error result + when (exception.errorCode) { + -1,1 -> context?.let { + it.showToast(it.getApiErrorMessage(errorCode)) + } + // since here we monitor new downloads, use a less generic, custom message + 2 -> context?.showToast(R.string.unsupported_hoster) + in 3..7 -> context?.let { + it.showToast(it.getApiErrorMessage(errorCode)) + } + in 8..15 -> { + context?.let { + it.showToast(it.getApiErrorMessage(errorCode)) + } + activityViewModel.setUnauthenticated() + } + else -> { + context?.let { + it.showToast(it.getApiErrorMessage(errorCode)) + } + } + } + } + is EmptyBodyError -> { + // call successful, fit to singular api case + } + is NetworkError -> { + // todo: alert the user according to the different network error + context?.showToast(R.string.network_error) + } + // already handled + null -> { + } } - 15 -> context?.showToast(R.string.account_not_activated) - 16 -> context?.showToast(R.string.unsupported_hoster) - 17 -> context?.showToast(R.string.hoster_in_maintenance) - 18 -> context?.showToast(R.string.hoster_limit_reached) - 19 -> context?.showToast(R.string.hoster_temporarily_unavailable) - 20 -> context?.showToast(R.string.hoster_not_available_for_free_users) - 21 -> context?.showToast(R.string.too_many_active_downloads) - 22 -> context?.showToast(R.string.ip_Address_not_allowed) - 23 -> context?.showToast(R.string.traffic_exhausted) - 24 -> context?.showToast(R.string.file_unavailable) - 25 -> context?.showToast(R.string.service_unavailable) - 26 -> context?.showToast(R.string.upload_too_big) - 27 -> context?.showToast(R.string.upload_error) - 28 -> context?.showToast(R.string.file_not_allowed) - 29 -> context?.showToast(R.string.torrent_too_big) - 30 -> context?.showToast(R.string.torrent_file_invalid) - 31 -> context?.showToast(R.string.action_already_done) - 32 -> context?.showToast(R.string.image_resolution_error) - 33 -> context?.showToast(R.string.torrent_already_active) - else -> context?.showToast(R.string.error_unrestricting_download) - } + }) + return downloadBinding.root } private val getTorrent: ActivityResultLauncher = diff --git a/app/app/src/main/java/com/github/livingwithhippos/unchained/newdownload/viewmodel/NewDownloadViewModel.kt b/app/app/src/main/java/com/github/livingwithhippos/unchained/newdownload/viewmodel/NewDownloadViewModel.kt index 7dfff709c..6e46df2fd 100644 --- a/app/app/src/main/java/com/github/livingwithhippos/unchained/newdownload/viewmodel/NewDownloadViewModel.kt +++ b/app/app/src/main/java/com/github/livingwithhippos/unchained/newdownload/viewmodel/NewDownloadViewModel.kt @@ -5,10 +5,11 @@ import androidx.hilt.lifecycle.ViewModelInject import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import arrow.core.Either import com.github.livingwithhippos.unchained.BuildConfig import com.github.livingwithhippos.unchained.data.model.APIError -import com.github.livingwithhippos.unchained.data.model.APIException import com.github.livingwithhippos.unchained.data.model.DownloadItem +import com.github.livingwithhippos.unchained.data.model.UnchainedNetworkException import com.github.livingwithhippos.unchained.data.model.UploadedTorrent import com.github.livingwithhippos.unchained.data.repositoy.CredentialsRepository import com.github.livingwithhippos.unchained.data.repositoy.TorrentsRepository @@ -34,17 +35,16 @@ class NewDownloadViewModel @ViewModelInject constructor( */ val linkLiveData = MutableLiveData>() val torrentLiveData = MutableLiveData>() - val apiErrorLiveData = MutableLiveData>() + val networkExceptionLiveData = MutableLiveData>() fun fetchUnrestrictedLink(link: String, password: String?, remote: Int? = null) { viewModelScope.launch { val token = getToken() - try { - val unrestrictedData = - unrestrictRepository.getUnrestrictedLink(token, link, password, remote) - linkLiveData.postValue(Event(unrestrictedData)) - } catch (e: APIException) { - apiErrorLiveData.postValue(Event(e.apiError)) + val response = + unrestrictRepository.getEitherUnrestrictedLink(token, link, password, remote) + when(response) { + is Either.Left -> networkExceptionLiveData.postValue(Event(response.a)) + is Either.Right -> linkLiveData.postValue(Event(response.b)) } } } diff --git a/app/app/src/main/java/com/github/livingwithhippos/unchained/utilities/extension/Extension.kt b/app/app/src/main/java/com/github/livingwithhippos/unchained/utilities/extension/Extension.kt index db031498c..441e597e8 100644 --- a/app/app/src/main/java/com/github/livingwithhippos/unchained/utilities/extension/Extension.kt +++ b/app/app/src/main/java/com/github/livingwithhippos/unchained/utilities/extension/Extension.kt @@ -170,4 +170,45 @@ fun AppCompatActivity.setCustomTheme(theme: String) { "original" -> setTheme(R.style.Theme_Unchained) "tropical_sunset" -> setTheme(R.style.Theme_Unchained_TropicalSunset) } +} + +fun Context.getApiErrorMessage(errorCode: Int): String { + return when (errorCode) { + -1 -> getString(R.string.internal_error) + 1 -> getString(R.string.missing_parameter) + 2 -> getString(R.string.bad_parameter_value) + 3 -> getString(R.string.unknown_method) + 4 -> getString(R.string.method_not_allowed) + // note: what is this error for? + 5 -> getString(R.string.slow_down) + 6 -> getString(R.string.resource_unreachable) + 7 -> getString(R.string.resource_not_found) + 8 -> getString(R.string.bad_token) + 9 -> getString(R.string.permission_denied) + 10 -> getString(R.string.tfa_needed) + 11 -> getString(R.string.tfa_pending) + 12 -> getString(R.string.invalid_login) + 13 -> getString(R.string.invalid_password) + 14 -> getString(R.string.account_locked) + 15 -> getString(R.string.account_not_activated) + 16 -> getString(R.string.unsupported_hoster) + 17 -> getString(R.string.hoster_in_maintenance) + 18 -> getString(R.string.hoster_limit_reached) + 19 -> getString(R.string.hoster_temporarily_unavailable) + 20 -> getString(R.string.hoster_not_available_for_free_users) + 21 -> getString(R.string.too_many_active_downloads) + 22 -> getString(R.string.ip_Address_not_allowed) + 23 -> getString(R.string.traffic_exhausted) + 24 -> getString(R.string.file_unavailable) + 25 -> getString(R.string.service_unavailable) + 26 -> getString(R.string.upload_too_big) + 27 -> getString(R.string.upload_error) + 28 -> getString(R.string.file_not_allowed) + 29 -> getString(R.string.torrent_too_big) + 30 -> getString(R.string.torrent_file_invalid) + 31 -> getString(R.string.action_already_done) + 32 -> getString(R.string.image_resolution_error) + 33 -> getString(R.string.torrent_already_active) + else -> getString(R.string.unknown_error) + } } \ No newline at end of file diff --git a/app/app/src/main/res/layout/fragment_user_profile.xml b/app/app/src/main/res/layout/fragment_user_profile.xml index 5ffc3da5d..dec991170 100644 --- a/app/app/src/main/res/layout/fragment_user_profile.xml +++ b/app/app/src/main/res/layout/fragment_user_profile.xml @@ -127,7 +127,7 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="20dp" - app:indicatorWidth="25dp" + app:indicatorSize="25dp" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/tvPremiumDays" app:layout_constraintBottom_toBottomOf="parent" diff --git a/app/app/src/main/res/values-it/strings.xml b/app/app/src/main/res/values-it/strings.xml index 033b76b70..62232a0e1 100644 --- a/app/app/src/main/res/values-it/strings.xml +++ b/app/app/src/main/res/values-it/strings.xml @@ -189,4 +189,6 @@ Debug Aggiorna riconoscimento link hosts Aggiornamento dei link… + Errore sconosciuto + Errore di rete \ No newline at end of file diff --git a/app/app/src/main/res/values/strings.xml b/app/app/src/main/res/values/strings.xml index 97452876e..717bd2698 100644 --- a/app/app/src/main/res/values/strings.xml +++ b/app/app/src/main/res/values/strings.xml @@ -405,4 +405,6 @@ Debug Update hosts link matcher Updating link matcher… + Unknown error + Network error \ No newline at end of file