-
-
Notifications
You must be signed in to change notification settings - Fork 29
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
base: develop
Are you sure you want to change the base?
Changes from all commits
11f508e
1855137
e9727af
86eee37
39b0a96
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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? { | ||
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() | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) |
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> { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Was it an attempt to reintroduce There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 = {}, | ||
|
@@ -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 |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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 | ||
|
@@ -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 | ||
|
@@ -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 { | ||
|
@@ -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) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think it's better to not create views in code There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 { | ||
|
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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