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

View File

@ -20,6 +20,8 @@ object Notifications {
const val ID_UPDATER = 1 const val ID_UPDATER = 1
const val ID_DOWNLOAD_IMAGE = 2 const val ID_DOWNLOAD_IMAGE = 2
const val ID_INSTALL = 3 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. * Notification channel and ids used by the library updater.
@ -139,15 +141,24 @@ object Notifications {
) )
context.notificationManager.createNotificationChannels(channels) context.notificationManager.createNotificationChannels(channels)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
val channel = NotificationChannel( val newChannels = listOf(
NotificationChannel(
CHANNEL_EXT_PROGRESS, CHANNEL_EXT_PROGRESS,
context.getString(R.string.updating_extensions), context.getString(R.string.updating_extensions),
NotificationManager.IMPORTANCE_LOW NotificationManager.IMPORTANCE_LOW
).apply { ).apply {
setShowBadge(false) setShowBadge(false)
setSound(null, null) setSound(null, null)
},
NotificationChannel(
CHANNEL_UPDATED,
context.getString(R.string.update_completed),
NotificationManager.IMPORTANCE_DEFAULT
).apply {
setShowBadge(false)
} }
context.notificationManager.createNotificationChannel(channel) )
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.app.NotificationCompat
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.net.toUri import androidx.core.net.toUri
import eu.kanade.tachiyomi.BuildConfig
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.notification.NotificationHandler import eu.kanade.tachiyomi.data.notification.NotificationHandler
import eu.kanade.tachiyomi.data.notification.NotificationReceiver 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) { fun promptUpdate(body: String, url: String, releaseUrl: String) {
val intent = Intent(context, UpdaterService::class.java).apply { val intent = Intent(context, UpdaterService::class.java).apply {
putExtra(UpdaterService.EXTRA_DOWNLOAD_URL, url) putExtra(UpdaterService.EXTRA_DOWNLOAD_URL, url)
putExtra(UpdaterService.EXTRA_NOTIFY_ON_INSTALL, true)
} }
val pendingIntent = NotificationReceiver.openUpdatePendingActivity(context, body, url) val pendingIntent = NotificationReceiver.openUpdatePendingActivity(context, body, url)
@ -155,6 +157,31 @@ internal class UpdaterNotifier(private val context: Context) {
notificationBuilder.show(Notifications.ID_INSTALL) 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 * Call when apk download throws a error
* *
@ -186,6 +213,32 @@ internal class UpdaterNotifier(private val context: Context) {
notificationBuilder.show(Notifications.ID_UPDATER) 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() { fun cancel() {
NotificationReceiver.dismissNotification(context, Notifications.ID_UPDATER) NotificationReceiver.dismissNotification(context, Notifications.ID_UPDATER)
} }

View File

@ -4,9 +4,11 @@ import android.app.PendingIntent
import android.app.Service import android.app.Service
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.pm.PackageInstaller
import android.os.Build import android.os.Build
import android.os.IBinder import android.os.IBinder
import android.os.PowerManager import android.os.PowerManager
import androidx.core.content.edit
import eu.kanade.tachiyomi.BuildConfig import eu.kanade.tachiyomi.BuildConfig
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.notification.Notifications 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.getUriCompat
import eu.kanade.tachiyomi.util.storage.saveTo import eu.kanade.tachiyomi.util.storage.saveTo
import eu.kanade.tachiyomi.util.system.acquireWakeLock 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.CancellationException
import kotlinx.coroutines.CoroutineExceptionHandler import kotlinx.coroutines.CoroutineExceptionHandler
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import okhttp3.Call import okhttp3.Call
import okhttp3.internal.http2.ErrorCode import okhttp3.internal.http2.ErrorCode
@ -64,6 +65,8 @@ class UpdaterService : Service() {
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
if (intent == null) return START_NOT_STICKY if (intent == null) return START_NOT_STICKY
instance = this
val handler = CoroutineExceptionHandler { _, exception -> val handler = CoroutineExceptionHandler { _, exception ->
Timber.e(exception) Timber.e(exception)
stopSelf(startId) stopSelf(startId)
@ -71,9 +74,10 @@ class UpdaterService : Service() {
val url = intent.getStringExtra(EXTRA_DOWNLOAD_URL) ?: return START_NOT_STICKY val url = intent.getStringExtra(EXTRA_DOWNLOAD_URL) ?: return START_NOT_STICKY
val title = intent.getStringExtra(EXTRA_DOWNLOAD_TITLE) ?: getString(R.string.app_name) 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) { runningJob = GlobalScope.launch(handler) {
downloadApk(title, url) downloadApk(title, url, notifyOnInstall)
} }
runningJob?.invokeOnCompletion { stopSelf(startId) } runningJob?.invokeOnCompletion { stopSelf(startId) }
@ -88,6 +92,9 @@ class UpdaterService : Service() {
override fun onDestroy() { override fun onDestroy() {
destroyJob() destroyJob()
if (instance == this) {
instance = null
}
super.onDestroy() super.onDestroy()
} }
@ -104,7 +111,7 @@ class UpdaterService : Service() {
* *
* @param url url location of file * @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. // Show notification download starting.
notifier.onDownloadStarted(title) notifier.onDownloadStarted(title)
@ -141,7 +148,11 @@ class UpdaterService : Service() {
response.close() response.close()
throw Exception("Unsuccessful response") throw Exception("Unsuccessful response")
} }
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
startInstalling(apkFile, notifyOnInstall)
} else {
notifier.onDownloadFinished(apkFile.getUriCompat(this)) notifier.onDownloadFinished(apkFile.getUriCompat(this))
}
} catch (error: Exception) { } catch (error: Exception) {
Timber.e(error) Timber.e(error)
if (error is CancellationException || 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 { 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_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 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. * Returns the status of the service.
* *
* @param context the application context. * @param context the application context.
* @return true if the service is running, false otherwise. * @return true if the service is running, false otherwise.
*/ */
private fun isRunning(context: Context): Boolean = fun isRunning(): Boolean = instance != null
context.isServiceRunning(UpdaterService::class.java)
/** /**
* Downloads a new update and let the user install the new version from a notification. * Downloads a new update and let the user install the new version from a notification.
* @param context the application context. * @param context the application context.
* @param url the url to the new update. * @param url the url to the new update.
*/ */
fun start(context: Context, url: String, title: String = context.getString(R.string.app_name)) { fun start(context: Context, url: String, notifyOnInstall: Boolean) {
if (!isRunning(context)) { if (!isRunning()) {
val title = context.getString(R.string.app_name)
val intent = Intent(context, UpdaterService::class.java).apply { val intent = Intent(context, UpdaterService::class.java).apply {
putExtra(EXTRA_DOWNLOAD_TITLE, title) putExtra(EXTRA_DOWNLOAD_TITLE, title)
putExtra(EXTRA_DOWNLOAD_URL, url) putExtra(EXTRA_DOWNLOAD_URL, url)
putExtra(EXTRA_NOTIFY_ON_INSTALL, notifyOnInstall)
} }
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
context.startService(intent) context.startService(intent)
@ -202,9 +260,10 @@ class UpdaterService : Service() {
* @param url the url to the new update. * @param url the url to the new update.
* @return [PendingIntent] * @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 { val intent = Intent(context, UpdaterService::class.java).apply {
putExtra(EXTRA_DOWNLOAD_URL, url) putExtra(EXTRA_DOWNLOAD_URL, url)
putExtra(EXTRA_NOTIFY_ON_INSTALL, notifyOnInstall)
} }
return PendingIntent.getService(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT) return PendingIntent.getService(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
} }

View File

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

View File

@ -117,6 +117,8 @@
<string name="no_new_updates_available">No new updates available</string> <string name="no_new_updates_available">No new updates available</string>
<string name="searching_for_updates">Searching for updates…</string> <string name="searching_for_updates">Searching for updates…</string>
<string name="release_page">Release page</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 --> <!-- Main Screens -->