[App] Rework update handling.

This commit is contained in:
Kuba Szczodrzyński 2022-10-22 22:10:04 +02:00
parent 0d4dee765a
commit c8e8c172a2
No known key found for this signature in database
GPG Key ID: 70CB8A85BA1633CB
15 changed files with 444 additions and 130 deletions

View File

@ -28,7 +28,11 @@ import com.google.gson.Gson
import com.hypertrack.hyperlog.HyperLog import com.hypertrack.hyperlog.HyperLog
import com.mikepenz.iconics.Iconics import com.mikepenz.iconics.Iconics
import im.wangchao.mhttp.MHttp 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 me.leolin.shortcutbadger.ShortcutBadger
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import org.greenrobot.eventbus.EventBus 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.Themes
import pl.szczodrzynski.edziennik.utils.Utils import pl.szczodrzynski.edziennik.utils.Utils
import pl.szczodrzynski.edziennik.utils.Utils.d 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 java.util.concurrent.TimeUnit
import kotlin.coroutines.CoroutineContext import kotlin.coroutines.CoroutineContext
import kotlin.system.exitProcess import kotlin.system.exitProcess
@ -80,18 +96,19 @@ class App : MultiDexApplication(), Configuration.Provider, CoroutineScope {
} }
val api by lazy { SzkolnyApi(this) } 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 attendanceManager by lazy { AttendanceManager(this) }
val buildManager by lazy { BuildManager(this) }
val availabilityManager by lazy { AvailabilityManager(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 messageManager by lazy { MessageManager(this) }
val noteManager by lazy { NoteManager(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 val db
get() = App.db get() = App.db

View File

@ -46,6 +46,7 @@ import pl.szczodrzynski.edziennik.databinding.ActivitySzkolnyBinding
import pl.szczodrzynski.edziennik.ext.* import pl.szczodrzynski.edziennik.ext.*
import pl.szczodrzynski.edziennik.sync.AppManagerDetectedEvent import pl.szczodrzynski.edziennik.sync.AppManagerDetectedEvent
import pl.szczodrzynski.edziennik.sync.SyncWorker import pl.szczodrzynski.edziennik.sync.SyncWorker
import pl.szczodrzynski.edziennik.sync.UpdateStateEvent
import pl.szczodrzynski.edziennik.sync.UpdateWorker import pl.szczodrzynski.edziennik.sync.UpdateWorker
import pl.szczodrzynski.edziennik.ui.base.MainSnackbar import pl.szczodrzynski.edziennik.ui.base.MainSnackbar
import pl.szczodrzynski.edziennik.ui.base.enums.NavTarget 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.ServerMessageDialog
import pl.szczodrzynski.edziennik.ui.dialogs.sync.SyncViewListDialog import pl.szczodrzynski.edziennik.ui.dialogs.sync.SyncViewListDialog
import pl.szczodrzynski.edziennik.ui.dialogs.sync.UpdateAvailableDialog 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.ErrorDetailsDialog
import pl.szczodrzynski.edziennik.ui.error.ErrorSnackbar import pl.szczodrzynski.edziennik.ui.error.ErrorSnackbar
import pl.szczodrzynski.edziennik.ui.event.EventManualDialog import pl.szczodrzynski.edziennik.ui.event.EventManualDialog
@ -536,6 +538,14 @@ class MainActivity : AppCompatActivity(), CoroutineScope {
UpdateAvailableDialog(this, event).show() 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) @Subscribe(threadMode = ThreadMode.MAIN, sticky = true)
fun onRegisterAvailabilityEvent(event: RegisterAvailabilityEvent) { fun onRegisterAvailabilityEvent(event: RegisterAvailabilityEvent) {
EventBus.getDefault().removeStickyEvent(event) EventBus.getDefault().removeStickyEvent(event)
@ -699,6 +709,10 @@ class MainActivity : AppCompatActivity(), CoroutineScope {
if (extras?.containsKey("action") == true) { if (extras?.containsKey("action") == true) {
val handled = when (extras.getString("action")) { val handled = when (extras.getString("action")) {
"updateRequest" -> {
UpdateAvailableDialog(this, app.config.update).show()
true
}
"serverMessage" -> { "serverMessage" -> {
ServerMessageDialog( ServerMessageDialog(
this, this,

View File

@ -13,7 +13,6 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import org.greenrobot.eventbus.EventBus
import pl.szczodrzynski.edziennik.* import pl.szczodrzynski.edziennik.*
import pl.szczodrzynski.edziennik.data.api.ERROR_API_INVALID_SIGNATURE import pl.szczodrzynski.edziennik.data.api.ERROR_API_INVALID_SIGNATURE
import pl.szczodrzynski.edziennik.data.api.szkolny.adapter.DateAdapter import pl.szczodrzynski.edziennik.data.api.szkolny.adapter.DateAdapter
@ -128,16 +127,10 @@ class SzkolnyApi(val app: App) : CoroutineScope {
response: Response<ApiResponse<T>>, response: Response<ApiResponse<T>>,
updateDeviceHash: Boolean = false, updateDeviceHash: Boolean = false,
): T { ): T {
app.config.update = response.body()?.update?.let { update -> response.body()?.update?.let { update ->
if (update.versionCode > BuildConfig.VERSION_CODE) { // do not process "null" update, as it might not mean there's no update
if (update.updateMandatory // do not notify; silently check and show the home update card
&& EventBus.getDefault().hasSubscriberForEvent(update::class.java)) { app.updateManager.process(update, notify = false)
EventBus.getDefault().postSticky(update)
}
update
}
else
null
} }
response.body()?.registerAvailability?.let { registerAvailability -> response.body()?.registerAvailability?.let { registerAvailability ->
@ -431,8 +424,8 @@ class SzkolnyApi(val app: App) : CoroutineScope {
} }
@Throws(Exception::class) @Throws(Exception::class)
fun getUpdate(channel: String): List<Update> { fun getUpdate(channel: Update.Type): List<Update> {
val response = api.updates(channel).execute() val response = api.updates(channel.name.lowercase()).execute()
return parseResponse(response) return parseResponse(response)
} }

View File

@ -5,12 +5,21 @@
package pl.szczodrzynski.edziennik.data.api.szkolny.response package pl.szczodrzynski.edziennik.data.api.szkolny.response
data class Update( data class Update(
val versionCode: Int, val versionCode: Int,
val versionName: String, val versionName: String,
val releaseDate: String, val releaseDate: String,
val releaseNotes: String?, val releaseNotes: String?,
val releaseType: String, val releaseType: String,
val isOnGooglePlay: Boolean, val isOnGooglePlay: Boolean,
val downloadUrl: String?, val downloadUrl: String?,
val updateMandatory: Boolean val updateMandatory: Boolean,
) ) {
enum class Type {
NIGHTLY,
DEV,
BETA,
RC,
RELEASE,
}
}

View File

@ -6,7 +6,11 @@ package pl.szczodrzynski.edziennik.data.firebase
import com.google.gson.JsonParser import com.google.gson.JsonParser
import com.google.gson.reflect.TypeToken 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 org.greenrobot.eventbus.EventBus
import pl.szczodrzynski.edziennik.App import pl.szczodrzynski.edziennik.App
import pl.szczodrzynski.edziennik.data.api.events.FeedbackMessageEvent 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.RegisterAvailabilityStatus
import pl.szczodrzynski.edziennik.data.api.szkolny.response.Update import pl.szczodrzynski.edziennik.data.api.szkolny.response.Update
import pl.szczodrzynski.edziennik.data.api.task.PostNotifications 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.MetadataType
import pl.szczodrzynski.edziennik.data.db.enums.NotificationType import pl.szczodrzynski.edziennik.data.db.enums.NotificationType
import pl.szczodrzynski.edziennik.ext.getInt import pl.szczodrzynski.edziennik.ext.getInt
import pl.szczodrzynski.edziennik.ext.getLong import pl.szczodrzynski.edziennik.ext.getLong
import pl.szczodrzynski.edziennik.ext.getString import pl.szczodrzynski.edziennik.ext.getString
import pl.szczodrzynski.edziennik.ext.resolveString import pl.szczodrzynski.edziennik.ext.resolveString
import pl.szczodrzynski.edziennik.sync.UpdateWorker
import pl.szczodrzynski.edziennik.ui.base.enums.NavTarget import pl.szczodrzynski.edziennik.ui.base.enums.NavTarget
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
@ -64,7 +72,10 @@ class SzkolnyAppFirebase(val app: App, val profiles: List<Profile>, val message:
message.data.getString("title") ?: "", message.data.getString("title") ?: "",
message.data.getString("message") ?: "" 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 { "feedbackMessage" -> launch {
val message = app.gson.fromJson(message.data.getString("message"), FeedbackMessage::class.java) ?: return@launch val message = app.gson.fromJson(message.data.getString("message"), FeedbackMessage::class.java) ?: return@launch
feedbackMessage(message) feedbackMessage(message)

View File

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

View File

@ -5,25 +5,14 @@
package pl.szczodrzynski.edziennik.sync package pl.szczodrzynski.edziennik.sync
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.NotificationManager
import android.app.PendingIntent
import android.content.Context import android.content.Context
import android.content.Intent
import android.widget.Toast
import androidx.core.app.NotificationCompat
import androidx.work.* import androidx.work.*
import kotlinx.coroutines.* import kotlinx.coroutines.*
import org.greenrobot.eventbus.EventBus
import pl.szczodrzynski.edziennik.* 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.data.api.szkolny.response.Update
import pl.szczodrzynski.edziennik.ext.DAY import pl.szczodrzynski.edziennik.ext.DAY
import pl.szczodrzynski.edziennik.ext.concat
import pl.szczodrzynski.edziennik.ext.formatDate import pl.szczodrzynski.edziennik.ext.formatDate
import pl.szczodrzynski.edziennik.ext.pendingIntentFlag
import pl.szczodrzynski.edziennik.utils.Utils import pl.szczodrzynski.edziennik.utils.Utils
import pl.szczodrzynski.edziennik.utils.html.BetterHtml
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import kotlin.coroutines.CoroutineContext 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") Utils.d(TAG, "Cancelling work by tag $TAG")
WorkManager.getInstance(app).cancelAllWorkByTag(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() private val job = Job()
@ -146,22 +78,13 @@ class UpdateWorker(val context: Context, val params: WorkerParameters) : Worker(
return Result.success() return Result.success()
} }
launch { val channel = if (App.devMode)
runNow(app) Update.Type.BETA
} else
Update.Type.RELEASE
app.updateManager.checkNowSync(channel, notify = true)
rescheduleNext(this.context) rescheduleNext(this.context)
return Result.success() return Result.success()
} }
class JavaWrapper(app: App) : CoroutineScope {
private val job = Job()
override val coroutineContext: CoroutineContext
get() = job + Dispatchers.Main
init {
launch {
runNow(app)
}
}
}
} }

View File

@ -43,11 +43,11 @@ class UpdateAvailableDialog(
override suspend fun onShow() = Unit override suspend fun onShow() = Unit
override suspend fun onPositiveClick(): Boolean { override suspend fun onPositiveClick(): Boolean {
if (update == null) if (update == null || update.isOnGooglePlay)
Utils.openGooglePlay(activity) Utils.openGooglePlay(activity)
else else
activity.startService(Intent(app, UpdateDownloaderService::class.java)) activity.startService(Intent(app, UpdateDownloaderService::class.java))
return NO_DISMISS return DISMISS
} }
override suspend fun onBeforeShow(): Boolean { override suspend fun onBeforeShow(): Boolean {

View File

@ -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<UpdateProgressDialogBinding>(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<DownloadManager>() ?: 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<DownloadManager>() ?: 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()
}
}

View File

@ -15,7 +15,10 @@ import coil.load
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job 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.data.db.entity.Profile
import pl.szczodrzynski.edziennik.databinding.CardHomeAvailabilityBinding import pl.szczodrzynski.edziennik.databinding.CardHomeAvailabilityBinding
import pl.szczodrzynski.edziennik.ext.Intent 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.HomeCard
import pl.szczodrzynski.edziennik.ui.home.HomeCardAdapter import pl.szczodrzynski.edziennik.ui.home.HomeCardAdapter
import pl.szczodrzynski.edziennik.ui.home.HomeFragment import pl.szczodrzynski.edziennik.ui.home.HomeFragment
import pl.szczodrzynski.edziennik.utils.Utils
import pl.szczodrzynski.edziennik.utils.html.BetterHtml import pl.szczodrzynski.edziennik.utils.html.BetterHtml
import kotlin.coroutines.CoroutineContext import kotlin.coroutines.CoroutineContext
@ -79,7 +83,7 @@ class HomeAvailabilityCard(
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)
b.homeAvailabilityUpdate.isVisible = true b.homeAvailabilityUpdate.isVisible = !app.buildManager.isPlayRelease
b.homeAvailabilityIcon.setImageResource(R.drawable.ic_update) b.homeAvailabilityIcon.setImageResource(R.drawable.ic_update)
onInfoClick = { onInfoClick = {
UpdateAvailableDialog(activity, update).show() UpdateAvailableDialog(activity, update).show()
@ -92,7 +96,10 @@ class HomeAvailabilityCard(
b.homeAvailabilityUpdate.onClick { b.homeAvailabilityUpdate.onClick {
if (update == null) if (update == null)
return@onClick 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) b.homeAvailabilityInfo.onClick(onInfoClick)

View File

@ -18,8 +18,8 @@ import kotlinx.coroutines.launch
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.R
import pl.szczodrzynski.edziennik.data.api.szkolny.response.Update
import pl.szczodrzynski.edziennik.ext.after import pl.szczodrzynski.edziennik.ext.after
import pl.szczodrzynski.edziennik.sync.UpdateWorker
import pl.szczodrzynski.edziennik.ui.dialogs.ChangelogDialog import pl.szczodrzynski.edziennik.ui.dialogs.ChangelogDialog
import pl.szczodrzynski.edziennik.ui.settings.SettingsCard import pl.szczodrzynski.edziennik.ui.settings.SettingsCard
import pl.szczodrzynski.edziennik.ui.settings.SettingsLicenseActivity import pl.szczodrzynski.edziennik.ui.settings.SettingsLicenseActivity
@ -113,7 +113,17 @@ class SettingsAboutCard(util: SettingsUtil) : SettingsCard(util), CoroutineScope
icon = CommunityMaterial.Icon3.cmd_update icon = CommunityMaterial.Icon3.cmd_update
) { ) {
launch { 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()
}
} }
} }
)), )),

View File

@ -9,11 +9,29 @@ import android.text.TextUtils
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import com.google.android.material.dialog.MaterialAlertDialogBuilder 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 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.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.ui.base.BuildInvalidActivity
import pl.szczodrzynski.edziennik.utils.Utils import pl.szczodrzynski.edziennik.utils.Utils
import pl.szczodrzynski.edziennik.utils.Utils.d import pl.szczodrzynski.edziennik.utils.Utils.d
@ -75,6 +93,14 @@ class BuildManager(val app: App) : CoroutineScope {
else -> null 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) { fun showVersionDialog(activity: AppCompatActivity) {
val yes = activity.getString(R.string.yes) val yes = activity.getString(R.string.yes)
val no = activity.getString(R.string.no) val no = activity.getString(R.string.no)

View File

@ -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<Update?> = 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<Update?> {
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))
}
}

View File

@ -0,0 +1,90 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (c) Kuba Szczodrzyński 2022-10-22.
-->
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
<import type="pl.szczodrzynski.edziennik.BuildConfig" />
<variable
name="update"
type="pl.szczodrzynski.edziennik.data.api.szkolny.response.Update" />
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingHorizontal="16dp"
android:paddingTop="24dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:text="@string/app_name"
android:textAppearance="@style/NavView.TextView.Medium" />
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:orientation="horizontal">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{BuildConfig.VERSION_BASE}"
android:textAppearance="@style/NavView.TextView.Medium"
tools:text="4.13-rc.1" />
<com.mikepenz.iconics.view.IconicsImageView
android:layout_width="24dp"
android:layout_height="24dp"
app:iiv_color="?android:textColorPrimary"
app:iiv_icon="cmd-chevron-right"
app:iiv_size="24dp"
tools:background="@drawable/ic_arrow_right" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{update.versionName}"
android:textAppearance="@style/NavView.TextView.Medium"
tools:text="4.13-rc.1" />
</LinearLayout>
<com.google.android.material.progressindicator.LinearProgressIndicator
android:id="@+id/progress"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginVertical="8dp"
tools:progress="33" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="64dp"
android:orientation="horizontal">
<TextView
android:id="@+id/downloadedSize"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
tools:text="2.5 MiB" />
<TextView
android:id="@+id/totalSize"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
tools:text="11.4 MiB" />
</LinearLayout>
</LinearLayout>
</layout>

View File

@ -15,6 +15,8 @@ import android.net.Uri
import android.os.Build import android.os.Build
import android.widget.Toast import android.widget.Toast
import androidx.core.content.FileProvider import androidx.core.content.FileProvider
import androidx.core.content.getSystemService
import org.greenrobot.eventbus.EventBus
import pl.szczodrzynski.edziennik.App import pl.szczodrzynski.edziennik.App
import pl.szczodrzynski.edziennik.R import pl.szczodrzynski.edziennik.R
import pl.szczodrzynski.edziennik.data.api.szkolny.response.Update import pl.szczodrzynski.edziennik.data.api.szkolny.response.Update
@ -49,6 +51,8 @@ class UpdateDownloaderService : IntentService(UpdateDownloaderService::class.jav
return return
val app = context.applicationContext as App 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()) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && !app.permissionChecker.canRequestApkInstall()) {
app.permissionChecker.requestApkInstall() app.permissionChecker.requestApkInstall()
return return
@ -79,11 +83,14 @@ class UpdateDownloaderService : IntentService(UpdateDownloaderService::class.jav
val app = application as App val app = application as App
val update = App.config.update ?: return val update = App.config.update ?: return
if (tryUpdateWithGooglePlay(update)) if (tryUpdateWithGooglePlay(update)) {
stopSelf()
return return
}
if (update.downloadUrl == null) { if (update.downloadUrl == null) {
Toast.makeText(app, "Nie można pobrać tej aktualizacji. Pobierz ręcznie z Google Play.", Toast.LENGTH_LONG).show() Toast.makeText(app, "Nie można pobrać tej aktualizacji. Pobierz ręcznie z Google Play.", Toast.LENGTH_LONG).show()
stopSelf()
return return
} }
@ -92,7 +99,7 @@ class UpdateDownloaderService : IntentService(UpdateDownloaderService::class.jav
return return
} }
(app.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager).cancel(app.notificationChannelsManager.updates.id) app.getSystemService<NotificationManager>()?.cancel(app.notificationChannelsManager.updates.id)
val dir: File? = app.getExternalFilesDir(null) val dir: File? = app.getExternalFilesDir(null)
if (dir?.isDirectory == true) { 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() Toast.makeText(app, "Pobieranie aktualizacji Szkolny.eu ${update.versionName}", Toast.LENGTH_LONG).show()
downloadId = downloadManager.enqueue(request) downloadId = downloadManager.enqueue(request)
EventBus.getDefault().postSticky(UpdateStateEvent(running = true, update, downloadId))
} }
} }