UpdaterService now uses PackageInstaller for Android 12

And also skip the user prompt for a12

On a12, update service will automatically install after the dl completes
This commit is contained in:
Jays2Kings 2021-07-18 21:25:13 -04:00
parent 9077140800
commit 5f0157499e
8 changed files with 213 additions and 24 deletions

View File

@ -200,6 +200,14 @@
android:name=".data.notification.NotificationReceiver"
android:exported="false" />
<receiver
android:name=".data.updater.UpdaterBroadcast"
android:exported="false">
<intent-filter>
<action android:name="android.intent.action.MY_PACKAGE_REPLACED" />
</intent-filter>
</receiver>
<service
android:name=".data.library.LibraryUpdateService"
android:exported="false" />

View File

@ -10,6 +10,7 @@ import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.preference.getOrDefault
import eu.kanade.tachiyomi.data.track.TrackManager
import eu.kanade.tachiyomi.data.updater.UpdaterJob
import eu.kanade.tachiyomi.data.updater.UpdaterService
import eu.kanade.tachiyomi.extension.ExtensionUpdateJob
import eu.kanade.tachiyomi.network.PREF_DOH_CLOUDFLARE
import eu.kanade.tachiyomi.ui.library.LibraryPresenter
@ -29,6 +30,10 @@ object Migrations {
*/
fun upgrade(preferences: PreferencesHelper): Boolean {
val context = preferences.context
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
prefs.edit {
remove(UpdaterService.NOTIFY_ON_INSTALL_KEY)
}
val oldVersion = preferences.lastVersionCode().getOrDefault()
if (oldVersion < BuildConfig.VERSION_CODE) {
preferences.lastVersionCode().set(BuildConfig.VERSION_CODE)
@ -103,7 +108,6 @@ object Migrations {
}
if (oldVersion < 71) {
// Migrate DNS over HTTPS setting
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
val wasDohEnabled = prefs.getBoolean("enable_doh", false)
if (wasDohEnabled) {
prefs.edit {
@ -114,7 +118,6 @@ object Migrations {
}
if (oldVersion < 73) {
// Reset rotation to Free after replacing Lock
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
if (prefs.contains("pref_rotation_type_key")) {
prefs.edit {
putInt("pref_rotation_type_key", 1)
@ -128,7 +131,6 @@ object Migrations {
}
}
if (oldVersion < 75) {
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
val wasShortcutsDisabled = !prefs.getBoolean("show_manga_app_shortcuts", true)
if (wasShortcutsDisabled) {
prefs.edit {
@ -149,7 +151,6 @@ object Migrations {
}
if (oldVersion < 77) {
// Migrate Rotation and Viewer values to default values for viewer_flags
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
val newOrientation = when (prefs.getInt("pref_rotation_type_key", 1)) {
1 -> OrientationType.FREE.flagValue
2 -> OrientationType.PORTRAIT.flagValue

View File

@ -20,6 +20,8 @@ object Notifications {
const val ID_UPDATER = 1
const val ID_DOWNLOAD_IMAGE = 2
const val ID_INSTALL = 3
const val CHANNEL_UPDATED = "updated_channel"
const val ID_INSTALLED = -6
/**
* Notification channel and ids used by the library updater.
@ -139,15 +141,24 @@ object Notifications {
)
context.notificationManager.createNotificationChannels(channels)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
val channel = NotificationChannel(
CHANNEL_EXT_PROGRESS,
context.getString(R.string.updating_extensions),
NotificationManager.IMPORTANCE_LOW
).apply {
setShowBadge(false)
setSound(null, null)
}
context.notificationManager.createNotificationChannel(channel)
val newChannels = listOf(
NotificationChannel(
CHANNEL_EXT_PROGRESS,
context.getString(R.string.updating_extensions),
NotificationManager.IMPORTANCE_LOW
).apply {
setShowBadge(false)
setSound(null, null)
},
NotificationChannel(
CHANNEL_UPDATED,
context.getString(R.string.update_completed),
NotificationManager.IMPORTANCE_DEFAULT
).apply {
setShowBadge(false)
}
)
context.notificationManager.createNotificationChannels(newChannels)
}
}
}

View File

@ -0,0 +1,55 @@
package eu.kanade.tachiyomi.data.updater
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.pm.PackageInstaller
import androidx.core.content.edit
import androidx.core.net.toUri
import androidx.preference.PreferenceManager
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.util.system.toast
class UpdaterBroadcast : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
if (UpdaterService.PACKAGE_INSTALLED_ACTION == intent.action) {
val extras = intent.extras ?: return
when (val status = extras.getInt(PackageInstaller.EXTRA_STATUS)) {
PackageInstaller.STATUS_PENDING_USER_ACTION -> {
val confirmIntent = extras[Intent.EXTRA_INTENT] as? Intent
context.startActivity(confirmIntent)
}
PackageInstaller.STATUS_SUCCESS -> {
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
prefs.edit {
remove(UpdaterService.NOTIFY_ON_INSTALL_KEY)
}
val notifyOnInstall = extras.getBoolean(UpdaterService.EXTRA_NOTIFY_ON_INSTALL, false)
try {
if (notifyOnInstall) {
UpdaterNotifier(context).onInstallFinished()
}
} finally {
UpdaterService.stop(context)
}
}
PackageInstaller.STATUS_FAILURE, PackageInstaller.STATUS_FAILURE_ABORTED, PackageInstaller.STATUS_FAILURE_BLOCKED, PackageInstaller.STATUS_FAILURE_CONFLICT, PackageInstaller.STATUS_FAILURE_INCOMPATIBLE, PackageInstaller.STATUS_FAILURE_INVALID, PackageInstaller.STATUS_FAILURE_STORAGE -> {
if (status != PackageInstaller.STATUS_FAILURE_ABORTED) {
context.toast(R.string.could_not_install_update)
val uri = intent.getStringExtra(UpdaterService.EXTRA_FILE_URI) ?: return
UpdaterNotifier(context).onInstallError(uri.toUri())
}
}
}
} else if (intent.action == Intent.ACTION_MY_PACKAGE_REPLACED) {
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
val notifyOnInstall = prefs.getBoolean(UpdaterService.NOTIFY_ON_INSTALL_KEY, false)
prefs.edit {
remove(UpdaterService.NOTIFY_ON_INSTALL_KEY)
}
if (notifyOnInstall) {
UpdaterNotifier(context).onInstallFinished()
}
}
}
}

View File

@ -7,6 +7,7 @@ import android.net.Uri
import androidx.core.app.NotificationCompat
import androidx.core.content.ContextCompat
import androidx.core.net.toUri
import eu.kanade.tachiyomi.BuildConfig
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.notification.NotificationHandler
import eu.kanade.tachiyomi.data.notification.NotificationReceiver
@ -44,6 +45,7 @@ internal class UpdaterNotifier(private val context: Context) {
fun promptUpdate(body: String, url: String, releaseUrl: String) {
val intent = Intent(context, UpdaterService::class.java).apply {
putExtra(UpdaterService.EXTRA_DOWNLOAD_URL, url)
putExtra(UpdaterService.EXTRA_NOTIFY_ON_INSTALL, true)
}
val pendingIntent = NotificationReceiver.openUpdatePendingActivity(context, body, url)
@ -155,6 +157,31 @@ internal class UpdaterNotifier(private val context: Context) {
notificationBuilder.show(Notifications.ID_INSTALL)
}
/**
* Call when apk download is finished.
*
* @param uri path location of apk.
*/
fun onInstallFinished() {
with(NotificationCompat.Builder(context, Notifications.CHANNEL_UPDATED)) {
setContentTitle(context.getString(R.string.updated_to_, BuildConfig.VERSION_NAME))
setSmallIcon(R.drawable.ic_tachij2k)
setAutoCancel(true)
setOngoing(false)
setProgress(0, 0, false)
val pendingIntent = PendingIntent.getActivity(
context,
0,
context.packageManager.getLaunchIntentForPackage(BuildConfig.APPLICATION_ID),
PendingIntent.FLAG_UPDATE_CURRENT
)
setContentIntent(pendingIntent)
clearActions()
addReleasePageAction()
show(Notifications.ID_INSTALLED)
}
}
/**
* Call when apk download throws a error
*
@ -186,6 +213,32 @@ internal class UpdaterNotifier(private val context: Context) {
notificationBuilder.show(Notifications.ID_UPDATER)
}
fun onInstallError(uri: Uri) {
with(notificationBuilder) {
setContentText(context.getString(R.string.could_not_install_update))
setSmallIcon(android.R.drawable.stat_sys_warning)
setOnlyAlertOnce(false)
setAutoCancel(false)
setProgress(0, 0, false)
color = ContextCompat.getColor(context, R.color.colorAccent)
clearActions()
// Retry action
addAction(
R.drawable.ic_refresh_24dp,
context.getString(R.string.retry),
NotificationHandler.installApkPendingActivity(context, uri)
)
// Cancel action
addAction(
R.drawable.ic_close_24dp,
context.getString(R.string.cancel),
NotificationReceiver.dismissNotificationPendingBroadcast(context, Notifications.ID_UPDATER)
)
addReleasePageAction()
}
notificationBuilder.show(Notifications.ID_UPDATER)
}
fun cancel() {
NotificationReceiver.dismissNotification(context, Notifications.ID_UPDATER)
}

View File

@ -4,9 +4,11 @@ import android.app.PendingIntent
import android.app.Service
import android.content.Context
import android.content.Intent
import android.content.pm.PackageInstaller
import android.os.Build
import android.os.IBinder
import android.os.PowerManager
import androidx.core.content.edit
import eu.kanade.tachiyomi.BuildConfig
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.notification.Notifications
@ -18,12 +20,11 @@ import eu.kanade.tachiyomi.network.newCallWithProgress
import eu.kanade.tachiyomi.util.storage.getUriCompat
import eu.kanade.tachiyomi.util.storage.saveTo
import eu.kanade.tachiyomi.util.system.acquireWakeLock
import eu.kanade.tachiyomi.util.system.isServiceRunning
import eu.kanade.tachiyomi.util.system.toast
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineExceptionHandler
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch
import okhttp3.Call
import okhttp3.internal.http2.ErrorCode
@ -64,6 +65,8 @@ class UpdaterService : Service() {
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
if (intent == null) return START_NOT_STICKY
instance = this
val handler = CoroutineExceptionHandler { _, exception ->
Timber.e(exception)
stopSelf(startId)
@ -71,9 +74,10 @@ class UpdaterService : Service() {
val url = intent.getStringExtra(EXTRA_DOWNLOAD_URL) ?: return START_NOT_STICKY
val title = intent.getStringExtra(EXTRA_DOWNLOAD_TITLE) ?: getString(R.string.app_name)
val notifyOnInstall = intent.getBooleanExtra(EXTRA_NOTIFY_ON_INSTALL, false)
runningJob = GlobalScope.launch(handler) {
downloadApk(title, url)
downloadApk(title, url, notifyOnInstall)
}
runningJob?.invokeOnCompletion { stopSelf(startId) }
@ -88,6 +92,9 @@ class UpdaterService : Service() {
override fun onDestroy() {
destroyJob()
if (instance == this) {
instance = null
}
super.onDestroy()
}
@ -104,7 +111,7 @@ class UpdaterService : Service() {
*
* @param url url location of file
*/
private suspend fun downloadApk(title: String, url: String) {
private suspend fun downloadApk(title: String, url: String, notifyOnInstall: Boolean) {
// Show notification download starting.
notifier.onDownloadStarted(title)
@ -141,7 +148,11 @@ class UpdaterService : Service() {
response.close()
throw Exception("Unsuccessful response")
}
notifier.onDownloadFinished(apkFile.getUriCompat(this))
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
startInstalling(apkFile, notifyOnInstall)
} else {
notifier.onDownloadFinished(apkFile.getUriCompat(this))
}
} catch (error: Exception) {
Timber.e(error)
if (error is CancellationException ||
@ -154,30 +165,77 @@ class UpdaterService : Service() {
}
}
private fun startInstalling(file: File, notifyOnInstall: Boolean) {
try {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) return
val packageInstaller = packageManager.packageInstaller
val data = file.inputStream()
val params = PackageInstaller.SessionParams(
PackageInstaller.SessionParams.MODE_FULL_INSTALL
)
params.setRequireUserAction(PackageInstaller.SessionParams.USER_ACTION_NOT_REQUIRED)
val sessionId = packageInstaller.createSession(params)
val session = packageInstaller.openSession(sessionId)
session.openWrite("package", 0, -1).use { packageInSession ->
data.copyTo(packageInSession)
}
if (notifyOnInstall) {
val prefs = PreferenceManager.getDefaultSharedPreferences(this)
prefs.edit {
putBoolean(NOTIFY_ON_INSTALL_KEY, true)
}
}
val newIntent = Intent(this, UpdaterBroadcast::class.java)
.setAction(PACKAGE_INSTALLED_ACTION)
.putExtra(EXTRA_NOTIFY_ON_INSTALL, notifyOnInstall)
.putExtra(EXTRA_FILE_URI, file.getUriCompat(this).toString())
val pendingIntent = PendingIntent.getBroadcast(this, -10053, newIntent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE)
val statusReceiver = pendingIntent.intentSender
session.commit(statusReceiver)
data.close()
} catch (error: Exception) {
// Either install package can't be found (probably bots) or there's a security exception
// with the download manager. Nothing we can workaround.
toast(error.message)
}
}
companion object {
const val PACKAGE_INSTALLED_ACTION =
"${BuildConfig.APPLICATION_ID}.SESSION_SELF_API_PACKAGE_INSTALLED"
internal const val EXTRA_NOTIFY_ON_INSTALL = "${BuildConfig.APPLICATION_ID}.UpdaterService.ACTION_ON_INSTALL"
internal const val EXTRA_DOWNLOAD_URL = "${BuildConfig.APPLICATION_ID}.UpdaterService.DOWNLOAD_URL"
internal const val EXTRA_FILE_URI = "${BuildConfig.APPLICATION_ID}.UpdaterService.FILE_URI"
internal const val EXTRA_DOWNLOAD_TITLE = "${BuildConfig.APPLICATION_ID}.UpdaterService.DOWNLOAD_TITLE"
internal const val NOTIFY_ON_INSTALL_KEY = "notify_on_install_complete"
private var instance: UpdaterService? = null
/**
* Returns the status of the service.
*
* @param context the application context.
* @return true if the service is running, false otherwise.
*/
private fun isRunning(context: Context): Boolean =
context.isServiceRunning(UpdaterService::class.java)
fun isRunning(): Boolean = instance != null
/**
* Downloads a new update and let the user install the new version from a notification.
* @param context the application context.
* @param url the url to the new update.
*/
fun start(context: Context, url: String, title: String = context.getString(R.string.app_name)) {
if (!isRunning(context)) {
fun start(context: Context, url: String, notifyOnInstall: Boolean) {
if (!isRunning()) {
val title = context.getString(R.string.app_name)
val intent = Intent(context, UpdaterService::class.java).apply {
putExtra(EXTRA_DOWNLOAD_TITLE, title)
putExtra(EXTRA_DOWNLOAD_URL, url)
putExtra(EXTRA_NOTIFY_ON_INSTALL, notifyOnInstall)
}
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
context.startService(intent)
@ -202,9 +260,10 @@ class UpdaterService : Service() {
* @param url the url to the new update.
* @return [PendingIntent]
*/
internal fun downloadApkPendingService(context: Context, url: String): PendingIntent {
internal fun downloadApkPendingService(context: Context, url: String, notifyOnInstall: Boolean = false): PendingIntent {
val intent = Intent(context, UpdaterService::class.java).apply {
putExtra(EXTRA_DOWNLOAD_URL, url)
putExtra(EXTRA_NOTIFY_ON_INSTALL, notifyOnInstall)
}
return PendingIntent.getService(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
}

View File

@ -198,7 +198,7 @@ class AboutController : SettingsController() {
if (appContext != null) {
// Start download
val url = args.getString(URL_KEY) ?: ""
UpdaterService.start(appContext, url)
UpdaterService.start(appContext, url, true)
}
}
.negativeButton(R.string.ignore)

View File

@ -117,6 +117,8 @@
<string name="no_new_updates_available">No new updates available</string>
<string name="searching_for_updates">Searching for updates…</string>
<string name="release_page">Release page</string>
<string name="could_not_install_update">Could not install update</string>
<string name="update_completed">Update completed</string>
<!-- Main Screens -->