Skip to content

Commit

Permalink
Merge pull request #64 from LivingWithHippos/arrow-either
Browse files Browse the repository at this point in the history
Manage api and network errors
  • Loading branch information
LivingWithHippos authored Oct 8, 2020
2 parents 7a45bb2 + ad25da8 commit 45f7cd6
Show file tree
Hide file tree
Showing 10 changed files with 182 additions and 78 deletions.
14 changes: 10 additions & 4 deletions app/app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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"

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()
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
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -102,4 +107,29 @@ open class BaseRepository {
}

}

suspend fun <T : Any> eitherApiResult(
call: suspend () -> Response<T>,
errorMessage: String
): Either<UnchainedNetworkException, T> {
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))
}
}
}
}
Original file line number Diff line number Diff line change
@@ -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<UnchainedNetworkException, DownloadItem> {

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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,16 @@ 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
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
Expand Down Expand Up @@ -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 {

Expand Down Expand Up @@ -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<String> =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -34,17 +35,16 @@ class NewDownloadViewModel @ViewModelInject constructor(
*/
val linkLiveData = MutableLiveData<Event<DownloadItem?>>()
val torrentLiveData = MutableLiveData<Event<UploadedTorrent?>>()
val apiErrorLiveData = MutableLiveData<Event<APIError?>>()
val networkExceptionLiveData = MutableLiveData<Event<UnchainedNetworkException>>()

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))
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
2 changes: 1 addition & 1 deletion app/app/src/main/res/layout/fragment_user_profile.xml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
2 changes: 2 additions & 0 deletions app/app/src/main/res/values-it/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -189,4 +189,6 @@
<string name="title_debugging">Debug</string>
<string name="update_regexps">Aggiorna riconoscimento link hosts</string>
<string name="updating_link_matcher">Aggiornamento dei link…</string>
<string name="unknown_error">Errore sconosciuto</string>
<string name="network_error">Errore di rete</string>
</resources>
Loading

0 comments on commit 45f7cd6

Please sign in to comment.