diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/MainActivity.kt b/app/src/main/java/pl/szczodrzynski/edziennik/MainActivity.kt index 3932ce3b..e1fe3b0c 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/MainActivity.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/MainActivity.kt @@ -41,6 +41,8 @@ import pl.droidsonroids.gif.GifDrawable import pl.szczodrzynski.edziennik.data.api.edziennik.EdziennikTask import pl.szczodrzynski.edziennik.data.api.events.* import pl.szczodrzynski.edziennik.data.api.models.ApiError +import pl.szczodrzynski.edziennik.data.api.szkolny.SzkolnyApi +import pl.szczodrzynski.edziennik.data.api.szkolny.response.Update import pl.szczodrzynski.edziennik.data.db.entity.LoginStore import pl.szczodrzynski.edziennik.data.db.entity.Metadata.* import pl.szczodrzynski.edziennik.data.db.entity.Profile @@ -48,7 +50,9 @@ import pl.szczodrzynski.edziennik.databinding.ActivitySzkolnyBinding import pl.szczodrzynski.edziennik.sync.AppManagerDetectedEvent import pl.szczodrzynski.edziennik.sync.SyncWorker import pl.szczodrzynski.edziennik.sync.UpdateWorker +import pl.szczodrzynski.edziennik.ui.dialogs.RegisterUnavailableDialog import pl.szczodrzynski.edziennik.ui.dialogs.ServerMessageDialog +import pl.szczodrzynski.edziennik.ui.dialogs.UpdateAvailableDialog import pl.szczodrzynski.edziennik.ui.dialogs.changelog.ChangelogDialog import pl.szczodrzynski.edziennik.ui.dialogs.event.EventManualDialog import pl.szczodrzynski.edziennik.ui.dialogs.settings.ProfileRemoveDialog @@ -438,7 +442,7 @@ class MainActivity : AppCompatActivity(), CoroutineScope { }) b.swipeRefreshLayout.isEnabled = true - b.swipeRefreshLayout.setOnRefreshListener { this.syncCurrentFeature() } + b.swipeRefreshLayout.setOnRefreshListener { launch { syncCurrentFeature() } } b.swipeRefreshLayout.setColorSchemeResources( R.color.md_blue_500, R.color.md_amber_500, @@ -604,7 +608,7 @@ class MainActivity : AppCompatActivity(), CoroutineScope { |_____/ \__, |_| |_|\___| __/ | |__*/ - fun syncCurrentFeature() { + suspend fun syncCurrentFeature() { if (app.profile.archived) { MaterialAlertDialogBuilder(this) .setTitle(R.string.profile_archived_title) @@ -640,6 +644,30 @@ class MainActivity : AppCompatActivity(), CoroutineScope { swipeRefreshLayout.isRefreshing = false return } + + app.profile.registerName?.let { registerName -> + var status = app.config.sync.registerAvailability[registerName] + if (status == null || status.nextCheck < currentTimeUnix()) { + withContext(Dispatchers.IO) { + val api = SzkolnyApi(app) + api.runCatching(this@MainActivity) { + val availability = getRegisterAvailability() + app.config.sync.registerAvailability = availability + status = availability[registerName] + } + } + } + + if (status?.available != true + || status?.minVersionCode ?: BuildConfig.VERSION_CODE > BuildConfig.VERSION_CODE) { + swipeRefreshLayout.isRefreshing = false + loadTarget(DRAWER_ITEM_HOME) + if (status != null) + RegisterUnavailableDialog(this, status!!) + return + } + } + swipeRefreshLayout.isRefreshing = true Toast.makeText(this, fragmentToSyncName(navTargetId), Toast.LENGTH_SHORT).show() val fragmentParam = when (navTargetId) { @@ -656,6 +684,20 @@ class MainActivity : AppCompatActivity(), CoroutineScope { arguments = arguments ).enqueue(this) } + @Subscribe(threadMode = ThreadMode.MAIN, sticky = true) + fun onUpdateEvent(event: Update) { + EventBus.getDefault().removeStickyEvent(event) + UpdateAvailableDialog(this, event) + } + @Subscribe(threadMode = ThreadMode.MAIN, sticky = true) + fun onRegisterAvailabilityEvent(event: RegisterAvailabilityEvent) { + EventBus.getDefault().removeStickyEvent(event) + app.profile.registerName?.let { registerName -> + event.data[registerName]?.let { + RegisterUnavailableDialog(this, it) + } + } + } @Subscribe(threadMode = ThreadMode.MAIN) fun onApiTaskStartedEvent(event: ApiTaskStartedEvent) { swipeRefreshLayout.isRefreshing = true diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/config/ConfigSync.kt b/app/src/main/java/pl/szczodrzynski/edziennik/config/ConfigSync.kt index a6bb68df..2d7dcf78 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/config/ConfigSync.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/config/ConfigSync.kt @@ -4,12 +4,18 @@ package pl.szczodrzynski.edziennik.config +import com.google.gson.Gson +import com.google.gson.reflect.TypeToken import pl.szczodrzynski.edziennik.config.utils.get import pl.szczodrzynski.edziennik.config.utils.getIntList import pl.szczodrzynski.edziennik.config.utils.set +import pl.szczodrzynski.edziennik.config.utils.setMap +import pl.szczodrzynski.edziennik.data.api.szkolny.response.RegisterAvailabilityStatus import pl.szczodrzynski.edziennik.utils.models.Time class ConfigSync(private val config: Config) { + private val gson = Gson() + private var mDontShowAppManagerDialog: Boolean? = null var dontShowAppManagerDialog: Boolean get() { mDontShowAppManagerDialog = mDontShowAppManagerDialog ?: config.values.get("dontShowAppManagerDialog", false); return mDontShowAppManagerDialog ?: false } @@ -106,4 +112,9 @@ class ConfigSync(private val config: Config) { var tokenVulcanList: List get() { mTokenVulcanList = mTokenVulcanList ?: config.values.getIntList("tokenVulcanList", listOf()); return mTokenVulcanList ?: listOf() } set(value) { config.set("tokenVulcanList", value); mTokenVulcanList = value } + + private var mRegisterAvailability: Map? = null + var registerAvailability: Map + get() { mRegisterAvailability = mRegisterAvailability ?: config.values.get("registerAvailability", null as String?)?.let { it -> gson.fromJson>(it, object: TypeToken>(){}.type) }; return mRegisterAvailability ?: mapOf() } + set(value) { config.setMap("registerAvailability", value); mRegisterAvailability = value } } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/config/utils/ConfigExtensions.kt b/app/src/main/java/pl/szczodrzynski/edziennik/config/utils/ConfigExtensions.kt index 1188b1a4..c98cf832 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/config/utils/ConfigExtensions.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/config/utils/ConfigExtensions.kt @@ -49,6 +49,9 @@ fun AbstractConfig.setIntList(key: String, value: List?) { fun AbstractConfig.setLongList(key: String, value: List?) { set(key, value?.let { gson.toJson(it) }) } +fun AbstractConfig.setMap(key: String, value: Map?) { + set(key, value?.let { gson.toJson(it) }) +} fun HashMap.get(key: String, default: String?): String? { return this[key] ?: default diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/EdziennikTask.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/EdziennikTask.kt index e1a830b5..aa316948 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/EdziennikTask.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/EdziennikTask.kt @@ -5,8 +5,8 @@ package pl.szczodrzynski.edziennik.data.api.edziennik import com.google.gson.JsonObject -import pl.szczodrzynski.edziennik.App -import pl.szczodrzynski.edziennik.R +import org.greenrobot.eventbus.EventBus +import pl.szczodrzynski.edziennik.* import pl.szczodrzynski.edziennik.data.api.* import pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik.Edudziennik import pl.szczodrzynski.edziennik.data.api.edziennik.idziennik.Idziennik @@ -15,9 +15,11 @@ import pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.Mobidziennik import pl.szczodrzynski.edziennik.data.api.edziennik.podlasie.Podlasie import pl.szczodrzynski.edziennik.data.api.edziennik.template.Template import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.Vulcan +import pl.szczodrzynski.edziennik.data.api.events.RegisterAvailabilityEvent import pl.szczodrzynski.edziennik.data.api.interfaces.EdziennikCallback import pl.szczodrzynski.edziennik.data.api.interfaces.EdziennikInterface 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.db.entity.LoginStore import pl.szczodrzynski.edziennik.data.db.entity.Profile @@ -88,6 +90,33 @@ open class EdziennikTask(override val profileId: Int, val request: Any) : IApiTa taskCallback.onCompleted() return } + + profile.registerName?.let { registerName -> + var status = app.config.sync.registerAvailability[registerName] + if (status == null || status.nextCheck < currentTimeUnix()) { + val api = SzkolnyApi(app) + api.runCatching({ + val availability = getRegisterAvailability() + app.config.sync.registerAvailability = availability + status = availability[registerName] + }, onError = { + taskCallback.onError(it.toApiError(TAG)) + return + }) + } + + if (status?.available != true + || status?.minVersionCode ?: BuildConfig.VERSION_CODE > BuildConfig.VERSION_CODE) { + if (EventBus.getDefault().hasSubscriberForEvent(RegisterAvailabilityEvent::class.java)) { + EventBus.getDefault().postSticky( + RegisterAvailabilityEvent(app.config.sync.registerAvailability) + ) + } + cancel() + taskCallback.onCompleted() + return + } + } } edziennikInterface = when (loginStore.type) { diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/events/RegisterAvailabilityEvent.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/events/RegisterAvailabilityEvent.kt new file mode 100644 index 00000000..fe66c4b3 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/events/RegisterAvailabilityEvent.kt @@ -0,0 +1,11 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2020-9-3. + */ + +package pl.szczodrzynski.edziennik.data.api.events + +import pl.szczodrzynski.edziennik.data.api.szkolny.response.RegisterAvailabilityStatus + +data class RegisterAvailabilityEvent( + val data: Map< String, RegisterAvailabilityStatus> +) 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 d2b4df70..042e63fe 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 @@ -12,12 +12,14 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.withContext import okhttp3.OkHttpClient +import org.greenrobot.eventbus.EventBus import pl.szczodrzynski.edziennik.* 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 import pl.szczodrzynski.edziennik.data.api.szkolny.request.* import pl.szczodrzynski.edziennik.data.api.szkolny.response.ApiResponse +import pl.szczodrzynski.edziennik.data.api.szkolny.response.RegisterAvailabilityStatus import pl.szczodrzynski.edziennik.data.api.szkolny.response.Update import pl.szczodrzynski.edziennik.data.api.szkolny.response.WebPushResponse import pl.szczodrzynski.edziennik.data.db.entity.Event @@ -112,6 +114,22 @@ class SzkolnyApi(val app: App) : CoroutineScope { */ @Throws(Exception::class) private inline fun parseResponse(response: Response>): T { + app.config.update = response.body()?.update?.let { update -> + if (update.versionCode > BuildConfig.VERSION_CODE) { + if (update.updateMandatory + && EventBus.getDefault().hasSubscriberForEvent(update::class.java)) { + EventBus.getDefault().postSticky(update) + } + update + } + else + null + } + + response.body()?.registerAvailability?.let { registerAvailability -> + app.config.sync.registerAvailability = registerAvailability + } + if (response.isSuccessful && response.body()?.success == true) { if (Unit is T) { return Unit @@ -341,4 +359,10 @@ class SzkolnyApi(val app: App) : CoroutineScope { val response = api.firebaseToken(registerName).execute() return parseResponse(response) } + + @Throws(Exception::class) + fun getRegisterAvailability(): Map { + val response = api.registerAvailability().execute() + return parseResponse(response) + } } 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 401d8886..6046e4da 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 @@ -38,4 +38,7 @@ interface SzkolnyService { @GET("firebase/token/{registerName}") fun firebaseToken(@Path("registerName") registerName: String): Call> + + @GET("registerAvailability") + fun registerAvailability(): Call>> } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/szkolny/response/ApiResponse.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/szkolny/response/ApiResponse.kt index ac56f53f..0987dcef 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/szkolny/response/ApiResponse.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/szkolny/response/ApiResponse.kt @@ -10,7 +10,10 @@ data class ApiResponse ( val errors: List? = null, - val data: T? = null + val data: T? = null, + + val update: Update? = null, + val registerAvailability: Map? = null ) { data class Error (val code: String, val reason: String) } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/szkolny/response/RegisterAvailabilityStatus.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/szkolny/response/RegisterAvailabilityStatus.kt new file mode 100644 index 00000000..0596a8ac --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/szkolny/response/RegisterAvailabilityStatus.kt @@ -0,0 +1,26 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2020-9-2. + */ + +package pl.szczodrzynski.edziennik.data.api.szkolny.response + +import pl.szczodrzynski.edziennik.BuildConfig +import pl.szczodrzynski.edziennik.DAY +import pl.szczodrzynski.edziennik.currentTimeUnix + +data class RegisterAvailabilityStatus( + val available: Boolean, + val name: String?, + val message: Message?, + val nextCheck: Long = currentTimeUnix() + 7 * DAY, + val minVersionCode: Int = BuildConfig.VERSION_CODE +) { + data class Message( + val title: String, + val contentShort: String, + val contentLong: String, + val icon: String?, + val image: String?, + val url: String? + ) +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/szkolny/response/UpdateResponse.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/szkolny/response/UpdateResponse.kt index a2028926..63a8ce9d 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/szkolny/response/UpdateResponse.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/szkolny/response/UpdateResponse.kt @@ -11,5 +11,6 @@ data class Update( val releaseNotes: String?, val releaseType: String, val isOnGooglePlay: Boolean, - val downloadUrl: String? -) \ No newline at end of file + val downloadUrl: String?, + val updateMandatory: Boolean +) diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/entity/Profile.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/entity/Profile.kt index 0dbb8502..997e845b 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/entity/Profile.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/entity/Profile.kt @@ -16,8 +16,7 @@ import androidx.room.Ignore import com.google.gson.JsonObject import pl.droidsonroids.gif.GifDrawable import pl.szczodrzynski.edziennik.* -import pl.szczodrzynski.edziennik.data.api.LOGIN_TYPE_EDUDZIENNIK -import pl.szczodrzynski.edziennik.data.api.LOGIN_TYPE_PODLASIE +import pl.szczodrzynski.edziennik.data.api.* import pl.szczodrzynski.edziennik.utils.ProfileImageHolder import pl.szczodrzynski.edziennik.utils.models.Date import pl.szczodrzynski.navlib.ImageHolder @@ -128,6 +127,17 @@ open class Profile( val isParent get() = accountName != null + val registerName + get() = when (loginStoreType) { + 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 -> null + } + override fun getImageDrawable(context: Context): Drawable { if (archived) { return context.getDrawableFromRes(pl.szczodrzynski.edziennik.R.drawable.profile_archived).also { diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/firebase/SzkolnyAppFirebase.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/firebase/SzkolnyAppFirebase.kt index 2ebb8c66..1d563c34 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/firebase/SzkolnyAppFirebase.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/firebase/SzkolnyAppFirebase.kt @@ -5,10 +5,13 @@ package pl.szczodrzynski.edziennik.data.firebase import com.google.gson.JsonParser +import com.google.gson.reflect.TypeToken import kotlinx.coroutines.* import org.greenrobot.eventbus.EventBus import pl.szczodrzynski.edziennik.* import pl.szczodrzynski.edziennik.data.api.events.FeedbackMessageEvent +import pl.szczodrzynski.edziennik.data.api.events.RegisterAvailabilityEvent +import pl.szczodrzynski.edziennik.data.api.szkolny.response.RegisterAvailabilityStatus import pl.szczodrzynski.edziennik.data.api.szkolny.response.Update import pl.szczodrzynski.edziennik.data.api.task.PostNotifications import pl.szczodrzynski.edziennik.data.db.entity.* @@ -50,6 +53,16 @@ class SzkolnyAppFirebase(val app: App, val profiles: List, val message: val message = app.gson.fromJson(message.data.getString("message"), FeedbackMessage::class.java) ?: return@launch feedbackMessage(message) } + "registerAvailability" -> launch { + val data = app.gson.fromJson>( + message.data.getString("registerAvailability"), + object: TypeToken>(){}.type + ) ?: return@launch + app.config.sync.registerAvailability = data + if (EventBus.getDefault().hasSubscriberForEvent(RegisterAvailabilityEvent::class.java)) { + EventBus.getDefault().postSticky(RegisterAvailabilityEvent(data)) + } + } } } } 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 377ab806..80e21e6b 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/sync/UpdateWorker.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/sync/UpdateWorker.kt @@ -14,6 +14,7 @@ import android.widget.Toast import androidx.core.app.NotificationCompat import androidx.work.* import kotlinx.coroutines.* +import org.greenrobot.eventbus.EventBus import pl.szczodrzynski.edziennik.* import pl.szczodrzynski.edziennik.R import pl.szczodrzynski.edziennik.data.api.szkolny.SzkolnyApi @@ -76,7 +77,7 @@ class UpdateWorker(val context: Context, val params: WorkerParameters) : Worker( try { val update = overrideUpdate ?: run { - val updates = withContext(Dispatchers.Default) { + withContext(Dispatchers.Default) { SzkolnyApi(app).runCatching({ getUpdate("beta") }, { @@ -84,15 +85,25 @@ class UpdateWorker(val context: Context, val params: WorkerParameters) : Worker( }) } ?: return@run null - if (updates.isEmpty()) { + if (app.config.update == null + || app.config.update?.versionCode ?: BuildConfig.VERSION_CODE <= BuildConfig.VERSION_CODE) { app.config.update = null Toast.makeText(app, app.getString(R.string.notification_no_update), Toast.LENGTH_SHORT).show() return@run null } - updates[0] + app.config.update } ?: return - app.config.update = update + if (update.versionCode <= BuildConfig.VERSION_CODE) { + app.config.update = null + return + } + + if (EventBus.getDefault().hasSubscriberForEvent(update::class.java)) { + if (!update.updateMandatory) // mandatory updates are posted by the SzkolnyApi + EventBus.getDefault().postSticky(update) + return + } val notificationIntent = Intent(app, UpdateDownloaderService::class.java) val pendingIntent = PendingIntent.getService(app, 0, notificationIntent, PendingIntent.FLAG_UPDATE_CURRENT) diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/RegisterUnavailableDialog.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/RegisterUnavailableDialog.kt new file mode 100644 index 00000000..de698c5a --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/RegisterUnavailableDialog.kt @@ -0,0 +1,93 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2020-9-3. + */ + +package pl.szczodrzynski.edziennik.ui.dialogs + +import android.view.LayoutInflater +import androidx.appcompat.app.AlertDialog +import androidx.appcompat.app.AppCompatActivity +import coil.api.load +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import pl.szczodrzynski.edziennik.App +import pl.szczodrzynski.edziennik.BuildConfig +import pl.szczodrzynski.edziennik.R +import pl.szczodrzynski.edziennik.data.api.szkolny.response.RegisterAvailabilityStatus +import pl.szczodrzynski.edziennik.databinding.DialogRegisterUnavailableBinding +import pl.szczodrzynski.edziennik.onClick +import pl.szczodrzynski.edziennik.utils.Utils +import kotlin.coroutines.CoroutineContext + +class RegisterUnavailableDialog( + val activity: AppCompatActivity, + val status: RegisterAvailabilityStatus, + val onShowListener: ((tag: String) -> Unit)? = null, + val onDismissListener: ((tag: String) -> Unit)? = null +) : CoroutineScope { + companion object { + private const val TAG = "RegisterUnavailableDialog" + } + + private lateinit var app: App + private lateinit var dialog: AlertDialog + + private val job = Job() + override val coroutineContext: CoroutineContext + get() = job + Dispatchers.Main + + init { run { + if (activity.isFinishing) + return@run + if (status.available && status.minVersionCode <= BuildConfig.VERSION_CODE) + return@run + onShowListener?.invoke(TAG) + app = activity.applicationContext as App + + if (!status.available && status.message != null) { + val b = DialogRegisterUnavailableBinding.inflate(LayoutInflater.from(activity), null, false) + b.message = status.message + if (status.message.image != null) + b.image.load(status.message.image) + if (status.message.url != null) { + b.readMore.onClick { + Utils.openUrl(activity, status.message.url) + } + } + dialog = MaterialAlertDialogBuilder(activity) + .setView(b.root) + .setPositiveButton(R.string.close) { dialog, _ -> + dialog.dismiss() + } + .setOnDismissListener { + onDismissListener?.invoke(TAG) + } + .show() + } + + val update = app.config.update + if (status.minVersionCode > BuildConfig.VERSION_CODE) { + if (update != null && update.versionCode >= status.minVersionCode) { + UpdateAvailableDialog(activity, update, true, onShowListener, onDismissListener) + } + else { + // this *should* never happen + dialog = MaterialAlertDialogBuilder(activity) + .setTitle(R.string.update_available_title) + .setMessage(R.string.update_available_fallback) + .setPositiveButton(R.string.update_available_button) { dialog, _ -> + Utils.openGooglePlay(activity) + dialog.dismiss() + } + .setCancelable(false) + .setOnDismissListener { + onDismissListener?.invoke(TAG) + } + .show() + } + return@run + } + }} +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/ServerMessageDialog.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/ServerMessageDialog.kt index 27d11dad..cebc4e04 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/ServerMessageDialog.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/ServerMessageDialog.kt @@ -17,7 +17,7 @@ import kotlin.coroutines.CoroutineContext class ServerMessageDialog( val activity: AppCompatActivity, val title: String, - val message: String, + val message: CharSequence, val onShowListener: ((tag: String) -> Unit)? = null, val onDismissListener: ((tag: String) -> Unit)? = null ) : CoroutineScope { diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/UpdateAvailableDialog.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/UpdateAvailableDialog.kt new file mode 100644 index 00000000..6446170b --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/UpdateAvailableDialog.kt @@ -0,0 +1,67 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2020-9-3. + */ + +package pl.szczodrzynski.edziennik.ui.dialogs + +import android.text.Html +import androidx.appcompat.app.AlertDialog +import androidx.appcompat.app.AppCompatActivity +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import pl.szczodrzynski.edziennik.* +import pl.szczodrzynski.edziennik.data.api.szkolny.response.Update +import pl.szczodrzynski.edziennik.sync.UpdateDownloaderService +import kotlin.coroutines.CoroutineContext + +class UpdateAvailableDialog( + val activity: AppCompatActivity, + val update: Update, + val mandatory: Boolean = update.updateMandatory, + val onShowListener: ((tag: String) -> Unit)? = null, + val onDismissListener: ((tag: String) -> Unit)? = null +) : CoroutineScope { + companion object { + private const val TAG = "UpdateAvailableDialog" + } + + private lateinit var app: App + private lateinit var dialog: AlertDialog + + private val job = Job() + override val coroutineContext: CoroutineContext + get() = job + Dispatchers.Main + + init { run { + if (activity.isFinishing) + return@run + if (update.versionCode <= BuildConfig.VERSION_CODE) + return@run + onShowListener?.invoke(TAG) + app = activity.applicationContext as App + + dialog = MaterialAlertDialogBuilder(activity) + .setTitle(R.string.update_available_title) + .setMessage( + R.string.update_available_format, + BuildConfig.VERSION_NAME, + update.versionName, + update.releaseNotes?.let { Html.fromHtml(it) } ?: "---" + ) + .setPositiveButton(R.string.update_available_button) { dialog, _ -> + activity.startService(Intent(app, UpdateDownloaderService::class.java)) + dialog.dismiss() + } + .also { + if (!mandatory) + it.setNeutralButton(R.string.update_available_later, null) + } + .setCancelable(!mandatory) + .setOnDismissListener { + onDismissListener?.invoke(TAG) + } + .show() + }} +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/home/CardItemTouchHelperCallback.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/home/CardItemTouchHelperCallback.kt index 34621474..7cfb3c2f 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/home/CardItemTouchHelperCallback.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/home/CardItemTouchHelperCallback.kt @@ -33,9 +33,10 @@ class CardItemTouchHelperCallback(private val cardAdapter: HomeCardAdapter, priv } override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) { - removeCard(viewHolder.adapterPosition, cardAdapter) - cardAdapter.items.removeAt(viewHolder.adapterPosition) - cardAdapter.notifyItemRemoved(viewHolder.adapterPosition) + val position = viewHolder.adapterPosition + removeCard(position, cardAdapter) + cardAdapter.items.removeAt(position) + cardAdapter.notifyItemRemoved(position) } override fun onSelectedChanged(viewHolder: RecyclerView.ViewHolder?, actionState: Int) { diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/home/HomeFragment.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/home/HomeFragment.kt index 8d276a82..1b53e826 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/home/HomeFragment.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/home/HomeFragment.kt @@ -21,12 +21,9 @@ import androidx.recyclerview.widget.RecyclerViewAccessibilityDelegate import com.mikepenz.iconics.typeface.library.community.material.CommunityMaterial.Icon import com.mikepenz.iconics.typeface.library.szkolny.font.SzkolnyFont import kotlinx.coroutines.* -import pl.szczodrzynski.edziennik.App -import pl.szczodrzynski.edziennik.MainActivity -import pl.szczodrzynski.edziennik.R +import pl.szczodrzynski.edziennik.* import pl.szczodrzynski.edziennik.data.db.entity.LoginStore import pl.szczodrzynski.edziennik.databinding.FragmentHomeBinding -import pl.szczodrzynski.edziennik.onClick import pl.szczodrzynski.edziennik.ui.dialogs.home.StudentNumberDialog import pl.szczodrzynski.edziennik.ui.modules.home.cards.* import pl.szczodrzynski.edziennik.utils.Themes @@ -50,9 +47,11 @@ class HomeFragment : Fragment(), CoroutineScope { cardAdapter.notifyItemMoved(fromPosition, toPosition) val homeCards = App.config.forProfile().ui.homeCards.toMutableList() - val fromPair = homeCards[fromPosition] - homeCards[fromPosition] = homeCards[toPosition] - homeCards[toPosition] = fromPair + val fromIndex = homeCards.indexOfFirst { it.cardId == fromCard.id } + val toIndex = homeCards.indexOfFirst { it.cardId == toCard.id } + val fromPair = homeCards[fromIndex] + homeCards[fromIndex] = homeCards[toIndex] + homeCards[toIndex] = fromPair App.config.forProfile().ui.homeCards = homeCards return true } @@ -64,10 +63,10 @@ class HomeFragment : Fragment(), CoroutineScope { val card = cardAdapter.items[position] if (card.id >= 100) { // debug & archive cards are not removable - cardAdapter.notifyDataSetChanged() + //cardAdapter.notifyDataSetChanged() return } - homeCards.removeAt(position) + homeCards.removeAll { it.cardId == card.id } App.config.forProfile().ui.homeCards = homeCards } } @@ -166,6 +165,13 @@ class HomeFragment : Fragment(), CoroutineScope { if (app.profile.archived) items.add(0, HomeArchiveCard(101, app, activity, this, app.profile)) + val status = app.config.sync.registerAvailability[app.profile.registerName] + val update = app.config.update + if (update != null && update.versionCode > BuildConfig.VERSION_CODE + || status != null && (!status.available || status.minVersionCode > BuildConfig.VERSION_CODE)) { + items.add(0, HomeAvailabilityCard(102, app, activity, this, app.profile)) + } + val adapter = HomeCardAdapter(items) val itemTouchHelper = ItemTouchHelper(CardItemTouchHelperCallback(adapter, b.refreshLayout)) adapter.itemTouchHelper = itemTouchHelper diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/home/cards/HomeAvailabilityCard.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/home/cards/HomeAvailabilityCard.kt new file mode 100644 index 00000000..26c8d609 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/home/cards/HomeAvailabilityCard.kt @@ -0,0 +1,92 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2020-9-3. + */ + +package pl.szczodrzynski.edziennik.ui.modules.home.cards + +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.FrameLayout +import androidx.core.view.isVisible +import androidx.core.view.plusAssign +import androidx.core.view.setMargins +import coil.api.load +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import pl.szczodrzynski.edziennik.* +import pl.szczodrzynski.edziennik.data.db.entity.Profile +import pl.szczodrzynski.edziennik.databinding.CardHomeAvailabilityBinding +import pl.szczodrzynski.edziennik.sync.UpdateDownloaderService +import pl.szczodrzynski.edziennik.ui.dialogs.RegisterUnavailableDialog +import pl.szczodrzynski.edziennik.ui.dialogs.UpdateAvailableDialog +import pl.szczodrzynski.edziennik.ui.modules.home.HomeCard +import pl.szczodrzynski.edziennik.ui.modules.home.HomeCardAdapter +import pl.szczodrzynski.edziennik.ui.modules.home.HomeFragment +import kotlin.coroutines.CoroutineContext + +class HomeAvailabilityCard( + override val id: Int, + val app: App, + val activity: MainActivity, + val fragment: HomeFragment, + val profile: Profile +) : HomeCard, CoroutineScope { + companion object { + private const val TAG = "HomeAvailabilityCard" + } + + private var job: Job = Job() + override val coroutineContext: CoroutineContext + get() = job + Dispatchers.Main + + override fun bind(position: Int, holder: HomeCardAdapter.ViewHolder) { + holder.root.removeAllViews() + val b = CardHomeAvailabilityBinding.inflate(LayoutInflater.from(holder.root.context)) + b.root.layoutParams = FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT).apply { + setMargins(8.dp) + } + holder.root += b.root + + val status = app.config.sync.registerAvailability[profile.registerName] + val update = app.config.update + + if (update == null && status == null) + return + + var onInfoClick = { _: View -> } + + if (status != null && !status.available && status.message != null) { + b.homeAvailabilityTitle.text = status.message.title + b.homeAvailabilityText.text = status.message.contentShort + b.homeAvailabilityUpdate.isVisible = false + b.homeAvailabilityIcon.setImageResource(R.drawable.ic_sync) + if (status.message.icon != null) + b.homeAvailabilityIcon.load(status.message.icon) + onInfoClick = { + RegisterUnavailableDialog(activity, status) + } + } + else if (update != null && update.versionCode > BuildConfig.VERSION_CODE) { + b.homeAvailabilityTitle.setText(R.string.home_availability_title) + b.homeAvailabilityText.setText(R.string.home_availability_text, update.versionName) + b.homeAvailabilityUpdate.isVisible = true + b.homeAvailabilityIcon.setImageResource(R.drawable.ic_update) + onInfoClick = { + UpdateAvailableDialog(activity, update) + } + } + + b.homeAvailabilityUpdate.onClick { + if (update == null) + return@onClick + activity.startService(Intent(app, UpdateDownloaderService::class.java)) + } + + b.homeAvailabilityInfo.onClick(onInfoClick) + holder.root.onClick(onInfoClick) + } + + override fun unbind(position: Int, holder: HomeCardAdapter.ViewHolder) = Unit +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/login/LoginChooserFragment.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/login/LoginChooserFragment.kt index c25b07d1..90945294 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/login/LoginChooserFragment.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/login/LoginChooserFragment.kt @@ -13,14 +13,12 @@ import android.view.ViewGroup import androidx.core.view.isVisible import androidx.fragment.app.Fragment import androidx.recyclerview.widget.LinearLayoutManager -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.Job -import pl.szczodrzynski.edziennik.App -import pl.szczodrzynski.edziennik.Bundle -import pl.szczodrzynski.edziennik.R +import kotlinx.coroutines.* +import pl.szczodrzynski.edziennik.* +import pl.szczodrzynski.edziennik.data.api.* +import pl.szczodrzynski.edziennik.data.api.szkolny.SzkolnyApi import pl.szczodrzynski.edziennik.databinding.LoginChooserFragmentBinding -import pl.szczodrzynski.edziennik.onClick +import pl.szczodrzynski.edziennik.ui.dialogs.RegisterUnavailableDialog import pl.szczodrzynski.edziennik.ui.modules.feedback.FeedbackActivity import pl.szczodrzynski.edziennik.utils.SimpleDividerItemDecoration import kotlin.coroutines.CoroutineContext @@ -53,18 +51,23 @@ class LoginChooserFragment : Fragment(), CoroutineScope { if (!isAdded) return val adapter = LoginChooserAdapter(activity) { loginType, loginMode -> - if (loginMode.isPlatformSelection) { - nav.navigate(R.id.loginPlatformListFragment, Bundle( + launch { + if (!checkAvailability(loginType.loginType)) + return@launch + + if (loginMode.isPlatformSelection) { + nav.navigate(R.id.loginPlatformListFragment, Bundle( + "loginType" to loginType.loginType, + "loginMode" to loginMode.loginMode + ), activity.navOptions) + return@launch + } + + nav.navigate(R.id.loginFormFragment, Bundle( "loginType" to loginType.loginType, "loginMode" to loginMode.loginMode ), activity.navOptions) - return@LoginChooserAdapter } - - nav.navigate(R.id.loginFormFragment, Bundle( - "loginType" to loginType.loginType, - "loginMode" to loginMode.loginMode - ), activity.navOptions) } LoginInfo.chooserList = LoginInfo.chooserList @@ -102,4 +105,35 @@ class LoginChooserFragment : Fragment(), CoroutineScope { } } } + + private suspend fun checkAvailability(loginType: Int): Boolean { + 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 -> null + }?.let { registerName -> + var status = app.config.sync.registerAvailability[registerName] + if (status == null || status.nextCheck < currentTimeUnix()) { + withContext(Dispatchers.IO) { + val api = SzkolnyApi(app) + api.runCatching(activity) { + val availability = getRegisterAvailability() + app.config.sync.registerAvailability = availability + status = availability[registerName] + } + } + } + + if (status?.available != true) { + if (status != null) + RegisterUnavailableDialog(activity, status!!) + return false + } + } + return true + } } diff --git a/app/src/main/res/drawable/ic_update.xml b/app/src/main/res/drawable/ic_update.xml new file mode 100644 index 00000000..e059b093 --- /dev/null +++ b/app/src/main/res/drawable/ic_update.xml @@ -0,0 +1,11 @@ + + + + + + + + diff --git a/app/src/main/res/layout/card_home_availability.xml b/app/src/main/res/layout/card_home_availability.xml new file mode 100644 index 00000000..9b4f67f5 --- /dev/null +++ b/app/src/main/res/layout/card_home_availability.xml @@ -0,0 +1,65 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/dialog_register_unavailable.xml b/app/src/main/res/layout/dialog_register_unavailable.xml new file mode 100644 index 00000000..2d395494 --- /dev/null +++ b/app/src/main/res/layout/dialog_register_unavailable.xml @@ -0,0 +1,72 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 60874690..d3f1ba4e 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1368,4 +1368,14 @@ Brak aktualnego profilu Uczeń %s nie posiada profilu na tym koncie w aktualnym roku szkolnym. Prawdopodobnie ten profil został usunięty lub uczeń nie uczęszcza już do tej klasy.\n\nAby przejść do aktualnego profilu, wybierz ucznia z listy lub zaloguj się na jego konto przyciskiem Dodaj ucznia. Znaki towarowe zamieszczone w tej aplikacji należą do ich prawowitych właścicieli i są używane wyłącznie w celach informacyjnych. + Dostępna aktualiacja aplikacji + Używasz starej wersji aplikacji Szkolny.eu (%s). Aby móc korzystać z aplikacji oraz zapewnić najlepsze działanie, zaktualizuj aplikację do wersji %s.\n\nDziennik zmian:\n%s + Posiadasz nieaktualną wersję aplikacji Szkolny.eu. Aby móc dalej synchronizować dane, musisz zaktualizować aplikację. + Aktualizuj + Nie teraz + Dostępna aktualizacja + Zaktualizuj aplikację do najnowszej wersji %s. + Zobacz więcej + Aktualizuj + Dowiedz się więcej