[API] Refactor register availability checking module.

This commit is contained in:
Kuba Szczodrzyński 2021-09-18 16:36:17 +02:00
parent 91b685576b
commit 80dcd9aa69
No known key found for this signature in database
GPG Key ID: 70CB8A85BA1633CB
12 changed files with 162 additions and 122 deletions

View File

@ -71,6 +71,7 @@ class App : MultiDexApplication(), Configuration.Provider, CoroutineScope {
val permissionManager by lazy { PermissionManager(this) } val permissionManager by lazy { PermissionManager(this) }
val attendanceManager by lazy { AttendanceManager(this) } val attendanceManager by lazy { AttendanceManager(this) }
val buildManager by lazy { BuildManager(this) } val buildManager by lazy { BuildManager(this) }
val availabilityManager by lazy { AvailabilityManager(this) }
val db val db
get() = App.db get() = App.db

View File

@ -85,6 +85,8 @@ import pl.szczodrzynski.edziennik.ui.modules.webpush.WebPushFragment
import pl.szczodrzynski.edziennik.utils.* import pl.szczodrzynski.edziennik.utils.*
import pl.szczodrzynski.edziennik.utils.Utils.d import pl.szczodrzynski.edziennik.utils.Utils.d
import pl.szczodrzynski.edziennik.utils.Utils.dpToPx import pl.szczodrzynski.edziennik.utils.Utils.dpToPx
import pl.szczodrzynski.edziennik.utils.managers.AvailabilityManager
import pl.szczodrzynski.edziennik.utils.managers.AvailabilityManager.Error.Type
import pl.szczodrzynski.edziennik.utils.models.Date import pl.szczodrzynski.edziennik.utils.models.Date
import pl.szczodrzynski.edziennik.utils.models.NavTarget import pl.szczodrzynski.edziennik.utils.models.NavTarget
import pl.szczodrzynski.navlib.* import pl.szczodrzynski.navlib.*
@ -634,45 +636,23 @@ class MainActivity : AppCompatActivity(), CoroutineScope {
return return
} }
app.profile.registerName?.let { registerName -> val error = withContext(Dispatchers.IO) {
var status = app.config.sync.registerAvailability[registerName] app.availabilityManager.check(app.profile)
if (status == null || status.nextCheckAt < currentTimeUnix()) { }
val api = SzkolnyApi(app) when (error?.type) {
val result = withContext(Dispatchers.IO) { Type.NOT_AVAILABLE -> {
return@withContext api.runCatching({
val availability = getRegisterAvailability()
app.config.sync.registerAvailability = availability
availability[registerName]
}, onError = {
if (it.toErrorCode() == ERROR_API_INVALID_SIGNATURE) {
return@withContext false
}
return@withContext it
})
}
when (result) {
false -> {
Toast.makeText(this@MainActivity, R.string.error_no_api_access, Toast.LENGTH_SHORT).show()
return@let
}
is Throwable -> {
errorSnackbar.addError(result.toApiError(TAG)).show()
return
}
is RegisterAvailabilityStatus -> {
status = result
}
}
}
if (status?.available != true || status.minVersionCode > BuildConfig.VERSION_CODE) {
swipeRefreshLayout.isRefreshing = false swipeRefreshLayout.isRefreshing = false
loadTarget(DRAWER_ITEM_HOME) loadTarget(DRAWER_ITEM_HOME)
if (status != null) RegisterUnavailableDialog(this, error.status!!)
RegisterUnavailableDialog(this, status)
return return
} }
Type.API_ERROR -> {
errorSnackbar.addError(error.apiError!!).show()
return
}
Type.NO_API_ACCESS -> {
Toast.makeText(this, R.string.error_no_api_access, Toast.LENGTH_SHORT).show()
}
} }
swipeRefreshLayout.isRefreshing = true swipeRefreshLayout.isRefreshing = true
@ -699,10 +679,9 @@ class MainActivity : AppCompatActivity(), CoroutineScope {
@Subscribe(threadMode = ThreadMode.MAIN, sticky = true) @Subscribe(threadMode = ThreadMode.MAIN, sticky = true)
fun onRegisterAvailabilityEvent(event: RegisterAvailabilityEvent) { fun onRegisterAvailabilityEvent(event: RegisterAvailabilityEvent) {
EventBus.getDefault().removeStickyEvent(event) EventBus.getDefault().removeStickyEvent(event)
app.profile.registerName?.let { registerName -> val error = app.availabilityManager.check(app.profile, cacheOnly = true)
event.data[registerName]?.let { if (error != null) {
RegisterUnavailableDialog(this, it) RegisterUnavailableDialog(this, error.status!!)
}
} }
} }
@Subscribe(threadMode = ThreadMode.MAIN) @Subscribe(threadMode = ThreadMode.MAIN)

View File

@ -125,6 +125,11 @@ class Config(val db: AppDb) : CoroutineScope, AbstractConfig {
get() { mApiInvalidCert = mApiInvalidCert ?: values["apiInvalidCert"]; return mApiInvalidCert } get() { mApiInvalidCert = mApiInvalidCert ?: values["apiInvalidCert"]; return mApiInvalidCert }
set(value) { set("apiInvalidCert", value); mApiInvalidCert = value } set(value) { set("apiInvalidCert", value); mApiInvalidCert = value }
private var mApiAvailabilityCheck: Boolean? = null
var apiAvailabilityCheck: Boolean
get() { mApiAvailabilityCheck = mApiAvailabilityCheck ?: values.get("apiAvailabilityCheck", true); return mApiAvailabilityCheck ?: true }
set(value) { set("apiAvailabilityCheck", value); mApiAvailabilityCheck = value }
private var rawEntries: List<ConfigEntry> = db.configDao().getAllNow() private var rawEntries: List<ConfigEntry> = db.configDao().getAllNow()
private val profileConfigs: HashMap<Int, ProfileConfig> = hashMapOf() private val profileConfigs: HashMap<Int, ProfileConfig> = hashMapOf()
init { init {

View File

@ -18,7 +18,6 @@ import pl.szczodrzynski.edziennik.data.api.events.RegisterAvailabilityEvent
import pl.szczodrzynski.edziennik.data.api.interfaces.EdziennikCallback import pl.szczodrzynski.edziennik.data.api.interfaces.EdziennikCallback
import pl.szczodrzynski.edziennik.data.api.interfaces.EdziennikInterface import pl.szczodrzynski.edziennik.data.api.interfaces.EdziennikInterface
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.edziennik.data.api.task.IApiTask import pl.szczodrzynski.edziennik.data.api.task.IApiTask
import pl.szczodrzynski.edziennik.data.db.entity.LoginStore import pl.szczodrzynski.edziennik.data.db.entity.LoginStore
import pl.szczodrzynski.edziennik.data.db.entity.Profile import pl.szczodrzynski.edziennik.data.db.entity.Profile
@ -27,6 +26,7 @@ import pl.szczodrzynski.edziennik.data.db.full.AnnouncementFull
import pl.szczodrzynski.edziennik.data.db.full.EventFull import pl.szczodrzynski.edziennik.data.db.full.EventFull
import pl.szczodrzynski.edziennik.data.db.full.MessageFull import pl.szczodrzynski.edziennik.data.db.full.MessageFull
import pl.szczodrzynski.edziennik.utils.Utils.d import pl.szczodrzynski.edziennik.utils.Utils.d
import pl.szczodrzynski.edziennik.utils.managers.AvailabilityManager.Error.Type
open class EdziennikTask(override val profileId: Int, val request: Any) : IApiTask(profileId) { open class EdziennikTask(override val profileId: Int, val request: Any) : IApiTask(profileId) {
companion object { companion object {
@ -90,35 +90,21 @@ open class EdziennikTask(override val profileId: Int, val request: Any) : IApiTa
return return
} }
profile.registerName?.also { registerName -> val error = app.availabilityManager.check(profile)
var status = app.config.sync.registerAvailability[registerName] when (error?.type) {
if (status == null || status.nextCheckAt < currentTimeUnix()) { Type.NOT_AVAILABLE -> {
val api = SzkolnyApi(app)
api.runCatching({
val availability = getRegisterAvailability()
app.config.sync.registerAvailability = availability
status = availability[registerName]
}, onError = {
val apiError = it.toApiError(TAG)
if (apiError.errorCode == ERROR_API_INVALID_SIGNATURE) {
return@also
}
taskCallback.onError(apiError)
return
})
}
if (status?.available != true
|| status?.minVersionCode ?: BuildConfig.VERSION_CODE > BuildConfig.VERSION_CODE) {
if (EventBus.getDefault().hasSubscriberForEvent(RegisterAvailabilityEvent::class.java)) { if (EventBus.getDefault().hasSubscriberForEvent(RegisterAvailabilityEvent::class.java)) {
EventBus.getDefault().postSticky( EventBus.getDefault().postSticky(RegisterAvailabilityEvent())
RegisterAvailabilityEvent(app.config.sync.registerAvailability)
)
} }
cancel() cancel()
taskCallback.onCompleted() taskCallback.onCompleted()
return return
} }
Type.API_ERROR -> {
taskCallback.onError(error.apiError!!)
return
}
else -> return@let
} }
} }

View File

@ -4,8 +4,4 @@
package pl.szczodrzynski.edziennik.data.api.events package pl.szczodrzynski.edziennik.data.api.events
import pl.szczodrzynski.edziennik.data.api.szkolny.response.RegisterAvailabilityStatus class RegisterAvailabilityEvent()
data class RegisterAvailabilityEvent(
val data: Map< String, RegisterAvailabilityStatus>
)

View File

@ -140,7 +140,7 @@ open class Profile(
LOGIN_TYPE_MOBIDZIENNIK -> "mobidziennik" LOGIN_TYPE_MOBIDZIENNIK -> "mobidziennik"
LOGIN_TYPE_PODLASIE -> "podlasie" LOGIN_TYPE_PODLASIE -> "podlasie"
LOGIN_TYPE_EDUDZIENNIK -> "edudziennik" LOGIN_TYPE_EDUDZIENNIK -> "edudziennik"
else -> null else -> "unknown"
} }
override fun getImageDrawable(context: Context): Drawable { override fun getImageDrawable(context: Context): Drawable {

View File

@ -60,7 +60,7 @@ class SzkolnyAppFirebase(val app: App, val profiles: List<Profile>, val message:
) ?: return@launch ) ?: return@launch
app.config.sync.registerAvailability = data app.config.sync.registerAvailability = data
if (EventBus.getDefault().hasSubscriberForEvent(RegisterAvailabilityEvent::class.java)) { if (EventBus.getDefault().hasSubscriberForEvent(RegisterAvailabilityEvent::class.java)) {
EventBus.getDefault().postSticky(RegisterAvailabilityEvent(data)) EventBus.getDefault().postSticky(RegisterAvailabilityEvent())
} }
} }
} }

