mirror of
https://github.com/tachiyomiorg/tachiyomi.git
synced 2024-12-23 17:41:50 +01:00
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:
parent
9077140800
commit
5f0157499e
@ -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" />
|
||||
|
@ -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
|
||||
|
@ -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(
|
||||
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.createNotificationChannel(channel)
|
||||
)
|
||||
context.notificationManager.createNotificationChannels(newChannels)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
|
@ -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")
|
||||
}
|
||||
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)
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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 -->
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user