diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index dd419fe399..58fea94f0b 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -200,6 +200,14 @@
android:name=".data.notification.NotificationReceiver"
android:exported="false" />
+
+
+
+
+
+
diff --git a/app/src/main/java/eu/kanade/tachiyomi/Migrations.kt b/app/src/main/java/eu/kanade/tachiyomi/Migrations.kt
index ce0e136ac1..3bd969dae2 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/Migrations.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/Migrations.kt
@@ -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
diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/notification/Notifications.kt b/app/src/main/java/eu/kanade/tachiyomi/data/notification/Notifications.kt
index cb465aef7b..c04e3d47d6 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/data/notification/Notifications.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/data/notification/Notifications.kt
@@ -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)
}
}
}
diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/updater/UpdaterBroadcast.kt b/app/src/main/java/eu/kanade/tachiyomi/data/updater/UpdaterBroadcast.kt
new file mode 100644
index 0000000000..59644902d8
--- /dev/null
+++ b/app/src/main/java/eu/kanade/tachiyomi/data/updater/UpdaterBroadcast.kt
@@ -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()
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/updater/UpdaterNotifier.kt b/app/src/main/java/eu/kanade/tachiyomi/data/updater/UpdaterNotifier.kt
index f72237eb0d..eee2d771b1 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/data/updater/UpdaterNotifier.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/data/updater/UpdaterNotifier.kt
@@ -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)
}
diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/updater/UpdaterService.kt b/app/src/main/java/eu/kanade/tachiyomi/data/updater/UpdaterService.kt
index ef574a782e..cd1e17f397 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/data/updater/UpdaterService.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/data/updater/UpdaterService.kt
@@ -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)
}
diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/AboutController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/AboutController.kt
index 1938da45fe..9c421daf97 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/AboutController.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/AboutController.kt
@@ -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)
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 2b2a388a5b..1e7ce05b7f 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -117,6 +117,8 @@
No new updates available
Searching for updates…
Release page
+ Could not install update
+ Update completed