Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

bugfix/error_signing_redirect: Added error handling #221

Open
wants to merge 5 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package ro.code4.monitorizarevot.exceptions

object ErrorCodes {
const val UNAUTHORIZED = 401
const val NOT_FOUND = 404
const val BAD_REQUEST = 400
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
package ro.code4.monitorizarevot.exceptions

import okhttp3.ResponseBody
import retrofit2.Converter
import retrofit2.Response
import retrofit2.Retrofit
import java.io.IOException


/**
* Exception that is retrieved from retrofit. It is of three types, http, network and unexpected.
*/
class RetrofitException internal constructor(
message: String?,
/**
* RobResponse object containing status code, headers, body, etc.
*/
val response: Response<*>?,
/**
* The event kind which triggered this error.
*/
val kind: Kind,
val exception: Throwable?,
/**
* The Retrofit this request was executed on
*/
val retrofit: Retrofit?
) :
RuntimeException(message, exception) {
/**
* Identifies the event kind which triggered a [RetrofitException].
*/
enum class Kind {
/**
* An [IOException] occurred while communicating to the server.
*/
NETWORK,

/**
* A non-200 HTTP status code was received from the server.
*/
HTTP,

/**
* An internal error occurred while attempting to execute a request. It is best practice to
* re-throw this exception so your application crashes.
*/
UNEXPECTED
}

/**
* HTTP response body converted to specified `type`. `null` if there is no
* response.
*
* @param type
* @throws IOException if unable to convert the body to the specified `type`.
*/
@Throws(IOException::class)
fun <T> getErrorBodyAs(type: Class<*>?): T? {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No usages. Mostly API has one type for any errors, but it can be useful if the app's backend uses different types.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I added it for a reason. I know it is not used at the moment. I plan to take on other user stories that will depend on that.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we should keep this PR as clean as possible

if (response?.errorBody() == null) {
return null
}
val converter: Converter<ResponseBody?, T> =
retrofit!!.responseBodyConverter(type, arrayOfNulls(0))
return converter.convert(response.errorBody())
}

companion object {
fun httpError(
response: Response<*>,
retrofit: Retrofit?
): RetrofitException {
val message = response.code().toString() + " " + response.message()
return RetrofitException(
message,
response,
Kind.HTTP,
null,
retrofit
)
}

fun networkError(exception: IOException): RetrofitException {
return RetrofitException(
exception.message,
null,
Kind.NETWORK,
exception,
null
)
}

fun unexpectedError(exception: Throwable): RetrofitException {
return RetrofitException(
exception.message,
null,
Kind.UNEXPECTED,
exception,
null
)
}
}

}


Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package ro.code4.monitorizarevot.extensions

import retrofit2.HttpException
import retrofit2.Response

fun <T> Response<T>.successOrThrow(): Boolean {
if (!isSuccessful) throw HttpException(this)
return true
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
package ro.code4.monitorizarevot.extensions

import io.reactivex.*
import retrofit2.Call
import retrofit2.CallAdapter
import retrofit2.HttpException
import retrofit2.Retrofit
import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory
import ro.code4.monitorizarevot.exceptions.RetrofitException
import ro.code4.monitorizarevot.exceptions.RetrofitException.Companion.httpError
import ro.code4.monitorizarevot.exceptions.RetrofitException.Companion.networkError
import ro.code4.monitorizarevot.exceptions.RetrofitException.Companion.unexpectedError
import java.io.IOException
import java.lang.reflect.ParameterizedType
import java.lang.reflect.Type

/**
* Rxjava error handling CallAdapter factory. This class ensures the mapping of errors to
* one of the following exceptions: http, network or unexpected exceptions.
*/
class RxErrorHandlingCallAdapterFactory private constructor() : CallAdapter.Factory() {
private val original: RxJava2CallAdapterFactory = RxJava2CallAdapterFactory.create()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

move to the constructor parameter. What if we want to specify the default Scheduler?


override fun get(
returnType: Type, annotations: Array<Annotation>,
retrofit: Retrofit
): CallAdapter<*, *> {
return RxCallAdapterWrapper(
returnType,
retrofit,
(original.get(returnType, annotations, retrofit) as CallAdapter<Any, Any>)
)
}

internal inner class RxCallAdapterWrapper(
private val returnType: Type,
private val retrofit: Retrofit,
private val wrapped: CallAdapter<Any, Any>
) :
CallAdapter<Any, Any> {
override fun responseType(): Type {
return wrapped.responseType()
}

override fun adapt(call: Call<Any>): Any? {
val rawType = getRawType(returnType)

val isFlowable = rawType == Flowable::class.java
val isSingle = rawType == Single::class.java
val isMaybe = rawType == Maybe::class.java
val isCompletable = rawType == Completable::class.java
if (rawType != Observable::class.java && !isFlowable && !isSingle && !isMaybe) {
return null
}
if (returnType !is ParameterizedType) {
val name = if (isFlowable)
"Flowable"
else if (isSingle) "Single" else if (isMaybe) "Maybe" else "Observable"
throw IllegalStateException(
name
+ " return type must be parameterized"
+ " as "
+ name
+ "<Foo> or "
+ name
+ "<? extends Foo>"
)
}

if (isFlowable) {
return (wrapped.adapt(call) as Flowable<*>).onErrorResumeNext { throwable: Throwable ->
Flowable.error(asRetrofitException(throwable))
}
}
if (isSingle) {
return (wrapped.adapt(call) as Single<*>).onErrorResumeNext { throwable ->
Single.error(asRetrofitException(throwable))
}
}
if (isMaybe) {
return (wrapped.adapt(call) as Maybe<*>).onErrorResumeNext { throwable: Throwable ->
Maybe.error(asRetrofitException(throwable))
}
}
if (isCompletable) {
return (wrapped.adapt(call) as Completable).onErrorResumeNext { throwable ->
Completable.error(asRetrofitException(throwable))
}
}
return (wrapped.adapt(call) as Observable<*>).onErrorResumeNext { throwable: Throwable ->
Observable.error(asRetrofitException(throwable))
}
}

private fun asRetrofitException(throwable: Throwable): RetrofitException {
return when (throwable) {
is HttpException -> {
val response = throwable.response()
httpError(response!!, retrofit)
}
is IOException -> {
networkError(throwable)
}
else -> unexpectedError(throwable)
}
}
}

companion object {
const val TAG = "RxErrorHandlingCallAdapterFactory"

fun create(): CallAdapter.Factory {
return RxErrorHandlingCallAdapterFactory()
}
}
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package ro.code4.monitorizarevot.helper

import com.google.gson.annotations.Expose

data class ErrorResponse(@Expose var error: String)
42 changes: 37 additions & 5 deletions app/src/main/java/ro/code4/monitorizarevot/helper/Result.kt
Original file line number Diff line number Diff line change
@@ -1,13 +1,21 @@
package ro.code4.monitorizarevot.helper


/**
* .:.:.:. Created by @henrikhorbovyi on 13/10/19 .:.:.:.

* Class that encapsulates successful result with a value of type [T] or
* a failure with a [Throwable] exception.
*/
sealed class Result<out T> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Was it an attempt to reintroduce kotlin.Result<T>?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It was there before, I just added some things. Changing it to Kotlin.Result might take a bit more effort. I analyzed that already.

class Failure(val error: Throwable, val message: String = "") : Result<Nothing>()
class Success<out T>(val data: T? = null) : Result<T>()
data class Error(val exception: Throwable) : Result<Nothing>()
data class Success<out T>(val data: T? = null) : Result<T>()
object Loading : Result<Nothing>()

fun exceptionOrNull(): Throwable? =
when (this) {
is Error -> exception
else -> null
}

fun handle(
onSuccess: (T?) -> Unit = {},
Expand All @@ -16,8 +24,32 @@ sealed class Result<out T> {
) {
when (this) {
is Success -> onSuccess(data)
is Failure -> onFailure(error)
is Error -> onFailure(exception)
is Loading -> onLoading()
}
}
}

override fun toString(): String {
return when (this) {
is Success<*> -> "Success[data=$data]"
is Error -> "Error[exception=$exception]"
Loading -> "Loading"
}
}
}

val Result<*>.succeeded
get() = this is Result.Success && data != null

val Result<*>.error
get() = this is Result.Error

inline fun <R, T : R> Result<T>.getOrThrow(onFailure: (exception: Throwable) -> R): R {
return when (val exception = exceptionOrNull()) {
null -> data as T
else -> onFailure(exception)
}
}

val <T> Result<T>.data: T?
get() = (this as? Result.Success)?.data
36 changes: 36 additions & 0 deletions app/src/main/java/ro/code4/monitorizarevot/helper/Utils.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package ro.code4.monitorizarevot.helper
import android.app.Activity
import android.content.ActivityNotFoundException
import android.content.Context
import android.content.DialogInterface
import android.content.Intent
import android.graphics.Rect
import android.graphics.Typeface
Expand All @@ -14,13 +15,17 @@ import android.os.Bundle
import android.os.Environment
import android.provider.MediaStore
import android.text.*
import android.text.method.LinkMovementMethod
import android.text.style.ForegroundColorSpan
import android.text.style.StyleSpan
import android.text.util.Linkify
import android.view.MotionEvent
import android.view.View
import android.view.inputmethod.InputMethodManager
import android.widget.TextView
import android.widget.EditText
import androidx.annotation.IdRes
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
import androidx.core.content.FileProvider
Expand All @@ -45,6 +50,7 @@ import ro.code4.monitorizarevot.ui.section.PollingStationActivity
import java.io.File
import java.text.SimpleDateFormat
import java.util.*
import java.util.concurrent.TimeUnit


fun String.createMultipart(name: String): MultipartBody.Part {
Expand Down Expand Up @@ -447,6 +453,36 @@ fun Context.browse(url: String, newTask: Boolean = false): Boolean {
}
}

fun Activity.createAndShowDialog(
message: String,
callback:() -> Unit,
title: String = getString(R.string.error_generic)
): AlertDialog? {

val s = SpannableString(message)

//added a TextView
val tx1 = TextView(this)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's better to not create views in code

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just moved it so we can reuse it. I plan to change it in future versions.

tx1.text = s
tx1.autoLinkMask = Activity.RESULT_OK
tx1.movementMethod = LinkMovementMethod.getInstance()
val valueInPixels = resources.getDimension(R.dimen.big_margin).toInt()
tx1.setPadding(valueInPixels, valueInPixels, valueInPixels, valueInPixels)

Linkify.addLinks(s, Linkify.PHONE_NUMBERS)
val builder = AlertDialog.Builder(this)
return builder.setTitle(title)
.setCancelable(false)
.setPositiveButton(R.string.push_notification_ok)
{ p0, _ -> p0.dismiss() }
.setCancelable(false)
.setOnDismissListener {
callback()
}
.setView(tx1)
.show()
}

@Suppress("NOTHING_TO_INLINE")
internal inline fun FirebaseRemoteConfig?.getStringOrDefault(key: String, defaultValue: String) =
this?.getString(key).takeUnless {
Expand Down
Loading