From d0992eaf54279ff5846f6cde3c3f27e9165e8b87 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kuba=20Szczodrzy=C5=84ski?= Date: Sun, 16 Feb 2020 22:50:06 +0100 Subject: [PATCH] [API] Implement error handling and exception catching in Szkolny API. --- .../pl/szczodrzynski/edziennik/Extensions.kt | 34 +++- .../edziennik/data/api/Errors.kt | 2 + .../edziennik/data/api/models/ApiError.kt | 12 ++ .../edziennik/data/api/models/Data.kt | 37 +--- .../edziennik/data/api/szkolny/SzkolnyApi.kt | 170 ++++++++++++++---- .../data/api/szkolny/SzkolnyApiException.kt | 9 + .../data/api/szkolny/SzkolnyService.kt | 6 +- .../edziennik/sync/UpdateWorker.kt | 16 +- .../ui/dialogs/event/EventDetailsDialog.kt | 20 +-- .../ui/dialogs/event/EventManualDialog.kt | 36 ++-- .../ui/modules/base/CrashActivity.kt | 26 ++- .../ui/modules/error/ErrorDetailsDialog.kt | 81 +++++++++ .../ui/modules/error/ErrorSnackbar.kt | 48 +---- .../ui/modules/feedback/FeedbackFragment.kt | 25 +-- .../modules/settings/SettingsNewFragment.java | 2 +- .../ui/modules/webpush/WebPushFragment.kt | 24 +-- 16 files changed, 352 insertions(+), 196 deletions(-) create mode 100644 app/src/main/java/pl/szczodrzynski/edziennik/data/api/szkolny/SzkolnyApiException.kt create mode 100644 app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/error/ErrorDetailsDialog.kt diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/Extensions.kt b/app/src/main/java/pl/szczodrzynski/edziennik/Extensions.kt index 5bb4cc9e..5f191b7f 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/Extensions.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/Extensions.kt @@ -49,15 +49,22 @@ import okhttp3.OkHttpClient import okhttp3.RequestBody import okhttp3.TlsVersion import okio.Buffer +import pl.szczodrzynski.edziennik.data.api.* +import pl.szczodrzynski.edziennik.data.api.szkolny.SzkolnyApiException +import pl.szczodrzynski.edziennik.data.api.szkolny.response.ApiResponse import pl.szczodrzynski.edziennik.data.db.entity.Notification import pl.szczodrzynski.edziennik.data.db.entity.Profile import pl.szczodrzynski.edziennik.data.db.entity.Teacher import pl.szczodrzynski.edziennik.data.db.entity.Team import pl.szczodrzynski.edziennik.network.TLSSocketFactory import pl.szczodrzynski.edziennik.utils.models.Time +import java.io.InterruptedIOException import java.io.PrintWriter import java.io.StringWriter import java.math.BigInteger +import java.net.ConnectException +import java.net.SocketTimeoutException +import java.net.UnknownHostException import java.nio.charset.Charset import java.security.KeyStore import java.security.MessageDigest @@ -67,6 +74,7 @@ import java.util.zip.CRC32 import javax.crypto.Mac import javax.crypto.spec.SecretKeySpec import javax.net.ssl.SSLContext +import javax.net.ssl.SSLException import javax.net.ssl.TrustManagerFactory import javax.net.ssl.X509TrustManager import kotlin.Pair @@ -1034,5 +1042,29 @@ fun CharSequence.containsAll(list: List, ignoreCase: Boolean = fal return true } -fun RadioButton.setOnSelectedListener(listener: (buttonView: CompoundButton) -> Unit) +inline fun RadioButton.setOnSelectedListener(crossinline listener: (buttonView: CompoundButton) -> Unit) = setOnCheckedChangeListener { buttonView, isChecked -> if (isChecked) listener(buttonView) } + +fun Response.toErrorCode() = when (this.code()) { + 400 -> ERROR_REQUEST_HTTP_400 + 401 -> ERROR_REQUEST_HTTP_401 + 403 -> ERROR_REQUEST_HTTP_403 + 404 -> ERROR_REQUEST_HTTP_404 + 405 -> ERROR_REQUEST_HTTP_405 + 410 -> ERROR_REQUEST_HTTP_410 + 424 -> ERROR_REQUEST_HTTP_424 + 500 -> ERROR_REQUEST_HTTP_500 + 503 -> ERROR_REQUEST_HTTP_503 + else -> null +} +fun Throwable.toErrorCode() = when (this) { + is UnknownHostException -> ERROR_REQUEST_FAILURE_HOSTNAME_NOT_FOUND + is SSLException -> ERROR_REQUEST_FAILURE_SSL_ERROR + is SocketTimeoutException -> ERROR_REQUEST_FAILURE_TIMEOUT + is InterruptedIOException, is ConnectException -> ERROR_REQUEST_FAILURE_NO_INTERNET + is SzkolnyApiException -> this.error?.toErrorCode() + else -> null +} +private fun ApiResponse.Error.toErrorCode() = when (this.code) { + else -> ERROR_API_EXCEPTION +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/Errors.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/Errors.kt index 8a59b937..7cb32555 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/Errors.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/Errors.kt @@ -32,6 +32,8 @@ const val CODE_LIBRUS_DISCONNECTED = 31 const val CODE_PROFILE_ARCHIVED = 30*/ const val ERROR_APP_CRASH = 1 +const val ERROR_EXCEPTION = 2 +const val ERROR_API_EXCEPTION = 3 const val ERROR_MESSAGE_NOT_SENT = 10 const val ERROR_REQUEST_FAILURE = 50 diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/models/ApiError.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/models/ApiError.kt index c6499226..d99e16a2 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/models/ApiError.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/models/ApiError.kt @@ -9,10 +9,20 @@ import com.google.gson.JsonObject import im.wangchao.mhttp.Request import im.wangchao.mhttp.Response import pl.szczodrzynski.edziennik.R +import pl.szczodrzynski.edziennik.data.api.ERROR_API_EXCEPTION +import pl.szczodrzynski.edziennik.data.api.ERROR_EXCEPTION +import pl.szczodrzynski.edziennik.data.api.szkolny.SzkolnyApiException import pl.szczodrzynski.edziennik.data.api.szkolny.request.ErrorReportRequest import pl.szczodrzynski.edziennik.stackTraceString +import pl.szczodrzynski.edziennik.toErrorCode class ApiError(val tag: String, var errorCode: Int) { + companion object { + fun fromThrowable(tag: String, throwable: Throwable) = + ApiError(tag, throwable.toErrorCode() ?: ERROR_EXCEPTION) + .withThrowable(throwable) + } + val id = System.currentTimeMillis() var profileId: Int? = null var throwable: Throwable? = null @@ -58,6 +68,8 @@ class ApiError(val tag: String, var errorCode: Int) { } fun getStringReason(context: Context): String { + if (errorCode == ERROR_API_EXCEPTION && throwable is SzkolnyApiException) + return throwable?.message.toString() return context.resources.getIdentifier("error_${errorCode}_reason", "string", context.packageName).let { if (it != 0) context.getString(it) diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/models/Data.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/models/Data.kt index ac726338..0292499f 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/models/Data.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/models/Data.kt @@ -6,21 +6,13 @@ import androidx.core.util.size import androidx.room.OnConflictStrategy import com.google.gson.JsonObject import im.wangchao.mhttp.Response -import pl.szczodrzynski.edziennik.App -import pl.szczodrzynski.edziennik.data.api.* +import pl.szczodrzynski.edziennik.* +import pl.szczodrzynski.edziennik.data.api.ERROR_REQUEST_FAILURE import pl.szczodrzynski.edziennik.data.api.interfaces.EndpointCallback import pl.szczodrzynski.edziennik.data.db.AppDb import pl.szczodrzynski.edziennik.data.db.entity.* -import pl.szczodrzynski.edziennik.singleOrNull -import pl.szczodrzynski.edziennik.toSparseArray import pl.szczodrzynski.edziennik.utils.Utils.d import pl.szczodrzynski.edziennik.utils.models.Date -import pl.szczodrzynski.edziennik.values -import java.io.InterruptedIOException -import java.net.ConnectException -import java.net.SocketTimeoutException -import java.net.UnknownHostException -import javax.net.ssl.SSLException abstract class Data(val app: App, val profile: Profile?, val loginStore: LoginStore) { companion object { @@ -347,27 +339,12 @@ abstract class Data(val app: App, val profile: Profile?, val loginStore: LoginSt } fun error(apiError: ApiError) { - apiError.errorCode = when (apiError.throwable) { - is UnknownHostException -> ERROR_REQUEST_FAILURE_HOSTNAME_NOT_FOUND - is SSLException -> ERROR_REQUEST_FAILURE_SSL_ERROR - is SocketTimeoutException -> ERROR_REQUEST_FAILURE_TIMEOUT - is InterruptedIOException, is ConnectException -> ERROR_REQUEST_FAILURE_NO_INTERNET - else -> + apiError.errorCode = apiError.throwable?.toErrorCode() ?: if (apiError.errorCode == ERROR_REQUEST_FAILURE) - when (apiError.response?.code()) { - 400 -> ERROR_REQUEST_HTTP_400 - 401 -> ERROR_REQUEST_HTTP_401 - 403 -> ERROR_REQUEST_HTTP_403 - 404 -> ERROR_REQUEST_HTTP_404 - 405 -> ERROR_REQUEST_HTTP_405 - 410 -> ERROR_REQUEST_HTTP_410 - 424 -> ERROR_REQUEST_HTTP_424 - 500 -> ERROR_REQUEST_HTTP_500 - 503 -> ERROR_REQUEST_HTTP_503 - else -> apiError.errorCode - } - else apiError.errorCode - } + apiError.response?.toErrorCode() ?: apiError.errorCode + else + apiError.errorCode + callback.onError(apiError) } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/szkolny/SzkolnyApi.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/szkolny/SzkolnyApi.kt index 7846aae4..13aaf807 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/szkolny/SzkolnyApi.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/szkolny/SzkolnyApi.kt @@ -5,10 +5,17 @@ package pl.szczodrzynski.edziennik.data.api.szkolny import android.os.Build +import androidx.appcompat.app.AppCompatActivity import com.google.gson.GsonBuilder +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.withContext import okhttp3.OkHttpClient import pl.szczodrzynski.edziennik.App import pl.szczodrzynski.edziennik.BuildConfig +import pl.szczodrzynski.edziennik.R +import pl.szczodrzynski.edziennik.data.api.models.ApiError import pl.szczodrzynski.edziennik.data.api.szkolny.adapter.DateAdapter import pl.szczodrzynski.edziennik.data.api.szkolny.adapter.TimeAdapter import pl.szczodrzynski.edziennik.data.api.szkolny.interceptor.SignatureInterceptor @@ -22,21 +29,33 @@ import pl.szczodrzynski.edziennik.data.db.entity.Notification import pl.szczodrzynski.edziennik.data.db.entity.Profile import pl.szczodrzynski.edziennik.data.db.full.EventFull import pl.szczodrzynski.edziennik.md5 +import pl.szczodrzynski.edziennik.ui.modules.error.ErrorDetailsDialog +import pl.szczodrzynski.edziennik.ui.modules.error.ErrorSnackbar import pl.szczodrzynski.edziennik.utils.models.Date import pl.szczodrzynski.edziennik.utils.models.Time +import retrofit2.Response import retrofit2.Retrofit import retrofit2.converter.gson.GsonConverterFactory import retrofit2.create import java.util.concurrent.TimeUnit.SECONDS +import kotlin.coroutines.CoroutineContext -class SzkolnyApi(val app: App) { +class SzkolnyApi(val app: App) : CoroutineScope { + companion object { + const val TAG = "SzkolnyApi" + } private val api: SzkolnyService + private val retrofit: Retrofit + + private val job = Job() + override val coroutineContext: CoroutineContext + get() = job + Dispatchers.Main init { val okHttpClient: OkHttpClient = app.http.newBuilder() .followRedirects(true) - .callTimeout(30, SECONDS) + .callTimeout(10, SECONDS) .addInterceptor(SignatureInterceptor(app)) .build() @@ -47,7 +66,7 @@ class SzkolnyApi(val app: App) { .registerTypeAdapter(Time::class.java, TimeAdapter()) .create()) - val retrofit: Retrofit = Retrofit.Builder() + retrofit = Retrofit.Builder() .baseUrl("https://api.szkolny.eu/") .addConverterFactory(gsonConverterFactory) .client(okHttpClient) @@ -56,6 +75,68 @@ class SzkolnyApi(val app: App) { api = retrofit.create() } + suspend inline fun runCatching(errorSnackbar: ErrorSnackbar, crossinline block: SzkolnyApi.() -> T?): T? { + return try { + withContext(Dispatchers.Default) { block() } + } + catch (e: Exception) { + errorSnackbar.addError(ApiError.fromThrowable(TAG, e)).show() + null + } + } + suspend inline fun runCatching(activity: AppCompatActivity, crossinline block: SzkolnyApi.() -> T?): T? { + return try { + withContext(Dispatchers.Default) { block() } + } + catch (e: Exception) { + ErrorDetailsDialog( + activity, + listOf(ApiError.fromThrowable(TAG, e)), + R.string.error_occured + ) + null + } + } + inline fun runCatching(block: SzkolnyApi.() -> T, onError: (e: Throwable) -> Unit): T? { + return try { + block() + } + catch (e: Exception) { + onError(e) + null + } + } + + /** + * Check if a server request returned a successful response. + * + * If not, throw a [SzkolnyApiException] containing an [ApiResponse.Error], + * or null if it's a HTTP call error. + */ + @Throws(Exception::class) + private inline fun parseResponse(response: Response>): T { + if (response.isSuccessful && response.body()?.success == true) { + if (Unit is T) { + return Unit + } + if (response.body()?.data != null) { + return response.body()?.data!! + } + } + + val body = response.body() ?: response.errorBody()?.let { + try { + retrofit.responseBodyConverter>(ApiResponse::class.java, arrayOf()).convert(it) + } + catch (e: Exception) { + null + } + } + + throw SzkolnyApiException(body?.errors?.firstOrNull()) + } + + @Throws(Exception::class) private fun getDevice() = run { val config = app.config val device = Device( @@ -78,6 +159,7 @@ class SzkolnyApi(val app: App) { } } + @Throws(Exception::class) fun getEvents(profiles: List, notifications: List, blacklistedIds: List): List { val teams = app.db.teamDao().allNow @@ -104,11 +186,12 @@ class SzkolnyApi(val app: App) { } }, notifications = notifications.map { ServerSyncRequest.Notification(it.profileName ?: "", it.type, it.text) } - )).execute().body() + )).execute() + parseResponse(response) val events = mutableListOf() - response?.data?.events?.forEach { event -> + response.body()?.data?.events?.forEach { event -> if (event.id in blacklistedIds) return@forEach teams.filter { it.code == event.teamCode }.onEach { team -> @@ -129,100 +212,117 @@ class SzkolnyApi(val app: App) { return events } - fun shareEvent(event: EventFull): ApiResponse? { + @Throws(Exception::class) + fun shareEvent(event: EventFull) { val team = app.db.teamDao().getByIdNow(event.profileId, event.teamId) - return api.shareEvent(EventShareRequest( + val response = api.shareEvent(EventShareRequest( deviceId = app.deviceId, device = getDevice(), sharedByName = event.sharedByName, shareTeamCode = team.code, event = event - )).execute().body() + )).execute() + parseResponse(response) } - fun unshareEvent(event: Event): ApiResponse? { + @Throws(Exception::class) + fun unshareEvent(event: Event) { val team = app.db.teamDao().getByIdNow(event.profileId, event.teamId) - return api.shareEvent(EventShareRequest( + val response = api.shareEvent(EventShareRequest( deviceId = app.deviceId, device = getDevice(), sharedByName = event.sharedByName, unshareTeamCode = team.code, eventId = event.id - )).execute().body() + )).execute() + parseResponse(response) } /*fun eventEditRequest(requesterName: String, event: Event): ApiResponse? { }*/ - fun pairBrowser(browserId: String?, pairToken: String?, onError: ((List) -> Unit)? = null): List { + @Throws(Exception::class) + fun pairBrowser(browserId: String?, pairToken: String?): List { val response = api.webPush(WebPushRequest( deviceId = app.deviceId, device = getDevice(), action = "pairBrowser", browserId = browserId, pairToken = pairToken - )).execute().body() + )).execute() + parseResponse(response) - response?.errors?.let { - onError?.invoke(it) - return emptyList() - } - - return response?.data?.browsers ?: emptyList() + return response.body()?.data?.browsers ?: emptyList() } - fun listBrowsers(onError: ((List) -> Unit)? = null): List { + @Throws(Exception::class) + fun listBrowsers(): List { val response = api.webPush(WebPushRequest( deviceId = app.deviceId, device = getDevice(), action = "listBrowsers" - )).execute().body() + )).execute() + parseResponse(response) - return response?.data?.browsers ?: emptyList() + return response.body()?.data?.browsers ?: emptyList() } + @Throws(Exception::class) fun unpairBrowser(browserId: String): List { val response = api.webPush(WebPushRequest( deviceId = app.deviceId, device = getDevice(), action = "unpairBrowser", browserId = browserId - )).execute().body() + )).execute() + parseResponse(response) - return response?.data?.browsers ?: emptyList() + return response.body()?.data?.browsers ?: emptyList() } - fun errorReport(errors: List): ApiResponse? { - return api.errorReport(ErrorReportRequest( + @Throws(Exception::class) + fun errorReport(errors: List) { + val response = api.errorReport(ErrorReportRequest( deviceId = app.deviceId, device = getDevice(), appVersion = BuildConfig.VERSION_NAME, errors = errors - )).execute().body() + )).execute() + parseResponse(response) } - fun unregisterAppUser(userCode: String): ApiResponse? { - return api.appUser(AppUserRequest( + @Throws(Exception::class) + fun unregisterAppUser(userCode: String) { + val response = api.appUser(AppUserRequest( deviceId = app.deviceId, device = getDevice(), userCode = userCode - )).execute().body() + )).execute() + parseResponse(response) } - fun getUpdate(channel: String): ApiResponse>? { - return api.updates(channel).execute().body() + @Throws(Exception::class) + fun getUpdate(channel: String): List { + val response = api.updates(channel).execute() + parseResponse(response) + + return response.body()?.data ?: emptyList() } - fun sendFeedbackMessage(senderName: String?, targetDeviceId: String?, text: String): FeedbackMessage? { - return api.feedbackMessage(FeedbackMessageRequest( + @Throws(Exception::class) + fun sendFeedbackMessage(senderName: String?, targetDeviceId: String?, text: String): FeedbackMessage { + val response = api.feedbackMessage(FeedbackMessageRequest( deviceId = app.deviceId, device = getDevice(), senderName = senderName, targetDeviceId = targetDeviceId, text = text - )).execute().body()?.data?.message + )).execute() + val data = parseResponse(response) + + return data.message } } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/szkolny/SzkolnyApiException.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/szkolny/SzkolnyApiException.kt new file mode 100644 index 00000000..7e15873d --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/szkolny/SzkolnyApiException.kt @@ -0,0 +1,9 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2020-2-16. + */ + +package pl.szczodrzynski.edziennik.data.api.szkolny + +import pl.szczodrzynski.edziennik.data.api.szkolny.response.ApiResponse + +class SzkolnyApiException(val error: ApiResponse.Error?) : Exception(if (error == null) "Error body does not contain a valid Error." else "${error.code}: ${error.reason}") diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/szkolny/SzkolnyService.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/szkolny/SzkolnyService.kt index 4b5bda6f..0e3dd574 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/szkolny/SzkolnyService.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/szkolny/SzkolnyService.kt @@ -18,16 +18,16 @@ interface SzkolnyService { fun serverSync(@Body request: ServerSyncRequest): Call> @POST("share") - fun shareEvent(@Body request: EventShareRequest): Call> + fun shareEvent(@Body request: EventShareRequest): Call> @POST("webPush") fun webPush(@Body request: WebPushRequest): Call> @POST("errorReport") - fun errorReport(@Body request: ErrorReportRequest): Call> + fun errorReport(@Body request: ErrorReportRequest): Call> @POST("appUser") - fun appUser(@Body request: AppUserRequest): Call> + fun appUser(@Body request: AppUserRequest): Call> @GET("updates/app") fun updates(@Query("channel") channel: String = "release"): Call>> diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/sync/UpdateWorker.kt b/app/src/main/java/pl/szczodrzynski/edziennik/sync/UpdateWorker.kt index 515c1703..377ab806 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/sync/UpdateWorker.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/sync/UpdateWorker.kt @@ -76,13 +76,15 @@ class UpdateWorker(val context: Context, val params: WorkerParameters) : Worker( try { val update = overrideUpdate ?: run { - val response = withContext(Dispatchers.Default) { SzkolnyApi(app).getUpdate("beta") } - if (response?.success != true) { - Toast.makeText(app, app.getString(R.string.notification_cant_check_update), Toast.LENGTH_SHORT).show() - return@run null - } - val updates = response.data - if (updates?.isNotEmpty() != true) { + val updates = withContext(Dispatchers.Default) { + SzkolnyApi(app).runCatching({ + getUpdate("beta") + }, { + Toast.makeText(app, app.getString(R.string.notification_cant_check_update), Toast.LENGTH_SHORT).show() + }) + } ?: return@run null + + if (updates.isEmpty()) { app.config.update = null Toast.makeText(app, app.getString(R.string.notification_no_update), Toast.LENGTH_SHORT).show() return@run null diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/event/EventDetailsDialog.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/event/EventDetailsDialog.kt index 3d0da8bc..b2c04308 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/event/EventDetailsDialog.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/event/EventDetailsDialog.kt @@ -150,23 +150,21 @@ class EventDetailsDialog( private fun removeEvent() { launch { if (eventShared && eventOwn) { - Toast.makeText(activity, "Unshare + remove own event", Toast.LENGTH_SHORT).show() + // unshare + remove own event + Toast.makeText(activity, R.string.event_manual_unshare_remove, Toast.LENGTH_SHORT).show() - val response = withContext(Dispatchers.Default) { - api.unshareEvent(event) - } - - response?.errors?.ifNotEmpty { - Toast.makeText(activity, "Error: "+it[0].reason, Toast.LENGTH_SHORT).show() - return@launch - } + api.runCatching(activity) { + unshareEvent(event) + } ?: return@launch finishRemoving() } else if (eventShared && !eventOwn) { - Toast.makeText(activity, "Remove + blacklist somebody's event", Toast.LENGTH_SHORT).show() + // remove + blacklist somebody's event + Toast.makeText(activity, "Nie zaimplementowana opcja :(", Toast.LENGTH_SHORT).show() // TODO } else { - Toast.makeText(activity, "Remove event", Toast.LENGTH_SHORT).show() + // remove event + Toast.makeText(activity, R.string.event_manual_remove, Toast.LENGTH_SHORT).show() finishRemoving() } } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/event/EventManualDialog.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/event/EventManualDialog.kt index 318fd8f9..20e03b75 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/event/EventManualDialog.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/event/EventManualDialog.kt @@ -621,14 +621,9 @@ class EventManualDialog( sharedByName = profile?.studentNameLong } - val response = withContext(Dispatchers.Default) { - api.unshareEvent(eventObject) - } - - response?.errors?.ifNotEmpty { - Toast.makeText(activity, "Error: "+it[0].reason, Toast.LENGTH_SHORT).show() - return@launch - } + api.runCatching(activity) { + unshareEvent(eventObject) + } ?: return@launch eventObject.sharedByName = null finishAdding(eventObject, metadataObject) @@ -643,14 +638,9 @@ class EventManualDialog( metadataObject.addedDate = System.currentTimeMillis() - val response = withContext(Dispatchers.Default) { - api.shareEvent(eventObject.withMetadata(metadataObject)) - } - - response?.errors?.ifNotEmpty { - Toast.makeText(activity, "Error: "+it[0].reason, Toast.LENGTH_SHORT).show() - return@launch - } + api.runCatching(activity) { + shareEvent(eventObject.withMetadata(metadataObject)) + } ?: return@launch eventObject.sharedBy = "self" finishAdding(eventObject, metadataObject) @@ -664,22 +654,20 @@ class EventManualDialog( private fun removeEvent() { launch { if (editingShared && editingOwn) { + // unshare + remove own event Toast.makeText(activity, R.string.event_manual_unshare_remove, Toast.LENGTH_SHORT).show() - val response = withContext(Dispatchers.Default) { - api.unshareEvent(editingEvent!!) - } - - response?.errors?.ifNotEmpty { - Toast.makeText(activity, "Error: "+it[0].reason, Toast.LENGTH_SHORT).show() - return@launch - } + api.runCatching(activity) { + unshareEvent(editingEvent!!) + } ?: return@launch finishRemoving() } else if (editingShared && !editingOwn) { + // remove + blacklist somebody's event Toast.makeText(activity, "Nie zaimplementowana opcja :(", Toast.LENGTH_SHORT).show() // TODO } else { + // remove event Toast.makeText(activity, R.string.event_manual_remove, Toast.LENGTH_SHORT).show() finishRemoving() } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/base/CrashActivity.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/base/CrashActivity.kt index bb091eb6..3ae649f6 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/base/CrashActivity.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/base/CrashActivity.kt @@ -21,7 +21,6 @@ import pl.szczodrzynski.edziennik.data.api.ERROR_APP_CRASH import pl.szczodrzynski.edziennik.data.api.szkolny.SzkolnyApi import pl.szczodrzynski.edziennik.data.api.szkolny.request.ErrorReportRequest import pl.szczodrzynski.edziennik.data.db.entity.Profile -import pl.szczodrzynski.edziennik.ifNotEmpty import pl.szczodrzynski.edziennik.utils.Themes.appTheme import kotlin.coroutines.CoroutineContext @@ -86,22 +85,17 @@ class CrashActivity : AppCompatActivity(), CoroutineScope { .show() } else { launch { - val response = withContext(Dispatchers.Default) { - api.errorReport(listOf(getReportableError(intent))) - } + api.runCatching({ + withContext(Dispatchers.Default) { + errorReport(listOf(getReportableError(intent))) + } + }, { + Toast.makeText(app, getString(R.string.crash_report_cannot_send) + it, Toast.LENGTH_LONG).show() + }) ?: return@launch - response?.errors?.ifNotEmpty { - Toast.makeText(app, getString(R.string.crash_report_cannot_send) + ": " + it[0].reason, Toast.LENGTH_LONG).show() - return@launch - } - - if (response != null) { - Toast.makeText(app, getString(R.string.crash_report_sent), Toast.LENGTH_SHORT).show() - reportButton.isEnabled = false - reportButton.setTextColor(resources.getColor(android.R.color.darker_gray)) - } else { - Toast.makeText(app, getString(R.string.crash_report_cannot_send) + " JsonObject equals null", Toast.LENGTH_LONG).show() - } + Toast.makeText(app, getString(R.string.crash_report_sent), Toast.LENGTH_SHORT).show() + reportButton.isEnabled = false + reportButton.setTextColor(resources.getColor(android.R.color.darker_gray)) } } } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/error/ErrorDetailsDialog.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/error/ErrorDetailsDialog.kt new file mode 100644 index 00000000..32ec46b9 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/error/ErrorDetailsDialog.kt @@ -0,0 +1,81 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2020-2-16. + */ + +package pl.szczodrzynski.edziennik.ui.modules.error + +import android.widget.Toast +import androidx.appcompat.app.AlertDialog +import androidx.appcompat.app.AppCompatActivity +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import kotlinx.coroutines.* +import pl.szczodrzynski.edziennik.* +import pl.szczodrzynski.edziennik.data.api.models.ApiError +import pl.szczodrzynski.edziennik.data.api.szkolny.SzkolnyApi +import kotlin.coroutines.CoroutineContext + +class ErrorDetailsDialog( + val activity: AppCompatActivity, + val errors: List, + val titleRes: Int = R.string.dialog_error_details_title, + val onShowListener: ((tag: String) -> Unit)? = null, + val onDismissListener: ((tag: String) -> Unit)? = null +) : CoroutineScope { + companion object { + private const val TAG = "ApiErrorDialog" + } + + private lateinit var app: App + private lateinit var dialog: AlertDialog + + private val job = Job() + override val coroutineContext: CoroutineContext + get() = job + Dispatchers.Main + + private val api by lazy { SzkolnyApi(activity.applicationContext as App) } + + init { run { + if (activity.isFinishing) + return@run + onShowListener?.invoke(TAG) + app = activity.applicationContext as App + + if (errors.isNotEmpty()) { + val message = errors.map { + listOf( + it.getStringReason(activity).asBoldSpannable().asColoredSpannable(R.attr.colorOnBackground.resolveAttr(activity)), + activity.getString(R.string.error_unknown_format, it.errorCode, it.tag), + if (App.devMode) + it.throwable?.stackTraceString ?: it.throwable?.localizedMessage + else + it.throwable?.localizedMessage + ).concat("\n") + }.concat("\n\n") + + dialog = MaterialAlertDialogBuilder(activity) + .setTitle(titleRes) + .setMessage(message) + .setPositiveButton(R.string.ok) { _, _ -> + dialog.dismiss() + } + .setNeutralButton(R.string.report) { _, _ -> + launch { + api.runCatching({ + withContext(Dispatchers.Default) { + errorReport(errors.map { it.toReportableError(activity) }) + } + }, { + Toast.makeText(activity, activity.getString(R.string.crash_report_cannot_send) + it, Toast.LENGTH_LONG).show() + }) ?: return@launch + + dialog.dismiss() + } + } + .setCancelable(false) + .setOnDismissListener { + onDismissListener?.invoke(TAG) + } + .show() + } + }} +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/error/ErrorSnackbar.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/error/ErrorSnackbar.kt index d74a91e8..b43e88df 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/error/ErrorSnackbar.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/error/ErrorSnackbar.kt @@ -5,16 +5,15 @@ package pl.szczodrzynski.edziennik.ui.modules.error import android.view.View -import android.widget.Toast import androidx.appcompat.app.AppCompatActivity import androidx.coordinatorlayout.widget.CoordinatorLayout import androidx.core.graphics.ColorUtils -import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.snackbar.Snackbar -import kotlinx.coroutines.* -import pl.szczodrzynski.edziennik.* +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import pl.szczodrzynski.edziennik.R import pl.szczodrzynski.edziennik.data.api.models.ApiError -import pl.szczodrzynski.edziennik.data.api.szkolny.SzkolnyApi import pl.szczodrzynski.navlib.getColorFromAttr import kotlin.coroutines.CoroutineContext @@ -31,47 +30,12 @@ class ErrorSnackbar(val activity: AppCompatActivity) : CoroutineScope { override val coroutineContext: CoroutineContext get() = job + Dispatchers.Main - private val api by lazy { SzkolnyApi(activity.applicationContext as App) } - fun setCoordinator(coordinatorLayout: CoordinatorLayout, showAbove: View? = null) { this.coordinator = coordinatorLayout snackbar = Snackbar.make(coordinator, R.string.snackbar_error_text, Snackbar.LENGTH_INDEFINITE) snackbar?.setAction(R.string.more) { - if (errors.isNotEmpty()) { - val message = errors.map { - listOf( - it.getStringReason(activity).asBoldSpannable().asColoredSpannable(R.attr.colorOnBackground.resolveAttr(activity)), - activity.getString(R.string.error_unknown_format, it.errorCode, it.tag), - if (App.devMode) - it.throwable?.stackTraceString ?: it.throwable?.localizedMessage - else - it.throwable?.localizedMessage - ).concat("\n") - }.concat("\n\n") - - MaterialAlertDialogBuilder(activity) - .setTitle(R.string.dialog_error_details_title) - .setMessage(message) - .setPositiveButton(R.string.ok) { dialog, _ -> - errors.clear() - dialog.dismiss() - } - .setNeutralButton(R.string.report) { dialog, _ -> - launch { - val response = withContext(Dispatchers.Default) { - api.errorReport(errors.map { it.toReportableError(activity) }) - } - - response?.errors?.ifNotEmpty { - Toast.makeText(activity, "Error: " + it[0].reason, Toast.LENGTH_SHORT).show() - return@launch - } - errors.clear() - dialog.dismiss() - } - } - .show() - } + ErrorDetailsDialog(activity, errors) + errors.clear() } val bgColor = ColorUtils.compositeColors( getColorFromAttr(activity, R.attr.colorOnSurface) and 0xcfffffff.toInt(), diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/feedback/FeedbackFragment.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/feedback/FeedbackFragment.kt index 97924168..de921aab 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/feedback/FeedbackFragment.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/feedback/FeedbackFragment.kt @@ -239,22 +239,15 @@ class FeedbackFragment : Fragment(), CoroutineScope { } launch { - val message = withContext(Dispatchers.Default) { - try { - api.sendFeedbackMessage( - senderName = App.profile.accountName ?: App.profile.studentNameLong, - targetDeviceId = if (isDev) currentDeviceId else null, - text = text - )?.also { - app.db.feedbackMessageDao().add(it) - } - } catch (ignore: Exception) { null } - } - - if (message == null) { - Toast.makeText(app, "Nie udało się wysłać wiadomości.", Toast.LENGTH_SHORT).show() - return@launch - } + val message = api.runCatching(activity.errorSnackbar) { + val message = api.sendFeedbackMessage( + senderName = App.profile.accountName ?: App.profile.studentNameLong, + targetDeviceId = if (isDev) currentDeviceId else null, + text = text + ) + app.db.feedbackMessageDao().add(message) + message + } ?: return@launch b.chatLayout.visibility = View.VISIBLE b.inputLayout.visibility = View.GONE diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/settings/SettingsNewFragment.java b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/settings/SettingsNewFragment.java index 6adeeb24..5865dde8 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/settings/SettingsNewFragment.java +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/settings/SettingsNewFragment.java @@ -915,7 +915,7 @@ public class SettingsNewFragment extends MaterialAboutFragment { .negativeText(R.string.abort) .show(); AsyncTask.execute(() -> { - new SzkolnyApi(app).unregisterAppUser(app.getProfile().getUserCode()); + new SzkolnyApi(app).runCatching(szkolnyApi -> null, szkolnyApi -> null); activity.runOnUiThread(() -> { progressDialog.dismiss(); Toast.makeText(activity, getString(R.string.settings_register_allow_registration_dialog_disabling_finished), Toast.LENGTH_SHORT).show(); diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/webpush/WebPushFragment.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/webpush/WebPushFragment.kt index ad47ca7d..b9727901 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/webpush/WebPushFragment.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/webpush/WebPushFragment.kt @@ -15,7 +15,10 @@ import androidx.core.app.ActivityCompat import androidx.core.content.ContextCompat import androidx.fragment.app.Fragment import androidx.recyclerview.widget.LinearLayoutManager -import kotlinx.coroutines.* +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.launch import pl.szczodrzynski.edziennik.* import pl.szczodrzynski.edziennik.data.api.szkolny.SzkolnyApi import pl.szczodrzynski.edziennik.data.api.szkolny.response.WebPushResponse @@ -93,9 +96,9 @@ class WebPushFragment : Fragment(), CoroutineScope { ) launch { - val browsers = withContext(Dispatchers.Default) { - api.listBrowsers() - } + val browsers = api.runCatching(activity.errorSnackbar) { + listBrowsers() + } ?: return@launch updateBrowserList(browsers) } } @@ -128,21 +131,22 @@ class WebPushFragment : Fragment(), CoroutineScope { b.tokenEditText.isEnabled = false b.tokenEditText.clearFocus() launch { - val browsers = withContext(Dispatchers.Default) { - api.pairBrowser(browserId, pairToken) + val browsers = api.runCatching(activity.errorSnackbar) { + pairBrowser(browserId, pairToken) } b.scanQrCode.isEnabled = true b.tokenAccept.isEnabled = true b.tokenEditText.isEnabled = true - updateBrowserList(browsers) + if (browsers != null) + updateBrowserList(browsers) } } private fun unpairBrowser(browserId: String) { launch { - val browsers = withContext(Dispatchers.Default) { - api.unpairBrowser(browserId) - } + val browsers = api.runCatching(activity.errorSnackbar) { + unpairBrowser(browserId) + } ?: return@launch updateBrowserList(browsers) } }