[API] Implement error handling and exception catching in Szkolny API.

This commit is contained in:
Kuba Szczodrzyński 2020-02-16 22:50:06 +01:00
parent fc21d757c3
commit d0992eaf54
16 changed files with 352 additions and 196 deletions

View File

@ -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<CharSequence>, 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
}

View File

@ -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

View File

@ -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)

View File

@ -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)
}

View File

@ -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 <T> 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 <T> 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 <T> 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 <reified T> parseResponse(response: Response<ApiResponse<T>>): 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<T>>(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<Profile>, notifications: List<Notification>, blacklistedIds: List<Long>): List<EventFull> {
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<EventFull>()
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<Nothing>? {
@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<Nothing>? {
@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<Nothing>? {
}*/
fun pairBrowser(browserId: String?, pairToken: String?, onError: ((List<ApiResponse.Error>) -> Unit)? = null): List<WebPushResponse.Browser> {
@Throws(Exception::class)
fun pairBrowser(browserId: String?, pairToken: String?): List<WebPushResponse.Browser> {
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<ApiResponse.Error>) -> Unit)? = null): List<WebPushResponse.Browser> {
@Throws(Exception::class)
fun listBrowsers(): List<WebPushResponse.Browser> {
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<WebPushResponse.Browser> {
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<ErrorReportRequest.Error>): ApiResponse<Nothing>? {
return api.errorReport(ErrorReportRequest(
@Throws(Exception::class)
fun errorReport(errors: List<ErrorReportRequest.Error>) {
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<Nothing>? {
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<List<Update>>? {
return api.updates(channel).execute().body()
@Throws(Exception::class)
fun getUpdate(channel: String): List<Update> {
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
}
}

View File

@ -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}")

View File

@ -18,16 +18,16 @@ interface SzkolnyService {
fun serverSync(@Body request: ServerSyncRequest): Call<ApiResponse<ServerSyncResponse>>
@POST("share")
fun shareEvent(@Body request: EventShareRequest): Call<ApiResponse<Nothing>>
fun shareEvent(@Body request: EventShareRequest): Call<ApiResponse<Unit>>
@POST("webPush")
fun webPush(@Body request: WebPushRequest): Call<ApiResponse<WebPushResponse>>
@POST("errorReport")
fun errorReport(@Body request: ErrorReportRequest): Call<ApiResponse<Nothing>>
fun errorReport(@Body request: ErrorReportRequest): Call<ApiResponse<Unit>>
@POST("appUser")
fun appUser(@Body request: AppUserRequest): Call<ApiResponse<Nothing>>
fun appUser(@Body request: AppUserRequest): Call<ApiResponse<Unit>>
@GET("updates/app")
fun updates(@Query("channel") channel: String = "release"): Call<ApiResponse<List<Update>>>

View File

@ -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

View File

@ -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()
}
}

View File

@ -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()
}

View File

@ -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))
}
}
}

View File

@ -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<ApiError>,
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()
}
}}
}

View File

@ -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(),

View File

@ -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

View File

@ -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();

View File

@ -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)
}
}