[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.RequestBody
import okhttp3.TlsVersion import okhttp3.TlsVersion
import okio.Buffer 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.Notification
import pl.szczodrzynski.edziennik.data.db.entity.Profile import pl.szczodrzynski.edziennik.data.db.entity.Profile
import pl.szczodrzynski.edziennik.data.db.entity.Teacher import pl.szczodrzynski.edziennik.data.db.entity.Teacher
import pl.szczodrzynski.edziennik.data.db.entity.Team import pl.szczodrzynski.edziennik.data.db.entity.Team
import pl.szczodrzynski.edziennik.network.TLSSocketFactory import pl.szczodrzynski.edziennik.network.TLSSocketFactory
import pl.szczodrzynski.edziennik.utils.models.Time import pl.szczodrzynski.edziennik.utils.models.Time
import java.io.InterruptedIOException
import java.io.PrintWriter import java.io.PrintWriter
import java.io.StringWriter import java.io.StringWriter
import java.math.BigInteger import java.math.BigInteger
import java.net.ConnectException
import java.net.SocketTimeoutException
import java.net.UnknownHostException
import java.nio.charset.Charset import java.nio.charset.Charset
import java.security.KeyStore import java.security.KeyStore
import java.security.MessageDigest import java.security.MessageDigest
@ -67,6 +74,7 @@ import java.util.zip.CRC32
import javax.crypto.Mac import javax.crypto.Mac
import javax.crypto.spec.SecretKeySpec import javax.crypto.spec.SecretKeySpec
import javax.net.ssl.SSLContext import javax.net.ssl.SSLContext
import javax.net.ssl.SSLException
import javax.net.ssl.TrustManagerFactory import javax.net.ssl.TrustManagerFactory
import javax.net.ssl.X509TrustManager import javax.net.ssl.X509TrustManager
import kotlin.Pair import kotlin.Pair
@ -1034,5 +1042,29 @@ fun CharSequence.containsAll(list: List<CharSequence>, ignoreCase: Boolean = fal
return true 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) } = 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 CODE_PROFILE_ARCHIVED = 30*/
const val ERROR_APP_CRASH = 1 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_MESSAGE_NOT_SENT = 10
const val ERROR_REQUEST_FAILURE = 50 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.Request
import im.wangchao.mhttp.Response import im.wangchao.mhttp.Response
import pl.szczodrzynski.edziennik.R 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.data.api.szkolny.request.ErrorReportRequest
import pl.szczodrzynski.edziennik.stackTraceString import pl.szczodrzynski.edziennik.stackTraceString
import pl.szczodrzynski.edziennik.toErrorCode
class ApiError(val tag: String, var errorCode: Int) { 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() val id = System.currentTimeMillis()
var profileId: Int? = null var profileId: Int? = null
var throwable: Throwable? = null var throwable: Throwable? = null
@ -58,6 +68,8 @@ class ApiError(val tag: String, var errorCode: Int) {
} }
fun getStringReason(context: Context): String { 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 { return context.resources.getIdentifier("error_${errorCode}_reason", "string", context.packageName).let {
if (it != 0) if (it != 0)
context.getString(it) context.getString(it)

View File

@ -6,21 +6,13 @@ import androidx.core.util.size
import androidx.room.OnConflictStrategy import androidx.room.OnConflictStrategy
import com.google.gson.JsonObject import com.google.gson.JsonObject
import im.wangchao.mhttp.Response import im.wangchao.mhttp.Response
import pl.szczodrzynski.edziennik.App import pl.szczodrzynski.edziennik.*
import pl.szczodrzynski.edziennik.data.api.* import pl.szczodrzynski.edziennik.data.api.ERROR_REQUEST_FAILURE
import pl.szczodrzynski.edziennik.data.api.interfaces.EndpointCallback import pl.szczodrzynski.edziennik.data.api.interfaces.EndpointCallback
import pl.szczodrzynski.edziennik.data.db.AppDb import pl.szczodrzynski.edziennik.data.db.AppDb
import pl.szczodrzynski.edziennik.data.db.entity.* 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.Utils.d
import pl.szczodrzynski.edziennik.utils.models.Date 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) { abstract class Data(val app: App, val profile: Profile?, val loginStore: LoginStore) {
companion object { companion object {
@ -347,27 +339,12 @@ abstract class Data(val app: App, val profile: Profile?, val loginStore: LoginSt
} }
fun error(apiError: ApiError) { fun error(apiError: ApiError) {
apiError.errorCode = when (apiError.throwable) { apiError.errorCode = apiError.throwable?.toErrorCode() ?:
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 ->
if (apiError.errorCode == ERROR_REQUEST_FAILURE) if (apiError.errorCode == ERROR_REQUEST_FAILURE)
when (apiError.response?.code()) { apiError.response?.toErrorCode() ?: apiError.errorCode
400 -> ERROR_REQUEST_HTTP_400 else
401 -> ERROR_REQUEST_HTTP_401 apiError.errorCode
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
}
callback.onError(apiError) callback.onError(apiError)
} }

View File

@ -5,10 +5,17 @@
package pl.szczodrzynski.edziennik.data.api.szkolny package pl.szczodrzynski.edziennik.data.api.szkolny
import android.os.Build import android.os.Build
import androidx.appcompat.app.AppCompatActivity
import com.google.gson.GsonBuilder 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 okhttp3.OkHttpClient
import pl.szczodrzynski.edziennik.App import pl.szczodrzynski.edziennik.App
import pl.szczodrzynski.edziennik.BuildConfig 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.DateAdapter
import pl.szczodrzynski.edziennik.data.api.szkolny.adapter.TimeAdapter import pl.szczodrzynski.edziennik.data.api.szkolny.adapter.TimeAdapter
import pl.szczodrzynski.edziennik.data.api.szkolny.interceptor.SignatureInterceptor 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.entity.Profile
import pl.szczodrzynski.edziennik.data.db.full.EventFull import pl.szczodrzynski.edziennik.data.db.full.EventFull
import pl.szczodrzynski.edziennik.md5 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.Date
import pl.szczodrzynski.edziennik.utils.models.Time import pl.szczodrzynski.edziennik.utils.models.Time
import retrofit2.Response
import retrofit2.Retrofit import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory import retrofit2.converter.gson.GsonConverterFactory
import retrofit2.create import retrofit2.create
import java.util.concurrent.TimeUnit.SECONDS 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 api: SzkolnyService
private val retrofit: Retrofit
private val job = Job()
override val coroutineContext: CoroutineContext
get() = job + Dispatchers.Main
init { init {
val okHttpClient: OkHttpClient = app.http.newBuilder() val okHttpClient: OkHttpClient = app.http.newBuilder()
.followRedirects(true) .followRedirects(true)
.callTimeout(30, SECONDS) .callTimeout(10, SECONDS)
.addInterceptor(SignatureInterceptor(app)) .addInterceptor(SignatureInterceptor(app))
.build() .build()
@ -47,7 +66,7 @@ class SzkolnyApi(val app: App) {
.registerTypeAdapter(Time::class.java, TimeAdapter()) .registerTypeAdapter(Time::class.java, TimeAdapter())
.create()) .create())
val retrofit: Retrofit = Retrofit.Builder() retrofit = Retrofit.Builder()
.baseUrl("https://api.szkolny.eu/") .baseUrl("https://api.szkolny.eu/")
.addConverterFactory(gsonConverterFactory) .addConverterFactory(gsonConverterFactory)
.client(okHttpClient) .client(okHttpClient)
@ -56,6 +75,68 @@ class SzkolnyApi(val app: App) {
api = retrofit.create() 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 { private fun getDevice() = run {
val config = app.config val config = app.config
val device = Device( 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> { fun getEvents(profiles: List<Profile>, notifications: List<Notification>, blacklistedIds: List<Long>): List<EventFull> {
val teams = app.db.teamDao().allNow 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) } notifications = notifications.map { ServerSyncRequest.Notification(it.profileName ?: "", it.type, it.text) }
)).execute().body() )).execute()
parseResponse(response)
val events = mutableListOf<EventFull>() val events = mutableListOf<EventFull>()
response?.data?.events?.forEach { event -> response.body()?.data?.events?.forEach { event ->
if (event.id in blacklistedIds) if (event.id in blacklistedIds)
return@forEach return@forEach
teams.filter { it.code == event.teamCode }.onEach { team -> teams.filter { it.code == event.teamCode }.onEach { team ->
@ -129,100 +212,117 @@ class SzkolnyApi(val app: App) {
return events 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) val team = app.db.teamDao().getByIdNow(event.profileId, event.teamId)
return api.shareEvent(EventShareRequest( val response = api.shareEvent(EventShareRequest(
deviceId = app.deviceId, deviceId = app.deviceId,
device = getDevice(), device = getDevice(),
sharedByName = event.sharedByName, sharedByName = event.sharedByName,
shareTeamCode = team.code, shareTeamCode = team.code,
event = event 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) val team = app.db.teamDao().getByIdNow(event.profileId, event.teamId)
return api.shareEvent(EventShareRequest( val response = api.shareEvent(EventShareRequest(
deviceId = app.deviceId, deviceId = app.deviceId,
device = getDevice(), device = getDevice(),
sharedByName = event.sharedByName, sharedByName = event.sharedByName,
unshareTeamCode = team.code, unshareTeamCode = team.code,
eventId = event.id eventId = event.id
)).execute().body() )).execute()
parseResponse(response)
} }
/*fun eventEditRequest(requesterName: String, event: Event): ApiResponse<Nothing>? { /*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( val response = api.webPush(WebPushRequest(
deviceId = app.deviceId, deviceId = app.deviceId,
device = getDevice(), device = getDevice(),
action = "pairBrowser", action = "pairBrowser",
browserId = browserId, browserId = browserId,
pairToken = pairToken pairToken = pairToken
)).execute().body() )).execute()
parseResponse(response)
response?.errors?.let { return response.body()?.data?.browsers ?: emptyList()
onError?.invoke(it)
return emptyList()
} }
return response?.data?.browsers ?: emptyList() @Throws(Exception::class)
} fun listBrowsers(): List<WebPushResponse.Browser> {
fun listBrowsers(onError: ((List<ApiResponse.Error>) -> Unit)? = null): List<WebPushResponse.Browser> {
val response = api.webPush(WebPushRequest( val response = api.webPush(WebPushRequest(
deviceId = app.deviceId, deviceId = app.deviceId,
device = getDevice(), device = getDevice(),
action = "listBrowsers" 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> { fun unpairBrowser(browserId: String): List<WebPushResponse.Browser> {
val response = api.webPush(WebPushRequest( val response = api.webPush(WebPushRequest(
deviceId = app.deviceId, deviceId = app.deviceId,
device = getDevice(), device = getDevice(),
action = "unpairBrowser", action = "unpairBrowser",
browserId = browserId 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>? { @Throws(Exception::class)
return api.errorReport(ErrorReportRequest( fun errorReport(errors: List<ErrorReportRequest.Error>) {
val response = api.errorReport(ErrorReportRequest(
deviceId = app.deviceId, deviceId = app.deviceId,
device = getDevice(), device = getDevice(),
appVersion = BuildConfig.VERSION_NAME, appVersion = BuildConfig.VERSION_NAME,
errors = errors errors = errors
)).execute().body() )).execute()
parseResponse(response)
} }
fun unregisterAppUser(userCode: String): ApiResponse<Nothing>? { @Throws(Exception::class)
return api.appUser(AppUserRequest( fun unregisterAppUser(userCode: String) {
val response = api.appUser(AppUserRequest(
deviceId = app.deviceId, deviceId = app.deviceId,
device = getDevice(), device = getDevice(),
userCode = userCode userCode = userCode
)).execute().body() )).execute()
parseResponse(response)
} }
fun getUpdate(channel: String): ApiResponse<List<Update>>? { @Throws(Exception::class)
return api.updates(channel).execute().body() 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? { @Throws(Exception::class)
return api.feedbackMessage(FeedbackMessageRequest( fun sendFeedbackMessage(senderName: String?, targetDeviceId: String?, text: String): FeedbackMessage {
val response = api.feedbackMessage(FeedbackMessageRequest(
deviceId = app.deviceId, deviceId = app.deviceId,
device = getDevice(), device = getDevice(),
senderName = senderName, senderName = senderName,
targetDeviceId = targetDeviceId, targetDeviceId = targetDeviceId,
text = text 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>> fun serverSync(@Body request: ServerSyncRequest): Call<ApiResponse<ServerSyncResponse>>
@POST("share") @POST("share")
fun shareEvent(@Body request: EventShareRequest): Call<ApiResponse<Nothing>> fun shareEvent(@Body request: EventShareRequest): Call<ApiResponse<Unit>>
@POST("webPush") @POST("webPush")
fun webPush(@Body request: WebPushRequest): Call<ApiResponse<WebPushResponse>> fun webPush(@Body request: WebPushRequest): Call<ApiResponse<WebPushResponse>>
@POST("errorReport") @POST("errorReport")
fun errorReport(@Body request: ErrorReportRequest): Call<ApiResponse<Nothing>> fun errorReport(@Body request: ErrorReportRequest): Call<ApiResponse<Unit>>
@POST("appUser") @POST("appUser")
fun appUser(@Body request: AppUserRequest): Call<ApiResponse<Nothing>> fun appUser(@Body request: AppUserRequest): Call<ApiResponse<Unit>>
@GET("updates/app") @GET("updates/app")
fun updates(@Query("channel") channel: String = "release"): Call<ApiResponse<List<Update>>> 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 { try {
val update = overrideUpdate val update = overrideUpdate
?: run { ?: run {
val response = withContext(Dispatchers.Default) { SzkolnyApi(app).getUpdate("beta") } val updates = withContext(Dispatchers.Default) {
if (response?.success != true) { SzkolnyApi(app).runCatching({
getUpdate("beta")
}, {
Toast.makeText(app, app.getString(R.string.notification_cant_check_update), Toast.LENGTH_SHORT).show() Toast.makeText(app, app.getString(R.string.notification_cant_check_update), Toast.LENGTH_SHORT).show()
return@run null })
} } ?: return@run null
val updates = response.data
if (updates?.isNotEmpty() != true) { if (updates.isEmpty()) {
app.config.update = null app.config.update = null
Toast.makeText(app, app.getString(R.string.notification_no_update), Toast.LENGTH_SHORT).show() Toast.makeText(app, app.getString(R.string.notification_no_update), Toast.LENGTH_SHORT).show()
return@run null return@run null

View File

@ -150,23 +150,21 @@ class EventDetailsDialog(
private fun removeEvent() { private fun removeEvent() {
launch { launch {
if (eventShared && eventOwn) { 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.runCatching(activity) {
api.unshareEvent(event) unshareEvent(event)
} } ?: return@launch
response?.errors?.ifNotEmpty {
Toast.makeText(activity, "Error: "+it[0].reason, Toast.LENGTH_SHORT).show()
return@launch
}
finishRemoving() finishRemoving()
} else if (eventShared && !eventOwn) { } 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 // TODO
} else { } 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() finishRemoving()
} }
} }

View File

@ -621,14 +621,9 @@ class EventManualDialog(
sharedByName = profile?.studentNameLong sharedByName = profile?.studentNameLong
} }
val response = withContext(Dispatchers.Default) { api.runCatching(activity) {
api.unshareEvent(eventObject) unshareEvent(eventObject)
} } ?: return@launch
response?.errors?.ifNotEmpty {
Toast.makeText(activity, "Error: "+it[0].reason, Toast.LENGTH_SHORT).show()
return@launch
}
eventObject.sharedByName = null eventObject.sharedByName = null
finishAdding(eventObject, metadataObject) finishAdding(eventObject, metadataObject)
@ -643,14 +638,9 @@ class EventManualDialog(
metadataObject.addedDate = System.currentTimeMillis() metadataObject.addedDate = System.currentTimeMillis()
val response = withContext(Dispatchers.Default) { api.runCatching(activity) {
api.shareEvent(eventObject.withMetadata(metadataObject)) shareEvent(eventObject.withMetadata(metadataObject))
} } ?: return@launch
response?.errors?.ifNotEmpty {
Toast.makeText(activity, "Error: "+it[0].reason, Toast.LENGTH_SHORT).show()
return@launch
}
eventObject.sharedBy = "self" eventObject.sharedBy = "self"
finishAdding(eventObject, metadataObject) finishAdding(eventObject, metadataObject)
@ -664,22 +654,20 @@ class EventManualDialog(
private fun removeEvent() { private fun removeEvent() {
launch { launch {
if (editingShared && editingOwn) { if (editingShared && editingOwn) {
// unshare + remove own event
Toast.makeText(activity, R.string.event_manual_unshare_remove, Toast.LENGTH_SHORT).show() Toast.makeText(activity, R.string.event_manual_unshare_remove, Toast.LENGTH_SHORT).show()
val response = withContext(Dispatchers.Default) { api.runCatching(activity) {
api.unshareEvent(editingEvent!!) unshareEvent(editingEvent!!)
} } ?: return@launch
response?.errors?.ifNotEmpty {
Toast.makeText(activity, "Error: "+it[0].reason, Toast.LENGTH_SHORT).show()
return@launch
}
finishRemoving() finishRemoving()
} else if (editingShared && !editingOwn) { } else if (editingShared && !editingOwn) {
// remove + blacklist somebody's event
Toast.makeText(activity, "Nie zaimplementowana opcja :(", Toast.LENGTH_SHORT).show() Toast.makeText(activity, "Nie zaimplementowana opcja :(", Toast.LENGTH_SHORT).show()
// TODO // TODO
} else { } else {
// remove event
Toast.makeText(activity, R.string.event_manual_remove, Toast.LENGTH_SHORT).show() Toast.makeText(activity, R.string.event_manual_remove, Toast.LENGTH_SHORT).show()
finishRemoving() 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.SzkolnyApi
import pl.szczodrzynski.edziennik.data.api.szkolny.request.ErrorReportRequest import pl.szczodrzynski.edziennik.data.api.szkolny.request.ErrorReportRequest
import pl.szczodrzynski.edziennik.data.db.entity.Profile import pl.szczodrzynski.edziennik.data.db.entity.Profile
import pl.szczodrzynski.edziennik.ifNotEmpty
import pl.szczodrzynski.edziennik.utils.Themes.appTheme import pl.szczodrzynski.edziennik.utils.Themes.appTheme
import kotlin.coroutines.CoroutineContext import kotlin.coroutines.CoroutineContext
@ -86,22 +85,17 @@ class CrashActivity : AppCompatActivity(), CoroutineScope {
.show() .show()
} else { } else {
launch { launch {
val response = withContext(Dispatchers.Default) { api.runCatching({
api.errorReport(listOf(getReportableError(intent))) 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() Toast.makeText(app, getString(R.string.crash_report_sent), Toast.LENGTH_SHORT).show()
reportButton.isEnabled = false reportButton.isEnabled = false
reportButton.setTextColor(resources.getColor(android.R.color.darker_gray)) 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()
}
} }
} }
} }

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 package pl.szczodrzynski.edziennik.ui.modules.error
import android.view.View import android.view.View
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.coordinatorlayout.widget.CoordinatorLayout import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.graphics.ColorUtils import androidx.core.graphics.ColorUtils
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import kotlinx.coroutines.* import kotlinx.coroutines.CoroutineScope
import pl.szczodrzynski.edziennik.* 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.models.ApiError
import pl.szczodrzynski.edziennik.data.api.szkolny.SzkolnyApi
import pl.szczodrzynski.navlib.getColorFromAttr import pl.szczodrzynski.navlib.getColorFromAttr
import kotlin.coroutines.CoroutineContext import kotlin.coroutines.CoroutineContext
@ -31,47 +30,12 @@ class ErrorSnackbar(val activity: AppCompatActivity) : CoroutineScope {
override val coroutineContext: CoroutineContext override val coroutineContext: CoroutineContext
get() = job + Dispatchers.Main get() = job + Dispatchers.Main
private val api by lazy { SzkolnyApi(activity.applicationContext as App) }
fun setCoordinator(coordinatorLayout: CoordinatorLayout, showAbove: View? = null) { fun setCoordinator(coordinatorLayout: CoordinatorLayout, showAbove: View? = null) {
this.coordinator = coordinatorLayout this.coordinator = coordinatorLayout
snackbar = Snackbar.make(coordinator, R.string.snackbar_error_text, Snackbar.LENGTH_INDEFINITE) snackbar = Snackbar.make(coordinator, R.string.snackbar_error_text, Snackbar.LENGTH_INDEFINITE)
snackbar?.setAction(R.string.more) { snackbar?.setAction(R.string.more) {
if (errors.isNotEmpty()) { ErrorDetailsDialog(activity, errors)
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() 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()
}
} }
val bgColor = ColorUtils.compositeColors( val bgColor = ColorUtils.compositeColors(
getColorFromAttr(activity, R.attr.colorOnSurface) and 0xcfffffff.toInt(), getColorFromAttr(activity, R.attr.colorOnSurface) and 0xcfffffff.toInt(),

View File

@ -239,22 +239,15 @@ class FeedbackFragment : Fragment(), CoroutineScope {
} }
launch { launch {
val message = withContext(Dispatchers.Default) { val message = api.runCatching(activity.errorSnackbar) {
try { val message = api.sendFeedbackMessage(
api.sendFeedbackMessage(
senderName = App.profile.accountName ?: App.profile.studentNameLong, senderName = App.profile.accountName ?: App.profile.studentNameLong,
targetDeviceId = if (isDev) currentDeviceId else null, targetDeviceId = if (isDev) currentDeviceId else null,
text = text text = text
)?.also { )
app.db.feedbackMessageDao().add(it) app.db.feedbackMessageDao().add(message)
} message
} catch (ignore: Exception) { null } } ?: return@launch
}
if (message == null) {
Toast.makeText(app, "Nie udało się wysłać wiadomości.", Toast.LENGTH_SHORT).show()
return@launch
}
b.chatLayout.visibility = View.VISIBLE b.chatLayout.visibility = View.VISIBLE
b.inputLayout.visibility = View.GONE b.inputLayout.visibility = View.GONE

View File

@ -915,7 +915,7 @@ public class SettingsNewFragment extends MaterialAboutFragment {
.negativeText(R.string.abort) .negativeText(R.string.abort)
.show(); .show();
AsyncTask.execute(() -> { AsyncTask.execute(() -> {
new SzkolnyApi(app).unregisterAppUser(app.getProfile().getUserCode()); new SzkolnyApi(app).runCatching(szkolnyApi -> null, szkolnyApi -> null);
activity.runOnUiThread(() -> { activity.runOnUiThread(() -> {
progressDialog.dismiss(); progressDialog.dismiss();
Toast.makeText(activity, getString(R.string.settings_register_allow_registration_dialog_disabling_finished), Toast.LENGTH_SHORT).show(); 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.core.content.ContextCompat
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.recyclerview.widget.LinearLayoutManager 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.*
import pl.szczodrzynski.edziennik.data.api.szkolny.SzkolnyApi import pl.szczodrzynski.edziennik.data.api.szkolny.SzkolnyApi
import pl.szczodrzynski.edziennik.data.api.szkolny.response.WebPushResponse import pl.szczodrzynski.edziennik.data.api.szkolny.response.WebPushResponse
@ -93,9 +96,9 @@ class WebPushFragment : Fragment(), CoroutineScope {
) )
launch { launch {
val browsers = withContext(Dispatchers.Default) { val browsers = api.runCatching(activity.errorSnackbar) {
api.listBrowsers() listBrowsers()
} } ?: return@launch
updateBrowserList(browsers) updateBrowserList(browsers)
} }
} }
@ -128,21 +131,22 @@ class WebPushFragment : Fragment(), CoroutineScope {
b.tokenEditText.isEnabled = false b.tokenEditText.isEnabled = false
b.tokenEditText.clearFocus() b.tokenEditText.clearFocus()
launch { launch {
val browsers = withContext(Dispatchers.Default) { val browsers = api.runCatching(activity.errorSnackbar) {
api.pairBrowser(browserId, pairToken) pairBrowser(browserId, pairToken)
} }
b.scanQrCode.isEnabled = true b.scanQrCode.isEnabled = true
b.tokenAccept.isEnabled = true b.tokenAccept.isEnabled = true
b.tokenEditText.isEnabled = true b.tokenEditText.isEnabled = true
if (browsers != null)
updateBrowserList(browsers) updateBrowserList(browsers)
} }
} }
private fun unpairBrowser(browserId: String) { private fun unpairBrowser(browserId: String) {
launch { launch {
val browsers = withContext(Dispatchers.Default) { val browsers = api.runCatching(activity.errorSnackbar) {
api.unpairBrowser(browserId) unpairBrowser(browserId)
} } ?: return@launch
updateBrowserList(browsers) updateBrowserList(browsers)
} }
} }