Show notification after app upgrade

This commit is contained in:
topjohnwu 2022-02-13 18:35:35 -08:00
parent 2414d5d7f5
commit 256ff31d11
10 changed files with 109 additions and 48 deletions

View File

@ -36,7 +36,7 @@ public final class APKInstall {
// @WorkerThread // @WorkerThread
public static void install(Context context, File apk) { public static void install(Context context, File apk) {
try (var src = new FileInputStream(apk); try (var src = new FileInputStream(apk);
var out = openStream(context, true)) { var out = openStream(context)) {
if (out != null) if (out != null)
transfer(src, out); transfer(src, out);
} catch (IOException e) { } catch (IOException e) {
@ -44,7 +44,7 @@ public final class APKInstall {
} }
} }
public static OutputStream openStream(Context context, boolean silent) { public static OutputStream openStream(Context context) {
//noinspection InlinedApi //noinspection InlinedApi
var flag = PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_MUTABLE; var flag = PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_MUTABLE;
var intent = new Intent(ACTION_SESSION_UPDATE).setPackage(context.getPackageName()); var intent = new Intent(ACTION_SESSION_UPDATE).setPackage(context.getPackageName());
@ -52,7 +52,7 @@ public final class APKInstall {
var installer = context.getPackageManager().getPackageInstaller(); var installer = context.getPackageManager().getPackageInstaller();
var params = new SessionParams(SessionParams.MODE_FULL_INSTALL); var params = new SessionParams(SessionParams.MODE_FULL_INSTALL);
if (silent && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
params.setRequireUserAction(SessionParams.USER_ACTION_NOT_REQUIRED); params.setRequireUserAction(SessionParams.USER_ACTION_NOT_REQUIRED);
} }
try { try {

View File

@ -46,6 +46,7 @@
<intent-filter> <intent-filter>
<action android:name="android.intent.action.LOCALE_CHANGED" /> <action android:name="android.intent.action.LOCALE_CHANGED" />
<action android:name="android.intent.action.UID_REMOVED" /> <action android:name="android.intent.action.UID_REMOVED" />
<action android:name="android.intent.action.MY_PACKAGE_REPLACED" />
</intent-filter> </intent-filter>
<intent-filter> <intent-filter>
<action android:name="android.intent.action.PACKAGE_REPLACED" /> <action android:name="android.intent.action.PACKAGE_REPLACED" />

View File

@ -61,6 +61,7 @@ object Config : PreferenceModel, DBConfig {
const val BOOT_ID = "boot_id" const val BOOT_ID = "boot_id"
const val ASKED_HOME = "asked_home" const val ASKED_HOME = "asked_home"
const val DOH = "doh" const val DOH = "doh"
const val SHOW_UPDATE_DONE = "update_done"
} }
object Value { object Value {
@ -133,6 +134,7 @@ object Config : PreferenceModel, DBConfig {
var suTapjack by preference(Key.SU_TAPJACK, true) var suTapjack by preference(Key.SU_TAPJACK, true)
var checkUpdate by preference(Key.CHECK_UPDATES, true) var checkUpdate by preference(Key.CHECK_UPDATES, true)
var doh by preference(Key.DOH, false) var doh by preference(Key.DOH, false)
var showUpdateDone by preference(Key.SHOW_UPDATE_DONE, false)
var showSystemApp by preference(Key.SHOW_SYSTEM_APP, false) var showSystemApp by preference(Key.SHOW_SYSTEM_APP, false)
var customChannelUrl by preference(Key.CUSTOM_CHANNEL, "") var customChannelUrl by preference(Key.CUSTOM_CHANNEL, "")

View File

@ -37,8 +37,6 @@ object Const {
object ID { object ID {
const val JOB_SERVICE_ID = 7 const val JOB_SERVICE_ID = 7
const val UPDATE_NOTIFICATION_CHANNEL = "update"
const val PROGRESS_NOTIFICATION_CHANNEL = "progress"
} }
object Url { object Url {

View File

@ -23,16 +23,20 @@ class JobService : BaseJobService() {
override fun onStartJob(params: JobParameters): Boolean { override fun onStartJob(params: JobParameters): Boolean {
val coroutineScope = CoroutineScope(Dispatchers.IO + job) val coroutineScope = CoroutineScope(Dispatchers.IO + job)
coroutineScope.launch { coroutineScope.launch {
svc.fetchUpdate()?.run { doWork()
Info.remote = this
if (Info.env.isActive && BuildConfig.VERSION_CODE < magisk.versionCode)
Notifications.managerUpdate(this@JobService)
}
jobFinished(params, false) jobFinished(params, false)
} }
return false return false
} }
private suspend fun doWork() {
svc.fetchUpdate()?.let {
Info.remote = it
if (Info.env.isActive && BuildConfig.VERSION_CODE < it.magisk.versionCode)
Notifications.updateAvailable(this)
}
}
override fun onStopJob(params: JobParameters): Boolean { override fun onStopJob(params: JobParameters): Boolean {
job.cancel() job.cancel()
return false return false

View File

@ -5,6 +5,7 @@ import android.content.ContextWrapper
import android.content.Intent import android.content.Intent
import com.topjohnwu.magisk.core.base.BaseReceiver import com.topjohnwu.magisk.core.base.BaseReceiver
import com.topjohnwu.magisk.di.ServiceLocator import com.topjohnwu.magisk.di.ServiceLocator
import com.topjohnwu.magisk.view.Notifications
import com.topjohnwu.magisk.view.Shortcuts import com.topjohnwu.magisk.view.Shortcuts
import com.topjohnwu.superuser.Shell import com.topjohnwu.superuser.Shell
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.GlobalScope
@ -45,6 +46,12 @@ open class Receiver : BaseReceiver() {
getPkg(intent)?.let { Shell.su("magisk --denylist rm $it").submit() } getPkg(intent)?.let { Shell.su("magisk --denylist rm $it").submit() }
} }
Intent.ACTION_LOCALE_CHANGED -> Shortcuts.setupDynamic(context) Intent.ACTION_LOCALE_CHANGED -> Shortcuts.setupDynamic(context)
Intent.ACTION_MY_PACKAGE_REPLACED -> {
if (Config.showUpdateDone) {
Notifications.updateDone(context)
Config.showUpdateDone = false
}
}
} }
} }
} }

View File

@ -11,10 +11,7 @@ import androidx.core.net.toFile
import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleOwner
import com.topjohnwu.magisk.R import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.StubApk import com.topjohnwu.magisk.StubApk
import com.topjohnwu.magisk.core.ActivityTracker import com.topjohnwu.magisk.core.*
import com.topjohnwu.magisk.core.Info
import com.topjohnwu.magisk.core.intent
import com.topjohnwu.magisk.core.isRunningAsStub
import com.topjohnwu.magisk.core.tasks.HideAPK import com.topjohnwu.magisk.core.tasks.HideAPK
import com.topjohnwu.magisk.core.utils.MediaStoreUtils.outputStream import com.topjohnwu.magisk.core.utils.MediaStoreUtils.outputStream
import com.topjohnwu.magisk.ktx.copyAndClose import com.topjohnwu.magisk.ktx.copyAndClose
@ -75,32 +72,49 @@ class DownloadService : NotificationService() {
} }
} }
private fun openApkSession(): OutputStream {
Config.showUpdateDone = true
return APKInstall.openStream(this)
}
private suspend fun handleApp(stream: InputStream, subject: Subject.App) { private suspend fun handleApp(stream: InputStream, subject: Subject.App) {
fun write(output: OutputStream) { fun writeTee(output: OutputStream) {
val external = subject.externalFile.outputStream() val external = subject.externalFile.outputStream()
stream.copyAndClose(TeeOutputStream(external, output)) stream.copyAndClose(TeeOutputStream(external, output))
} }
if (isRunningAsStub) { if (isRunningAsStub) {
val apk = subject.file.toFile() val updateApk = StubApk.update(this)
val id = subject.notifyId
try { try {
write(StubApk.update(this).outputStream()) // Download full APK to stub update path
writeTee(updateApk.outputStream())
if (Info.stub!!.version < subject.stub.versionCode) { if (Info.stub!!.version < subject.stub.versionCode) {
// Also upgrade stub // Also upgrade stub
update(id) { update(subject.notifyId) {
it.setProgress(0, 0, true) it.setProgress(0, 0, true)
.setContentTitle(getString(R.string.hide_app_title)) .setContentTitle(getString(R.string.hide_app_title))
.setContentText("") .setContentText("")
} }
// Download
val apk = subject.file.toFile()
service.fetchFile(subject.stub.link).byteStream().writeTo(apk) service.fetchFile(subject.stub.link).byteStream().writeTo(apk)
// Patch
val patched = File(apk.parent, "patched.apk") val patched = File(apk.parent, "patched.apk")
val label = applicationInfo.nonLocalizedLabel val label = applicationInfo.nonLocalizedLabel
if (!HideAPK.patch(this, apk, patched, packageName, label)) { if (!HideAPK.patch(this, apk, patched, packageName, label)) {
throw IOException("HideAPK patch error") throw IOException("HideAPK patch error")
} }
apk.delete() apk.delete()
patched.renameTo(apk)
// Install
val receiver = APKInstall.register(this, null, null)
patched.inputStream().copyAndClose(openApkSession())
subject.intent = receiver.waitIntent()
patched.delete()
} else { } else {
ActivityTracker.foreground?.let { ActivityTracker.foreground?.let {
// Relaunch the process if we are foreground // Relaunch the process if we are foreground
@ -112,13 +126,16 @@ class DownloadService : NotificationService() {
return return
} }
} catch (e: Exception) { } catch (e: Exception) {
StubApk.update(this).delete() // If any error occurred, do not let stub load the new APK
} updateApk.delete()
throw e
} }
} else {
val receiver = APKInstall.register(this, null, null) val receiver = APKInstall.register(this, null, null)
write(APKInstall.openStream(this, false)) writeTee(openApkSession())
subject.intent = receiver.waitIntent() subject.intent = receiver.waitIntent()
} }
}
private fun handleModule(src: InputStream, file: Uri) { private fun handleModule(src: InputStream, file: Uri) {
val input = ZipInputStream(src.buffered()) val input = ZipInputStream(src.buffered())

View File

@ -15,7 +15,10 @@ import com.topjohnwu.magisk.MainDirections
import com.topjohnwu.magisk.R import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.arch.BaseMainActivity import com.topjohnwu.magisk.arch.BaseMainActivity
import com.topjohnwu.magisk.arch.BaseViewModel import com.topjohnwu.magisk.arch.BaseViewModel
import com.topjohnwu.magisk.core.* import com.topjohnwu.magisk.core.Config
import com.topjohnwu.magisk.core.Const
import com.topjohnwu.magisk.core.Info
import com.topjohnwu.magisk.core.isRunningAsStub
import com.topjohnwu.magisk.databinding.ActivityMainMd2Binding import com.topjohnwu.magisk.databinding.ActivityMainMd2Binding
import com.topjohnwu.magisk.di.viewModel import com.topjohnwu.magisk.di.viewModel
import com.topjohnwu.magisk.ktx.startAnimations import com.topjohnwu.magisk.ktx.startAnimations
@ -48,10 +51,11 @@ class MainActivity : BaseMainActivity<ActivityMainMd2Binding>() {
setContentView() setContentView()
showUnsupportedMessage() showUnsupportedMessage()
askForHomeShortcut() askForHomeShortcut()
Config.showUpdateDone = false
window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE) window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE)
navigation?.addOnDestinationChangedListener { _, destination, _ -> navigation.addOnDestinationChangedListener { _, destination, _ ->
isRootFragment = when (destination.id) { isRootFragment = when (destination.id) {
R.id.homeFragment, R.id.homeFragment,
R.id.modulesFragment, R.id.modulesFragment,

View File

@ -1,15 +1,16 @@
package com.topjohnwu.magisk.view package com.topjohnwu.magisk.view
import android.annotation.SuppressLint
import android.app.Notification import android.app.Notification
import android.app.NotificationChannel import android.app.NotificationChannel
import android.app.NotificationManager import android.app.NotificationManager
import android.app.PendingIntent
import android.content.Context import android.content.Context
import android.content.Intent
import android.os.Build.VERSION.SDK_INT import android.os.Build.VERSION.SDK_INT
import androidx.core.content.getSystemService import androidx.core.content.getSystemService
import androidx.core.graphics.drawable.toIcon import androidx.core.graphics.drawable.toIcon
import com.topjohnwu.magisk.R import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.core.Const.ID.PROGRESS_NOTIFICATION_CHANNEL
import com.topjohnwu.magisk.core.Const.ID.UPDATE_NOTIFICATION_CHANNEL
import com.topjohnwu.magisk.core.download.DownloadService import com.topjohnwu.magisk.core.download.DownloadService
import com.topjohnwu.magisk.core.download.Subject import com.topjohnwu.magisk.core.download.Subject
import com.topjohnwu.magisk.di.AppContext import com.topjohnwu.magisk.di.AppContext
@ -21,51 +22,75 @@ object Notifications {
val mgr by lazy { AppContext.getSystemService<NotificationManager>()!! } val mgr by lazy { AppContext.getSystemService<NotificationManager>()!! }
private const val APK_UPDATE_NOTIFICATION_ID = 5 private const val APP_UPDATED_NOTIFICATION_ID = 4
private val nextId = AtomicInteger(APK_UPDATE_NOTIFICATION_ID) private const val APP_UPDATE_NOTIFICATION_ID = 5
private const val UPDATE_CHANNEL = "update"
private const val PROGRESS_CHANNEL = "progress"
private const val UPDATED_CHANNEL = "updated"
private val nextId = AtomicInteger(APP_UPDATE_NOTIFICATION_ID)
fun setup(context: Context) { fun setup(context: Context) {
if (SDK_INT >= 26) { if (SDK_INT >= 26) {
val channel = NotificationChannel(UPDATE_NOTIFICATION_CHANNEL, val channel = NotificationChannel(UPDATE_CHANNEL,
context.getString(R.string.update_channel), NotificationManager.IMPORTANCE_DEFAULT) context.getString(R.string.update_channel), NotificationManager.IMPORTANCE_DEFAULT)
val channel2 = NotificationChannel(PROGRESS_NOTIFICATION_CHANNEL, val channel2 = NotificationChannel(PROGRESS_CHANNEL,
context.getString(R.string.progress_channel), NotificationManager.IMPORTANCE_LOW) context.getString(R.string.progress_channel), NotificationManager.IMPORTANCE_LOW)
mgr.createNotificationChannels(listOf(channel, channel2)) val channel3 = NotificationChannel(UPDATED_CHANNEL,
context.getString(R.string.updated_channel), NotificationManager.IMPORTANCE_HIGH)
mgr.createNotificationChannels(listOf(channel, channel2, channel3))
} }
} }
private fun updateBuilder(context: Context): Notification.Builder { @SuppressLint("InlinedApi")
return Notification.Builder(context).apply { fun updateDone(context: Context) {
val bitmap = context.getBitmap(R.drawable.ic_magisk_outline) val pm = context.packageManager
setLargeIcon(bitmap) val intent = pm.getLaunchIntentForPackage(context.packageName) ?: return
if (SDK_INT >= 26) { intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK)
setSmallIcon(bitmap.toIcon()) val flag = PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
setChannelId(UPDATE_NOTIFICATION_CHANNEL) val pending = PendingIntent.getActivity(context, 0, intent, flag)
val builder = if (SDK_INT >= 26) {
Notification.Builder(context, UPDATED_CHANNEL)
.setSmallIcon(context.getBitmap(R.drawable.ic_magisk_outline).toIcon())
} else { } else {
setSmallIcon(R.drawable.ic_magisk_outline) Notification.Builder(context).setPriority(Notification.PRIORITY_HIGH)
} .setSmallIcon(R.drawable.ic_magisk_outline)
} }
.setContentIntent(pending)
.setContentTitle(context.getText(R.string.updated_title))
.setContentText(context.getText(R.string.updated_text))
.setAutoCancel(true)
mgr.notify(APP_UPDATED_NOTIFICATION_ID, builder.build())
} }
fun managerUpdate(context: Context) { fun updateAvailable(context: Context) {
val intent = DownloadService.getPendingIntent(context, Subject.App()) val intent = DownloadService.getPendingIntent(context, Subject.App())
val builder = updateBuilder(context) val bitmap = context.getBitmap(R.drawable.ic_magisk_outline)
val builder = if (SDK_INT >= 26) {
Notification.Builder(context, UPDATE_CHANNEL)
.setSmallIcon(bitmap.toIcon())
} else {
Notification.Builder(context)
.setSmallIcon(R.drawable.ic_magisk_outline)
}
.setLargeIcon(bitmap)
.setContentTitle(context.getString(R.string.magisk_update_title)) .setContentTitle(context.getString(R.string.magisk_update_title))
.setContentText(context.getString(R.string.manager_download_install)) .setContentText(context.getString(R.string.manager_download_install))
.setAutoCancel(true) .setAutoCancel(true)
.setContentIntent(intent) .setContentIntent(intent)
mgr.notify(APK_UPDATE_NOTIFICATION_ID, builder.build()) mgr.notify(APP_UPDATE_NOTIFICATION_ID, builder.build())
} }
fun progress(context: Context, title: CharSequence): Notification.Builder { fun progress(context: Context, title: CharSequence): Notification.Builder {
val builder = if (SDK_INT >= 26) { val builder = if (SDK_INT >= 26) {
Notification.Builder(context, PROGRESS_NOTIFICATION_CHANNEL) Notification.Builder(context, PROGRESS_CHANNEL)
} else { } else {
Notification.Builder(context).setPriority(Notification.PRIORITY_LOW) Notification.Builder(context).setPriority(Notification.PRIORITY_LOW)
} }
builder.setSmallIcon(android.R.drawable.stat_sys_download) .setSmallIcon(android.R.drawable.stat_sys_download)
.setContentTitle(title) .setContentTitle(title)
.setProgress(0, 0, true) .setProgress(0, 0, true)
.setOngoing(true) .setOngoing(true)

View File

@ -190,9 +190,12 @@
<!--Notifications--> <!--Notifications-->
<string name="update_channel">Magisk Updates</string> <string name="update_channel">Magisk Updates</string>
<string name="progress_channel">Progress Notifications</string> <string name="progress_channel">Progress Notifications</string>
<string name="updated_channel">Update Complete</string>
<string name="download_complete">Download complete</string> <string name="download_complete">Download complete</string>
<string name="download_file_error">Error downloading file</string> <string name="download_file_error">Error downloading file</string>
<string name="magisk_update_title">Magisk Update Available!</string> <string name="magisk_update_title">Magisk Update Available!</string>
<string name="updated_title">Magisk Updated</string>
<string name="updated_text">Tap to open app</string>
<!--Toasts, Dialogs--> <!--Toasts, Dialogs-->
<string name="yes">Yes</string> <string name="yes">Yes</string>