diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/App.kt b/app/src/main/java/pl/szczodrzynski/edziennik/App.kt index 98bfbce3..283d83ef 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/App.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/App.kt @@ -28,7 +28,11 @@ import com.google.gson.Gson import com.hypertrack.hyperlog.HyperLog import com.mikepenz.iconics.Iconics import im.wangchao.mhttp.MHttp -import kotlinx.coroutines.* +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import me.leolin.shortcutbadger.ShortcutBadger import okhttp3.OkHttpClient import org.greenrobot.eventbus.EventBus @@ -55,7 +59,19 @@ import pl.szczodrzynski.edziennik.utils.PermissionChecker import pl.szczodrzynski.edziennik.utils.Themes import pl.szczodrzynski.edziennik.utils.Utils import pl.szczodrzynski.edziennik.utils.Utils.d -import pl.szczodrzynski.edziennik.utils.managers.* +import pl.szczodrzynski.edziennik.utils.managers.AttendanceManager +import pl.szczodrzynski.edziennik.utils.managers.AvailabilityManager +import pl.szczodrzynski.edziennik.utils.managers.BuildManager +import pl.szczodrzynski.edziennik.utils.managers.EventManager +import pl.szczodrzynski.edziennik.utils.managers.GradesManager +import pl.szczodrzynski.edziennik.utils.managers.MessageManager +import pl.szczodrzynski.edziennik.utils.managers.NoteManager +import pl.szczodrzynski.edziennik.utils.managers.NotificationChannelsManager +import pl.szczodrzynski.edziennik.utils.managers.PermissionManager +import pl.szczodrzynski.edziennik.utils.managers.TextStylingManager +import pl.szczodrzynski.edziennik.utils.managers.TimetableManager +import pl.szczodrzynski.edziennik.utils.managers.UpdateManager +import pl.szczodrzynski.edziennik.utils.managers.UserActionManager import java.util.concurrent.TimeUnit import kotlin.coroutines.CoroutineContext import kotlin.system.exitProcess @@ -80,18 +96,19 @@ class App : MultiDexApplication(), Configuration.Provider, CoroutineScope { } val api by lazy { SzkolnyApi(this) } - val notificationChannelsManager by lazy { NotificationChannelsManager(this) } - val userActionManager by lazy { UserActionManager(this) } - val gradesManager by lazy { GradesManager(this) } - val timetableManager by lazy { TimetableManager(this) } - val eventManager by lazy { EventManager(this) } - val permissionManager by lazy { PermissionManager(this) } val attendanceManager by lazy { AttendanceManager(this) } - val buildManager by lazy { BuildManager(this) } val availabilityManager by lazy { AvailabilityManager(this) } - val textStylingManager by lazy { TextStylingManager(this) } + val buildManager by lazy { BuildManager(this) } + val eventManager by lazy { EventManager(this) } + val gradesManager by lazy { GradesManager(this) } val messageManager by lazy { MessageManager(this) } val noteManager by lazy { NoteManager(this) } + val notificationChannelsManager by lazy { NotificationChannelsManager(this) } + val permissionManager by lazy { PermissionManager(this) } + val textStylingManager by lazy { TextStylingManager(this) } + val timetableManager by lazy { TimetableManager(this) } + val updateManager by lazy { UpdateManager(this) } + val userActionManager by lazy { UserActionManager(this) } val db get() = App.db diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/MainActivity.kt b/app/src/main/java/pl/szczodrzynski/edziennik/MainActivity.kt index 2a819c7b..ce39bc77 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/MainActivity.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/MainActivity.kt @@ -46,6 +46,7 @@ import pl.szczodrzynski.edziennik.databinding.ActivitySzkolnyBinding import pl.szczodrzynski.edziennik.ext.* import pl.szczodrzynski.edziennik.sync.AppManagerDetectedEvent import pl.szczodrzynski.edziennik.sync.SyncWorker +import pl.szczodrzynski.edziennik.sync.UpdateStateEvent import pl.szczodrzynski.edziennik.sync.UpdateWorker import pl.szczodrzynski.edziennik.ui.base.MainSnackbar import pl.szczodrzynski.edziennik.ui.base.enums.NavTarget @@ -56,6 +57,7 @@ import pl.szczodrzynski.edziennik.ui.dialogs.sync.RegisterUnavailableDialog import pl.szczodrzynski.edziennik.ui.dialogs.sync.ServerMessageDialog import pl.szczodrzynski.edziennik.ui.dialogs.sync.SyncViewListDialog import pl.szczodrzynski.edziennik.ui.dialogs.sync.UpdateAvailableDialog +import pl.szczodrzynski.edziennik.ui.dialogs.sync.UpdateProgressDialog import pl.szczodrzynski.edziennik.ui.error.ErrorDetailsDialog import pl.szczodrzynski.edziennik.ui.error.ErrorSnackbar import pl.szczodrzynski.edziennik.ui.event.EventManualDialog @@ -536,6 +538,14 @@ class MainActivity : AppCompatActivity(), CoroutineScope { UpdateAvailableDialog(this, event).show() } + @Subscribe(threadMode = ThreadMode.MAIN, sticky = true) + fun onUpdateStateEvent(event: UpdateStateEvent) { + if (!event.running) + return + EventBus.getDefault().removeStickyEvent(event) + UpdateProgressDialog(this, event.update ?: return, event.downloadId).show() + } + @Subscribe(threadMode = ThreadMode.MAIN, sticky = true) fun onRegisterAvailabilityEvent(event: RegisterAvailabilityEvent) { EventBus.getDefault().removeStickyEvent(event) @@ -699,6 +709,10 @@ class MainActivity : AppCompatActivity(), CoroutineScope { if (extras?.containsKey("action") == true) { val handled = when (extras.getString("action")) { + "updateRequest" -> { + UpdateAvailableDialog(this, app.config.update).show() + true + } "serverMessage" -> { ServerMessageDialog( this, 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 43e705dc..d7ce1f80 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 @@ -13,7 +13,6 @@ 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.ERROR_API_INVALID_SIGNATURE import pl.szczodrzynski.edziennik.data.api.szkolny.adapter.DateAdapter @@ -128,16 +127,10 @@ class SzkolnyApi(val app: App) : CoroutineScope { response: Response>, updateDeviceHash: Boolean = false, ): 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()?.update?.let { update -> + // do not process "null" update, as it might not mean there's no update + // do not notify; silently check and show the home update card + app.updateManager.process(update, notify = false) } response.body()?.registerAvailability?.let { registerAvailability -> @@ -431,8 +424,8 @@ class SzkolnyApi(val app: App) : CoroutineScope { } @Throws(Exception::class) - fun getUpdate(channel: String): List { - val response = api.updates(channel).execute() + fun getUpdate(channel: Update.Type): List { + val response = api.updates(channel.name.lowercase()).execute() return parseResponse(response) } 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 63a8ce9d..0b0433f3 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 @@ -5,12 +5,21 @@ package pl.szczodrzynski.edziennik.data.api.szkolny.response data class Update( - val versionCode: Int, - val versionName: String, - val releaseDate: String, - val releaseNotes: String?, - val releaseType: String, - val isOnGooglePlay: Boolean, - val downloadUrl: String?, - val updateMandatory: Boolean -) + val versionCode: Int, + val versionName: String, + val releaseDate: String, + val releaseNotes: String?, + val releaseType: String, + val isOnGooglePlay: Boolean, + val downloadUrl: String?, + val updateMandatory: Boolean, +) { + + enum class Type { + NIGHTLY, + DEV, + BETA, + RC, + RELEASE, + } +} 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 9c1a248f..9d3a1a9e 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 @@ -6,7 +6,11 @@ package pl.szczodrzynski.edziennik.data.firebase import com.google.gson.JsonParser import com.google.gson.reflect.TypeToken -import kotlinx.coroutines.* +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import org.greenrobot.eventbus.EventBus import pl.szczodrzynski.edziennik.App import pl.szczodrzynski.edziennik.data.api.events.FeedbackMessageEvent @@ -14,14 +18,18 @@ 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.* +import pl.szczodrzynski.edziennik.data.db.entity.Event +import pl.szczodrzynski.edziennik.data.db.entity.FeedbackMessage +import pl.szczodrzynski.edziennik.data.db.entity.Metadata +import pl.szczodrzynski.edziennik.data.db.entity.Note +import pl.szczodrzynski.edziennik.data.db.entity.Notification +import pl.szczodrzynski.edziennik.data.db.entity.Profile import pl.szczodrzynski.edziennik.data.db.enums.MetadataType import pl.szczodrzynski.edziennik.data.db.enums.NotificationType import pl.szczodrzynski.edziennik.ext.getInt import pl.szczodrzynski.edziennik.ext.getLong import pl.szczodrzynski.edziennik.ext.getString import pl.szczodrzynski.edziennik.ext.resolveString -import pl.szczodrzynski.edziennik.sync.UpdateWorker import pl.szczodrzynski.edziennik.ui.base.enums.NavTarget import pl.szczodrzynski.edziennik.utils.models.Date import pl.szczodrzynski.edziennik.utils.models.Time @@ -64,7 +72,10 @@ class SzkolnyAppFirebase(val app: App, val profiles: List, val message: message.data.getString("title") ?: "", message.data.getString("message") ?: "" ) - "appUpdate" -> launch { UpdateWorker.runNow(app, app.gson.fromJson(message.data.getString("update"), Update::class.java)) } + "appUpdate" -> { + val update = app.gson.fromJson(message.data.getString("update"), Update::class.java) + app.updateManager.process(update, notify = true) + } "feedbackMessage" -> launch { val message = app.gson.fromJson(message.data.getString("message"), FeedbackMessage::class.java) ?: return@launch feedbackMessage(message) diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/sync/UpdateStateEvent.kt b/app/src/main/java/pl/szczodrzynski/edziennik/sync/UpdateStateEvent.kt new file mode 100644 index 00000000..878efdb8 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/sync/UpdateStateEvent.kt @@ -0,0 +1,9 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2022-10-22. + */ + +package pl.szczodrzynski.edziennik.sync + +import pl.szczodrzynski.edziennik.data.api.szkolny.response.Update + +class UpdateStateEvent(val running: Boolean, val update: Update?, val downloadId: Long) 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 0df0bc83..3d22e044 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/sync/UpdateWorker.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/sync/UpdateWorker.kt @@ -5,25 +5,14 @@ package pl.szczodrzynski.edziennik.sync import android.annotation.SuppressLint -import android.app.NotificationManager -import android.app.PendingIntent import android.content.Context -import android.content.Intent -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 import pl.szczodrzynski.edziennik.data.api.szkolny.response.Update import pl.szczodrzynski.edziennik.ext.DAY -import pl.szczodrzynski.edziennik.ext.concat import pl.szczodrzynski.edziennik.ext.formatDate -import pl.szczodrzynski.edziennik.ext.pendingIntentFlag import pl.szczodrzynski.edziennik.utils.Utils -import pl.szczodrzynski.edziennik.utils.html.BetterHtml import java.util.concurrent.TimeUnit import kotlin.coroutines.CoroutineContext @@ -76,63 +65,6 @@ class UpdateWorker(val context: Context, val params: WorkerParameters) : Worker( Utils.d(TAG, "Cancelling work by tag $TAG") WorkManager.getInstance(app).cancelAllWorkByTag(TAG) } - - suspend fun runNow(app: App, overrideUpdate: Update? = null) { - try { - val update = overrideUpdate - ?: run { - withContext(Dispatchers.Default) { - SzkolnyApi(app).runCatching({ - getUpdate("beta") - }, { - Toast.makeText(app, app.getString(R.string.notification_cant_check_update), Toast.LENGTH_SHORT).show() - }) - } ?: return@run null - - if (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 - } - app.config.update - } ?: return - - 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 or pendingIntentFlag()) - val notification = NotificationCompat.Builder(app, app.notificationChannelsManager.updates.key) - .setContentTitle(app.getString(R.string.notification_updates_title)) - .setContentText(app.getString(R.string.notification_updates_text, update.versionName)) - .setTicker(app.getString(R.string.notification_updates_summary)) - .setSmallIcon(R.drawable.ic_notification) - .setStyle(NotificationCompat.BigTextStyle() - .bigText(listOf( - app.getString(R.string.notification_updates_text, update.versionName), - update.releaseNotes?.let { BetterHtml.fromHtml(context = null, it) } - ).concat("\n"))) - .setColor(0xff2196f3.toInt()) - .setLights(0xFF00FFFF.toInt(), 2000, 2000) - .setPriority(NotificationCompat.PRIORITY_HIGH) - .setDefaults(NotificationCompat.DEFAULT_ALL) - .setGroup(app.notificationChannelsManager.updates.key) - .setContentIntent(pendingIntent) - .setAutoCancel(false) - .build() - (app.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager).notify(app.notificationChannelsManager.updates.id, notification) - - } catch (ignore: Exception) { } - } } private val job = Job() @@ -146,22 +78,13 @@ class UpdateWorker(val context: Context, val params: WorkerParameters) : Worker( return Result.success() } - launch { - runNow(app) - } + val channel = if (App.devMode) + Update.Type.BETA + else + Update.Type.RELEASE + app.updateManager.checkNowSync(channel, notify = true) rescheduleNext(this.context) return Result.success() } - - class JavaWrapper(app: App) : CoroutineScope { - private val job = Job() - override val coroutineContext: CoroutineContext - get() = job + Dispatchers.Main - init { - launch { - runNow(app) - } - } - } } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/sync/UpdateAvailableDialog.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/sync/UpdateAvailableDialog.kt index 733febd8..c6766ece 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/sync/UpdateAvailableDialog.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/sync/UpdateAvailableDialog.kt @@ -43,11 +43,11 @@ class UpdateAvailableDialog( override suspend fun onShow() = Unit override suspend fun onPositiveClick(): Boolean { - if (update == null) + if (update == null || update.isOnGooglePlay) Utils.openGooglePlay(activity) else activity.startService(Intent(app, UpdateDownloaderService::class.java)) - return NO_DISMISS + return DISMISS } override suspend fun onBeforeShow(): Boolean { diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/sync/UpdateProgressDialog.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/sync/UpdateProgressDialog.kt new file mode 100644 index 00000000..68bd8d54 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/sync/UpdateProgressDialog.kt @@ -0,0 +1,87 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2022-10-22. + */ + +package pl.szczodrzynski.edziennik.ui.dialogs.sync + +import android.app.DownloadManager +import android.database.CursorIndexOutOfBoundsException +import android.view.LayoutInflater +import androidx.appcompat.app.AppCompatActivity +import androidx.core.content.getSystemService +import kotlinx.coroutines.Job +import org.greenrobot.eventbus.EventBus +import org.greenrobot.eventbus.Subscribe +import org.greenrobot.eventbus.ThreadMode +import pl.szczodrzynski.edziennik.R +import pl.szczodrzynski.edziennik.data.api.szkolny.response.Update +import pl.szczodrzynski.edziennik.databinding.UpdateProgressDialogBinding +import pl.szczodrzynski.edziennik.ext.getInt +import pl.szczodrzynski.edziennik.ext.startCoroutineTimer +import pl.szczodrzynski.edziennik.sync.UpdateStateEvent +import pl.szczodrzynski.edziennik.ui.dialogs.base.BindingDialog +import pl.szczodrzynski.edziennik.utils.Utils + +class UpdateProgressDialog( + activity: AppCompatActivity, + private val update: Update, + private val downloadId: Long, + onShowListener: ((tag: String) -> Unit)? = null, + onDismissListener: ((tag: String) -> Unit)? = null, +) : BindingDialog(activity, onShowListener, onDismissListener) { + + override val TAG = "UpdateProgressDialog" + + override fun getTitleRes() = R.string.notification_downloading_update + override fun inflate(layoutInflater: LayoutInflater) = + UpdateProgressDialogBinding.inflate(layoutInflater) + + override fun isCancelable() = false + override fun getNegativeButtonText() = R.string.cancel + + private var timerJob: Job? = null + + override suspend fun onShow() { + EventBus.getDefault().register(this) + b.update = update + b.progress.progress = 0 + + val downloadManager = app.getSystemService() ?: return + val query = DownloadManager.Query().setFilterById(downloadId) + + timerJob?.cancel() + timerJob = startCoroutineTimer(repeatMillis = 100L) { + try { + val cursor = downloadManager.query(query) + cursor.moveToFirst() + val progress = cursor.getInt(DownloadManager.COLUMN_BYTES_DOWNLOADED_SO_FAR) + ?.toFloat() ?: return@startCoroutineTimer + b.downloadedSize.text = Utils.readableFileSize(progress.toLong()) + val total = cursor.getInt(DownloadManager.COLUMN_TOTAL_SIZE_BYTES) + ?.toFloat() ?: return@startCoroutineTimer + b.totalSize.text = Utils.readableFileSize(total.toLong()) + b.progress.progress = (progress / total * 100.0f).toInt() + } catch (_: CursorIndexOutOfBoundsException) {} + } + } + + override fun onDismiss() { + EventBus.getDefault().unregister(this) + timerJob?.cancel() + } + + override suspend fun onNegativeClick(): Boolean { + val downloadManager = app.getSystemService() ?: return NO_DISMISS + downloadManager.remove(downloadId) + return DISMISS + } + + @Subscribe(threadMode = ThreadMode.MAIN, sticky = true) + fun onUpdateStateEvent(event: UpdateStateEvent) { + if (event.downloadId != downloadId) + return + EventBus.getDefault().removeStickyEvent(event) + if (!event.running) + dismiss() + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/home/cards/HomeAvailabilityCard.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/home/cards/HomeAvailabilityCard.kt index 9960b305..4bb264d0 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/home/cards/HomeAvailabilityCard.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/home/cards/HomeAvailabilityCard.kt @@ -15,7 +15,10 @@ import coil.load import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job -import pl.szczodrzynski.edziennik.* +import pl.szczodrzynski.edziennik.App +import pl.szczodrzynski.edziennik.BuildConfig +import pl.szczodrzynski.edziennik.MainActivity +import pl.szczodrzynski.edziennik.R import pl.szczodrzynski.edziennik.data.db.entity.Profile import pl.szczodrzynski.edziennik.databinding.CardHomeAvailabilityBinding import pl.szczodrzynski.edziennik.ext.Intent @@ -28,6 +31,7 @@ import pl.szczodrzynski.edziennik.ui.dialogs.sync.UpdateAvailableDialog import pl.szczodrzynski.edziennik.ui.home.HomeCard import pl.szczodrzynski.edziennik.ui.home.HomeCardAdapter import pl.szczodrzynski.edziennik.ui.home.HomeFragment +import pl.szczodrzynski.edziennik.utils.Utils import pl.szczodrzynski.edziennik.utils.html.BetterHtml import kotlin.coroutines.CoroutineContext @@ -79,7 +83,7 @@ class HomeAvailabilityCard( 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.homeAvailabilityUpdate.isVisible = !app.buildManager.isPlayRelease b.homeAvailabilityIcon.setImageResource(R.drawable.ic_update) onInfoClick = { UpdateAvailableDialog(activity, update).show() @@ -92,7 +96,10 @@ class HomeAvailabilityCard( b.homeAvailabilityUpdate.onClick { if (update == null) return@onClick - activity.startService(Intent(app, UpdateDownloaderService::class.java)) + if (update.isOnGooglePlay) + Utils.openGooglePlay(activity) + else + activity.startService(Intent(app, UpdateDownloaderService::class.java)) } b.homeAvailabilityInfo.onClick(onInfoClick) diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/settings/cards/SettingsAboutCard.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/settings/cards/SettingsAboutCard.kt index 88ecab05..b06393d3 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/settings/cards/SettingsAboutCard.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/settings/cards/SettingsAboutCard.kt @@ -18,8 +18,8 @@ import kotlinx.coroutines.launch import pl.szczodrzynski.edziennik.App import pl.szczodrzynski.edziennik.BuildConfig import pl.szczodrzynski.edziennik.R +import pl.szczodrzynski.edziennik.data.api.szkolny.response.Update import pl.szczodrzynski.edziennik.ext.after -import pl.szczodrzynski.edziennik.sync.UpdateWorker import pl.szczodrzynski.edziennik.ui.dialogs.ChangelogDialog import pl.szczodrzynski.edziennik.ui.settings.SettingsCard import pl.szczodrzynski.edziennik.ui.settings.SettingsLicenseActivity @@ -113,7 +113,17 @@ class SettingsAboutCard(util: SettingsUtil) : SettingsCard(util), CoroutineScope icon = CommunityMaterial.Icon3.cmd_update ) { launch { - UpdateWorker.runNow(app) + val channel = if (App.devMode) + Update.Type.BETA + else + Update.Type.RC + val result = app.updateManager.checkNow(channel, notify = false) + val update = result.getOrNull() + // the dialog is shown by MainActivity (EventBus) + when { + result.isFailure -> Toast.makeText(app, app.getString(R.string.notification_cant_check_update), Toast.LENGTH_SHORT).show() + update == null -> Toast.makeText(app, app.getString(R.string.notification_no_update), Toast.LENGTH_SHORT).show() + } } } )), diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/utils/managers/BuildManager.kt b/app/src/main/java/pl/szczodrzynski/edziennik/utils/managers/BuildManager.kt index 1c1e2dbe..2cbf4f23 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/utils/managers/BuildManager.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/utils/managers/BuildManager.kt @@ -9,11 +9,29 @@ import android.text.TextUtils import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AppCompatActivity import com.google.android.material.dialog.MaterialAlertDialogBuilder -import kotlinx.coroutines.* +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import okhttp3.Request -import pl.szczodrzynski.edziennik.* +import pl.szczodrzynski.edziennik.App +import pl.szczodrzynski.edziennik.BuildConfig +import pl.szczodrzynski.edziennik.R import pl.szczodrzynski.edziennik.data.api.szkolny.interceptor.Signing -import pl.szczodrzynski.edziennik.ext.* +import pl.szczodrzynski.edziennik.data.api.szkolny.response.Update +import pl.szczodrzynski.edziennik.ext.Intent +import pl.szczodrzynski.edziennik.ext.asBoldSpannable +import pl.szczodrzynski.edziennik.ext.asColoredSpannable +import pl.szczodrzynski.edziennik.ext.concat +import pl.szczodrzynski.edziennik.ext.getJsonObject +import pl.szczodrzynski.edziennik.ext.getString +import pl.szczodrzynski.edziennik.ext.isNotNullNorBlank +import pl.szczodrzynski.edziennik.ext.join +import pl.szczodrzynski.edziennik.ext.md5 +import pl.szczodrzynski.edziennik.ext.resolveAttr +import pl.szczodrzynski.edziennik.ext.resolveColor +import pl.szczodrzynski.edziennik.ext.toJsonObject import pl.szczodrzynski.edziennik.ui.base.BuildInvalidActivity import pl.szczodrzynski.edziennik.utils.Utils import pl.szczodrzynski.edziennik.utils.Utils.d @@ -75,6 +93,14 @@ class BuildManager(val app: App) : CoroutineScope { else -> null } + val releaseType = when { + isNightly || isDaily -> Update.Type.NIGHTLY + BuildConfig.VERSION_BASE.endsWith("-dev") -> Update.Type.DEV + BuildConfig.VERSION_BASE.contains("-beta.") -> Update.Type.BETA + BuildConfig.VERSION_BASE.contains("-rc.") -> Update.Type.RC + else -> Update.Type.RELEASE + } + fun showVersionDialog(activity: AppCompatActivity) { val yes = activity.getString(R.string.yes) val no = activity.getString(R.string.no) diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/utils/managers/UpdateManager.kt b/app/src/main/java/pl/szczodrzynski/edziennik/utils/managers/UpdateManager.kt new file mode 100644 index 00000000..9efbecf2 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/utils/managers/UpdateManager.kt @@ -0,0 +1,110 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2022-10-22. + */ + +package pl.szczodrzynski.edziennik.utils.managers + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.withContext +import org.greenrobot.eventbus.EventBus +import pl.szczodrzynski.edziennik.App +import pl.szczodrzynski.edziennik.BuildConfig +import pl.szczodrzynski.edziennik.R +import pl.szczodrzynski.edziennik.data.api.szkolny.response.Update +import pl.szczodrzynski.edziennik.data.api.task.PostNotifications +import pl.szczodrzynski.edziennik.data.db.entity.Notification +import pl.szczodrzynski.edziennik.data.db.enums.NotificationType +import pl.szczodrzynski.edziennik.ext.concat +import pl.szczodrzynski.edziennik.ext.resolveString +import pl.szczodrzynski.edziennik.utils.html.BetterHtml +import kotlin.coroutines.CoroutineContext + +class UpdateManager(val app: App) : CoroutineScope { + companion object { + private const val TAG = "UpdateManager" + } + + private val job = Job() + override val coroutineContext: CoroutineContext + get() = job + Dispatchers.Default + + /** + * Check for updates on the specified [maxChannel]. + * If the running build is of "more-unstable" type, + * that channel is used instead. + * + * Optionally, post a notification if [notify] is true. + * + * @return [Result] containing a newer update, or null if not available + */ + suspend fun checkNow( + maxChannel: Update.Type, + notify: Boolean, + ): Result = withContext(Dispatchers.IO) { + return@withContext checkNowSync(maxChannel, notify) + } + + /** + * Check for updates on the specified [maxChannel]. + * If the running build is of "more-unstable" type, + * that channel is used instead. + * + * Optionally, post a notification if [notify] is true. + * + * @return [Result] containing a newer update, or null if not available + */ + fun checkNowSync( + maxChannel: Update.Type, + notify: Boolean, + ): Result { + val channel = minOf(app.buildManager.releaseType, maxChannel) + val update = app.api.runCatching({ + getUpdate(channel).firstOrNull() + }, { + return Result.failure(it) + }) + return Result.success(process(update, notify)) + } + + /** + * Process the update: check if the version is newer, and optionally + * post a notification. + * + * @return [update] if it's a newer version, null otherwise + */ + fun process(update: Update?, notify: Boolean): Update? { + if (update == null || update.versionCode <= BuildConfig.VERSION_CODE) { + app.config.update = null + return null + } + app.config.update = update + + if (EventBus.getDefault().hasSubscriberForEvent(update::class.java)) { + EventBus.getDefault().postSticky(update) + return update + } + + if (notify) + notify(update) + return update + } + + fun notify(update: Update) { + val bigText = listOf( + app.getString(R.string.notification_updates_text, update.versionName), + update.releaseNotes?.let { BetterHtml.fromHtml(context = null, it) }, + ) + val notification = Notification( + id = System.currentTimeMillis(), + title = R.string.notification_updates_title.resolveString(app), + text = bigText.concat("\n").toString(), + type = NotificationType.UPDATE, + profileId = null, + profileName = R.string.notification_updates_title.resolveString(app), + ).addExtra("action", "updateRequest") + app.db.notificationDao().add(notification) + PostNotifications(app, listOf(notification)) + } +} diff --git a/app/src/main/res/layout/update_progress_dialog.xml b/app/src/main/res/layout/update_progress_dialog.xml new file mode 100644 index 00000000..767c6a1b --- /dev/null +++ b/app/src/main/res/layout/update_progress_dialog.xml @@ -0,0 +1,90 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/play-not/java/pl/szczodrzynski/edziennik/sync/UpdateDownloaderService.kt b/app/src/play-not/java/pl/szczodrzynski/edziennik/sync/UpdateDownloaderService.kt index 93881939..a4c9feae 100644 --- a/app/src/play-not/java/pl/szczodrzynski/edziennik/sync/UpdateDownloaderService.kt +++ b/app/src/play-not/java/pl/szczodrzynski/edziennik/sync/UpdateDownloaderService.kt @@ -15,6 +15,8 @@ import android.net.Uri import android.os.Build import android.widget.Toast import androidx.core.content.FileProvider +import androidx.core.content.getSystemService +import org.greenrobot.eventbus.EventBus import pl.szczodrzynski.edziennik.App import pl.szczodrzynski.edziennik.R import pl.szczodrzynski.edziennik.data.api.szkolny.response.Update @@ -49,6 +51,8 @@ class UpdateDownloaderService : IntentService(UpdateDownloaderService::class.jav return val app = context.applicationContext as App + EventBus.getDefault().postSticky(UpdateStateEvent(running = false, update = null, downloadId)) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && !app.permissionChecker.canRequestApkInstall()) { app.permissionChecker.requestApkInstall() return @@ -79,11 +83,14 @@ class UpdateDownloaderService : IntentService(UpdateDownloaderService::class.jav val app = application as App val update = App.config.update ?: return - if (tryUpdateWithGooglePlay(update)) + if (tryUpdateWithGooglePlay(update)) { + stopSelf() return + } if (update.downloadUrl == null) { Toast.makeText(app, "Nie można pobrać tej aktualizacji. Pobierz ręcznie z Google Play.", Toast.LENGTH_LONG).show() + stopSelf() return } @@ -92,7 +99,7 @@ class UpdateDownloaderService : IntentService(UpdateDownloaderService::class.jav return } - (app.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager).cancel(app.notificationChannelsManager.updates.id) + app.getSystemService()?.cancel(app.notificationChannelsManager.updates.id) val dir: File? = app.getExternalFilesDir(null) if (dir?.isDirectory == true) { @@ -117,5 +124,6 @@ class UpdateDownloaderService : IntentService(UpdateDownloaderService::class.jav } Toast.makeText(app, "Pobieranie aktualizacji Szkolny.eu ${update.versionName}", Toast.LENGTH_LONG).show() downloadId = downloadManager.enqueue(request) + EventBus.getDefault().postSticky(UpdateStateEvent(running = true, update, downloadId)) } }