View File

@ -42,8 +42,6 @@ class RegisterUnavailableDialog(
init { run { init { run {
if (activity.isFinishing) if (activity.isFinishing)
return@run return@run
if (status.available && status.minVersionCode <= BuildConfig.VERSION_CODE)
return@run
onShowListener?.invoke(TAG) onShowListener?.invoke(TAG)
app = activity.applicationContext as App app = activity.applicationContext as App

View File

@ -163,10 +163,9 @@ class HomeFragment : Fragment(), CoroutineScope {
if (app.profile.archived) if (app.profile.archived)
items.add(0, HomeArchiveCard(101, app, activity, this, app.profile)) items.add(0, HomeArchiveCard(101, app, activity, this, app.profile))
val status = app.config.sync.registerAvailability[app.profile.registerName] val status = app.availabilityManager.check(app.profile, cacheOnly = true)?.status
val update = app.config.update val update = app.config.update
if (update != null && update.versionCode > BuildConfig.VERSION_CODE if (update != null && update.versionCode > BuildConfig.VERSION_CODE || status?.userMessage != null) {
|| status != null && (!status.available || status.minVersionCode > BuildConfig.VERSION_CODE)) {
items.add(0, HomeAvailabilityCard(102, app, activity, this, app.profile)) items.add(0, HomeAvailabilityCard(102, app, activity, this, app.profile))
} }

View File

@ -50,7 +50,8 @@ class HomeAvailabilityCard(
} }
holder.root += b.root holder.root += b.root
val status = app.config.sync.registerAvailability[profile.registerName] val error = app.availabilityManager.check(profile, cacheOnly = true)
val status = error?.status
val update = app.config.update val update = app.config.update
if (update == null && status == null) if (update == null && status == null)
@ -58,7 +59,8 @@ class HomeAvailabilityCard(
var onInfoClick = { _: View -> } var onInfoClick = { _: View -> }
if (status != null && !status.available && status.userMessage != null) { // show "register unavailable" only when disabled
if (status?.userMessage != null) {
b.homeAvailabilityTitle.text = HtmlCompat.fromHtml(status.userMessage.title, HtmlCompat.FROM_HTML_MODE_LEGACY) b.homeAvailabilityTitle.text = HtmlCompat.fromHtml(status.userMessage.title, HtmlCompat.FROM_HTML_MODE_LEGACY)
b.homeAvailabilityText.text = HtmlCompat.fromHtml(status.userMessage.contentShort, HtmlCompat.FROM_HTML_MODE_LEGACY) b.homeAvailabilityText.text = HtmlCompat.fromHtml(status.userMessage.contentShort, HtmlCompat.FROM_HTML_MODE_LEGACY)
b.homeAvailabilityUpdate.isVisible = false b.homeAvailabilityUpdate.isVisible = false
@ -69,6 +71,7 @@ class HomeAvailabilityCard(
RegisterUnavailableDialog(activity, status) RegisterUnavailableDialog(activity, status)
} }
} }
// show "update available" when available OR version too old for the register
else if (update != null && update.versionCode > BuildConfig.VERSION_CODE) { else if (update != null && update.versionCode > BuildConfig.VERSION_CODE) {
b.homeAvailabilityTitle.setText(R.string.home_availability_title) b.homeAvailabilityTitle.setText(R.string.home_availability_title)
b.homeAvailabilityText.setText(R.string.home_availability_text, update.versionName) b.homeAvailabilityText.setText(R.string.home_availability_text, update.versionName)
@ -78,6 +81,9 @@ class HomeAvailabilityCard(
UpdateAvailableDialog(activity, update) UpdateAvailableDialog(activity, update)
} }
} }
else {
b.root.isVisible = false
}
b.homeAvailabilityUpdate.onClick { b.homeAvailabilityUpdate.onClick {
if (update == null) if (update == null)

View File

@ -26,13 +26,12 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder
import kotlinx.coroutines.* import kotlinx.coroutines.*
import pl.szczodrzynski.edziennik.* import pl.szczodrzynski.edziennik.*
import pl.szczodrzynski.edziennik.data.api.* import pl.szczodrzynski.edziennik.data.api.*
import pl.szczodrzynski.edziennik.data.api.szkolny.SzkolnyApi
import pl.szczodrzynski.edziennik.data.api.szkolny.response.RegisterAvailabilityStatus
import pl.szczodrzynski.edziennik.databinding.LoginChooserFragmentBinding import pl.szczodrzynski.edziennik.databinding.LoginChooserFragmentBinding
import pl.szczodrzynski.edziennik.ui.dialogs.RegisterUnavailableDialog import pl.szczodrzynski.edziennik.ui.dialogs.RegisterUnavailableDialog
import pl.szczodrzynski.edziennik.ui.modules.feedback.FeedbackActivity import pl.szczodrzynski.edziennik.ui.modules.feedback.FeedbackActivity
import pl.szczodrzynski.edziennik.utils.BetterLinkMovementMethod import pl.szczodrzynski.edziennik.utils.BetterLinkMovementMethod
import pl.szczodrzynski.edziennik.utils.SimpleDividerItemDecoration import pl.szczodrzynski.edziennik.utils.SimpleDividerItemDecoration
import pl.szczodrzynski.edziennik.utils.managers.AvailabilityManager.Error.Type
import pl.szczodrzynski.edziennik.utils.models.Date import pl.szczodrzynski.edziennik.utils.models.Date
import kotlin.coroutines.CoroutineContext import kotlin.coroutines.CoroutineContext
@ -269,52 +268,23 @@ class LoginChooserFragment : Fragment(), CoroutineScope {
} }
private suspend fun checkAvailability(loginType: Int): Boolean { private suspend fun checkAvailability(loginType: Int): Boolean {
when (loginType) { val error = withContext(Dispatchers.IO) {
LOGIN_TYPE_LIBRUS -> "librus" app.availabilityManager.check(loginType)
LOGIN_TYPE_VULCAN -> "vulcan" } ?: return true
LOGIN_TYPE_IDZIENNIK -> "idziennik"
LOGIN_TYPE_MOBIDZIENNIK -> "mobidziennik"
LOGIN_TYPE_PODLASIE -> "podlasie"
LOGIN_TYPE_EDUDZIENNIK -> "edudziennik"
else -> null
}?.let { registerName ->
var status = app.config.sync.registerAvailability[registerName]
if (status == null || status.nextCheckAt < currentTimeUnix()) {
val api = SzkolnyApi(app)
val result = withContext(Dispatchers.IO) {
return@withContext api.runCatching({
val availability = getRegisterAvailability()
app.config.sync.registerAvailability = availability
availability[registerName]
}, onError = {
if (it.toErrorCode() == ERROR_API_INVALID_SIGNATURE) {
return@withContext false
}
return@withContext it
})
}
when (result) { return when (error.type) {
false -> { Type.NOT_AVAILABLE -> {
Toast.makeText(activity, R.string.error_no_api_access, Toast.LENGTH_SHORT).show() RegisterUnavailableDialog(activity, error.status!!)
return@let false
}
is Throwable -> {
activity.errorSnackbar.addError(result.toApiError(TAG)).show()
return false
}
is RegisterAvailabilityStatus -> {
status = result
}
}
} }
Type.API_ERROR -> {
if (status?.available != true || status.minVersionCode > BuildConfig.VERSION_CODE) { activity.errorSnackbar.addError(error.apiError!!).show()
if (status != null) false
RegisterUnavailableDialog(activity, status) }
return false Type.NO_API_ACCESS -> {
Toast.makeText(activity, R.string.error_no_api_access, Toast.LENGTH_SHORT).show()
true
} }
} }
return true
} }
} }

View File

@ -0,0 +1,100 @@
/*
* Copyright (c) Kuba Szczodrzyński 2021-9-18.
*/
package pl.szczodrzynski.edziennik.utils.managers
import pl.szczodrzynski.edziennik.App
import pl.szczodrzynski.edziennik.BuildConfig
import pl.szczodrzynski.edziennik.currentTimeUnix
import pl.szczodrzynski.edziennik.data.api.*
import pl.szczodrzynski.edziennik.data.api.models.ApiError
import pl.szczodrzynski.edziennik.data.api.szkolny.SzkolnyApi
import pl.szczodrzynski.edziennik.data.api.szkolny.response.RegisterAvailabilityStatus
import pl.szczodrzynski.edziennik.data.db.entity.Profile
import pl.szczodrzynski.edziennik.toApiError
class AvailabilityManager(val app: App) {
companion object {
private const val TAG = "AvailabilityManager"
}
private val api = SzkolnyApi(app)
data class Error(
val type: Type,
val status: RegisterAvailabilityStatus?,
val apiError: ApiError?
) {
companion object {
fun notAvailable(status: RegisterAvailabilityStatus) =
Error(Type.NOT_AVAILABLE, status, null)
fun apiError(apiError: ApiError) =
Error(Type.API_ERROR, null, apiError)
fun noApiAccess() =
Error(Type.NO_API_ACCESS, null, null)
}
enum class Type {
NOT_AVAILABLE,
API_ERROR,
NO_API_ACCESS,
}
}
fun check(profile: Profile, cacheOnly: Boolean = false): Error? {
return check(profile.registerName, cacheOnly)
}
fun check(loginType: Int, cacheOnly: Boolean = false): Error? {
val registerName = when (loginType) {
LOGIN_TYPE_LIBRUS -> "librus"
LOGIN_TYPE_VULCAN -> "vulcan"
LOGIN_TYPE_IDZIENNIK -> "idziennik"
LOGIN_TYPE_MOBIDZIENNIK -> "mobidziennik"
LOGIN_TYPE_PODLASIE -> "podlasie"
LOGIN_TYPE_EDUDZIENNIK -> "edudziennik"
else -> "unknown"
}
return check(registerName, cacheOnly)
}
fun check(registerName: String, cacheOnly: Boolean = false): Error? {
if (!app.config.apiAvailabilityCheck)
return null
val status = app.config.sync.registerAvailability[registerName]
if (status != null && status.nextCheckAt > currentTimeUnix()) {
return reportStatus(status)
}
if (cacheOnly) {
return reportStatus(status)
}
return try {
val availability = api.getRegisterAvailability()
app.config.sync.registerAvailability = availability
reportStatus(availability[registerName])
} catch (e: Throwable) {
reportApiError(e)
}
}
private fun reportStatus(status: RegisterAvailabilityStatus?): Error? {
if (status == null)
return null
if (!status.available || status.minVersionCode > BuildConfig.VERSION_CODE)
return Error.notAvailable(status)
return null
}
private fun reportApiError(throwable: Throwable): Error {
val apiError = throwable.toApiError(TAG)
if (apiError.errorCode == ERROR_API_INVALID_SIGNATURE) {
app.config.sync.registerAvailability = mapOf()
return Error.noApiAccess()
}
return Error.apiError(apiError)
}
}