Send bitmap to notifications and shortcuts

On API 23+, the platform unifies the way to handle drawable
resources across processes: all drawables can be passed via Icon.
This allows us to send raw bitmap to the system without the need to
specify a resource ID. This means that we are allowed to NOT include
these drawable resources within our stub APK, since our full APK can
draw the images programmatically and send raw bitmaps to the system.
This commit is contained in:
topjohnwu 2019-10-30 01:02:53 -04:00
parent 5e87483f34
commit fdf04f77f2
18 changed files with 152 additions and 110 deletions

View File

@ -73,17 +73,6 @@ fun Context.intent(c: Class<*>): Intent {
} ?: Intent(this, cls)
}
fun resolveRes(idx: Int): Int {
return Info.stub?.resourceMap?.get(idx) ?: when(idx) {
DynAPK.NOTIFICATION -> R.drawable.ic_magisk_outline
DynAPK.DOWNLOAD -> R.drawable.sc_cloud_download
DynAPK.SUPERUSER -> R.drawable.sc_superuser
DynAPK.MODULES -> R.drawable.sc_extension
DynAPK.MAGISKHIDE -> R.drawable.sc_magiskhide
else -> -1
}
}
private open class GlobalResContext(base: Context) : ContextWrapper(base) {
open val mRes: Resources get() = ResourceMgr.resource
private val loader by lazy { javaClass.classLoader!! }

View File

@ -12,12 +12,19 @@ import android.content.pm.PackageManager.*
import android.content.res.Configuration
import android.content.res.Resources
import android.database.Cursor
import android.graphics.Bitmap
import android.graphics.Canvas
import android.graphics.drawable.AdaptiveIconDrawable
import android.graphics.drawable.BitmapDrawable
import android.graphics.drawable.LayerDrawable
import android.net.Uri
import android.os.Build
import android.os.Build.VERSION.SDK_INT
import android.provider.OpenableColumns
import android.view.View
import androidx.annotation.ColorRes
import androidx.annotation.DrawableRes
import androidx.appcompat.content.res.AppCompatResources
import androidx.core.content.ContextCompat
import androidx.core.net.toUri
import com.topjohnwu.magisk.Const
@ -97,6 +104,23 @@ fun Context.rawResource(id: Int) = resources.openRawResource(id)
fun Context.readUri(uri: Uri) =
contentResolver.openInputStream(uri) ?: throw FileNotFoundException()
fun Context.getBitmap(id: Int): Bitmap {
var drawable = AppCompatResources.getDrawable(this, id)!!
if (drawable is BitmapDrawable)
return drawable.bitmap
if (SDK_INT >= 26 && drawable is AdaptiveIconDrawable) {
drawable = LayerDrawable(arrayOf(drawable.background, drawable.foreground))
}
val bitmap = Bitmap.createBitmap(
drawable.intrinsicWidth, drawable.intrinsicHeight,
Bitmap.Config.ARGB_8888
)
val canvas = Canvas(bitmap)
drawable.setBounds(0, 0, canvas.width, canvas.height)
drawable.draw(canvas)
return bitmap
}
fun Intent.startActivity(context: Context) = context.startActivity(this)
fun Intent.startActivityWithRoot() {

View File

@ -1,12 +1,12 @@
package com.topjohnwu.magisk.model.download
import android.annotation.SuppressLint
import android.app.Notification
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.os.Build
import android.webkit.MimeTypeMap
import androidx.core.app.NotificationCompat
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.extensions.chooser
import com.topjohnwu.magisk.extensions.exists
@ -69,14 +69,14 @@ open class DownloadService : RemoteFileService() {
// ---
override fun NotificationCompat.Builder.addActions(subject: DownloadSubject)
override fun Notification.Builder.addActions(subject: DownloadSubject)
= when (subject) {
is Magisk -> addActionsInternal(subject)
is Module -> addActionsInternal(subject)
is Manager -> addActionsInternal(subject)
}
private fun NotificationCompat.Builder.addActionsInternal(subject: Magisk)
private fun Notification.Builder.addActionsInternal(subject: Magisk)
= when (val conf = subject.configuration) {
Download -> this.apply {
fileIntent(subject.file.parentFile!!)
@ -92,7 +92,7 @@ open class DownloadService : RemoteFileService() {
else -> this
}
private fun NotificationCompat.Builder.addActionsInternal(subject: Module)
private fun Notification.Builder.addActionsInternal(subject: Module)
= when (subject.configuration) {
Download -> this.apply {
fileIntent(subject.file.parentFile!!)
@ -106,19 +106,19 @@ open class DownloadService : RemoteFileService() {
else -> this
}
private fun NotificationCompat.Builder.addActionsInternal(subject: Manager)
private fun Notification.Builder.addActionsInternal(subject: Manager)
= when (subject.configuration) {
APK.Upgrade -> setContentIntent(APKInstall.installIntent(context, subject.file))
else -> this
}
@Suppress("ReplaceSingleLineLet")
private fun NotificationCompat.Builder.setContentIntent(intent: Intent) =
private fun Notification.Builder.setContentIntent(intent: Intent) =
PendingIntent.getActivity(context, nextInt(), intent, PendingIntent.FLAG_ONE_SHOT)
.let { setContentIntent(it) }
@Suppress("ReplaceSingleLineLet")
private fun NotificationCompat.Builder.addAction(icon: Int, title: Int, intent: Intent) =
private fun Notification.Builder.addAction(icon: Int, title: Int, intent: Intent) =
PendingIntent.getActivity(context, nextInt(), intent, PendingIntent.FLAG_ONE_SHOT)
.let { addAction(icon, getString(title), it) }

View File

@ -3,22 +3,20 @@ package com.topjohnwu.magisk.model.download
import android.app.Notification
import android.content.Intent
import android.os.IBinder
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import com.topjohnwu.magisk.base.BaseService
import com.topjohnwu.magisk.view.Notifications
import org.koin.core.KoinComponent
import java.util.*
import kotlin.random.Random.Default.nextInt
abstract class NotificationService : BaseService(), KoinComponent {
abstract val defaultNotification: NotificationCompat.Builder
abstract val defaultNotification: Notification.Builder
private val manager by lazy { NotificationManagerCompat.from(this) }
private val hasNotifications get() = notifications.isNotEmpty()
private val notifications =
Collections.synchronizedMap(mutableMapOf<Int, NotificationCompat.Builder>())
Collections.synchronizedMap(mutableMapOf<Int, Notification.Builder>())
override fun onTaskRemoved(rootIntent: Intent?) {
super.onTaskRemoved(rootIntent)
@ -30,7 +28,7 @@ abstract class NotificationService : BaseService(), KoinComponent {
fun update(
id: Int,
body: (NotificationCompat.Builder) -> Unit = {}
body: (Notification.Builder) -> Unit = {}
) {
val notification = notifications.getOrPut(id) { defaultNotification }
@ -43,7 +41,7 @@ abstract class NotificationService : BaseService(), KoinComponent {
protected fun finishNotify(
id: Int,
editBody: (NotificationCompat.Builder) -> NotificationCompat.Builder? = { null }
editBody: (Notification.Builder) -> Notification.Builder? = { null }
) : Int {
val currentNotification = remove(id)?.run(editBody)
@ -62,11 +60,11 @@ abstract class NotificationService : BaseService(), KoinComponent {
// ---
private fun notify(id: Int, notification: Notification) {
manager.notify(id, notification)
Notifications.mgr.notify(id, notification)
}
private fun cancel(id: Int) {
manager.cancel(id)
Notifications.mgr.cancel(id)
}
protected fun remove(id: Int) = notifications.remove(id).also {
@ -84,4 +82,4 @@ abstract class NotificationService : BaseService(), KoinComponent {
// --
override fun onBind(p0: Intent?): IBinder? = null
}
}

View File

@ -1,8 +1,8 @@
package com.topjohnwu.magisk.model.download
import android.app.Activity
import android.app.Notification
import android.content.Intent
import androidx.core.app.NotificationCompat
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.data.network.GithubRawServices
import com.topjohnwu.magisk.di.NullActivity
@ -24,7 +24,7 @@ abstract class RemoteFileService : NotificationService() {
val service: GithubRawServices by inject()
override val defaultNotification: NotificationCompat.Builder
override val defaultNotification
get() = Notifications.progress(this, "")
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
@ -108,8 +108,8 @@ abstract class RemoteFileService : NotificationService() {
@Throws(Throwable::class)
protected abstract fun onFinished(subject: DownloadSubject, id: Int)
protected abstract fun NotificationCompat.Builder.addActions(subject: DownloadSubject)
: NotificationCompat.Builder
protected abstract fun Notification.Builder.addActions(subject: DownloadSubject)
: Notification.Builder
companion object {
const val ARG_URL = "arg_url"

View File

@ -1,35 +1,51 @@
package com.topjohnwu.magisk.view
import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.content.Context
import android.os.Build
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import android.os.Build.VERSION.SDK_INT
import androidx.core.app.TaskStackBuilder
import androidx.core.content.getSystemService
import androidx.core.graphics.drawable.toIcon
import com.topjohnwu.magisk.*
import com.topjohnwu.magisk.Const.ID.PROGRESS_NOTIFICATION_CHANNEL
import com.topjohnwu.magisk.Const.ID.UPDATE_NOTIFICATION_CHANNEL
import com.topjohnwu.magisk.extensions.get
import com.topjohnwu.magisk.extensions.getBitmap
import com.topjohnwu.magisk.model.receiver.GeneralReceiver
import com.topjohnwu.magisk.ui.SplashActivity
object Notifications {
val mgr by lazy { NotificationManagerCompat.from(get()) }
private val icon by lazy { resolveRes(DynAPK.NOTIFICATION) }
val mgr by lazy { get<Context>().getSystemService<NotificationManager>()!! }
fun setup(context: Context) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
mgr.deleteNotificationChannel("magisk_notification")
var channel = NotificationChannel(Const.ID.UPDATE_NOTIFICATION_CHANNEL,
if (SDK_INT >= 26) {
var channel = NotificationChannel(UPDATE_NOTIFICATION_CHANNEL,
context.getString(R.string.update_channel), NotificationManager.IMPORTANCE_DEFAULT)
mgr.createNotificationChannel(channel)
channel = NotificationChannel(Const.ID.PROGRESS_NOTIFICATION_CHANNEL,
channel = NotificationChannel(PROGRESS_NOTIFICATION_CHANNEL,
context.getString(R.string.progress_channel), NotificationManager.IMPORTANCE_LOW)
mgr.createNotificationChannel(channel)
}
}
private fun updateBuilder(context: Context): Notification.Builder {
return Notification.Builder(context).apply {
val bitmap = context.getBitmap(R.drawable.ic_magisk_outline)
setLargeIcon(bitmap)
if (SDK_INT >= 26) {
setSmallIcon(bitmap.toIcon())
setChannelId(UPDATE_NOTIFICATION_CHANNEL)
} else {
setSmallIcon(R.drawable.ic_magisk_outline)
setVibrate(longArrayOf(0, 100, 100, 100))
}
}
}
fun magiskUpdate(context: Context) {
val intent = context.intent(SplashActivity::class.java)
.putExtra(Const.Key.OPEN_SECTION, "magisk")
@ -39,13 +55,11 @@ object Notifications {
val pendingIntent = stackBuilder.getPendingIntent(Const.ID.MAGISK_UPDATE_NOTIFICATION_ID,
PendingIntent.FLAG_UPDATE_CURRENT)
val builder = NotificationCompat.Builder(context, Const.ID.UPDATE_NOTIFICATION_CHANNEL)
builder.setSmallIcon(icon)
.setContentTitle(context.getString(R.string.magisk_update_title))
.setContentText(context.getString(R.string.manager_download_install))
.setVibrate(longArrayOf(0, 100, 100, 100))
.setAutoCancel(true)
.setContentIntent(pendingIntent)
val builder = updateBuilder(context)
.setContentTitle(context.getString(R.string.magisk_update_title))
.setContentText(context.getString(R.string.manager_download_install))
.setAutoCancel(true)
.setContentIntent(pendingIntent)
mgr.notify(Const.ID.MAGISK_UPDATE_NOTIFICATION_ID, builder.build())
}
@ -58,13 +72,11 @@ object Notifications {
val pendingIntent = PendingIntent.getBroadcast(context,
Const.ID.APK_UPDATE_NOTIFICATION_ID, intent, PendingIntent.FLAG_UPDATE_CURRENT)
val builder = NotificationCompat.Builder(context, Const.ID.UPDATE_NOTIFICATION_CHANNEL)
builder.setSmallIcon(icon)
.setContentTitle(context.getString(R.string.manager_update_title))
.setContentText(context.getString(R.string.manager_download_install))
.setVibrate(longArrayOf(0, 100, 100, 100))
.setAutoCancel(true)
.setContentIntent(pendingIntent)
val builder = updateBuilder(context)
.setContentTitle(context.getString(R.string.manager_update_title))
.setContentText(context.getString(R.string.manager_download_install))
.setAutoCancel(true)
.setContentIntent(pendingIntent)
mgr.notify(Const.ID.APK_UPDATE_NOTIFICATION_ID, builder.build())
}
@ -75,23 +87,34 @@ object Notifications {
val pendingIntent = PendingIntent.getBroadcast(context,
Const.ID.DTBO_NOTIFICATION_ID, intent, PendingIntent.FLAG_UPDATE_CURRENT)
val builder = NotificationCompat.Builder(context, Const.ID.UPDATE_NOTIFICATION_CHANNEL)
builder.setSmallIcon(icon)
.setContentTitle(context.getString(R.string.dtbo_patched_title))
.setContentText(context.getString(R.string.dtbo_patched_reboot))
.setVibrate(longArrayOf(0, 100, 100, 100))
.addAction(R.drawable.ic_refresh, context.getString(R.string.reboot), pendingIntent)
val builder = updateBuilder(context)
.setContentTitle(context.getString(R.string.dtbo_patched_title))
.setContentText(context.getString(R.string.dtbo_patched_reboot))
if (SDK_INT >= 23) {
val action = Notification.Action.Builder(
context.getBitmap(R.drawable.ic_refresh).toIcon(),
context.getString(R.string.reboot), pendingIntent).build()
builder.addAction(action)
} else {
builder.addAction(
R.drawable.ic_refresh,
context.getString(R.string.reboot), pendingIntent)
}
mgr.notify(Const.ID.DTBO_NOTIFICATION_ID, builder.build())
}
fun progress(context: Context, title: CharSequence): NotificationCompat.Builder {
val builder = NotificationCompat.Builder(context, Const.ID.PROGRESS_NOTIFICATION_CHANNEL)
builder.setPriority(NotificationCompat.PRIORITY_LOW)
.setSmallIcon(android.R.drawable.stat_sys_download)
.setContentTitle(title)
.setProgress(0, 0, true)
.setOngoing(true)
fun progress(context: Context, title: CharSequence): Notification.Builder {
val builder = if (SDK_INT >= 26) {
Notification.Builder(context, PROGRESS_NOTIFICATION_CHANNEL)
} else {
Notification.Builder(context).setPriority(Notification.PRIORITY_LOW)
}
builder.setSmallIcon(android.R.drawable.stat_sys_download)
.setContentTitle(title)
.setProgress(0, 0, true)
.setOngoing(true)
return builder
}
}

View File

@ -7,7 +7,10 @@ import android.content.pm.ShortcutManager
import android.graphics.drawable.Icon
import android.os.Build
import androidx.annotation.RequiresApi
import androidx.core.graphics.drawable.toAdaptiveIcon
import androidx.core.graphics.drawable.toIcon
import com.topjohnwu.magisk.*
import com.topjohnwu.magisk.extensions.getBitmap
import com.topjohnwu.magisk.ui.SplashActivity
import com.topjohnwu.magisk.utils.Utils
import com.topjohnwu.superuser.Shell
@ -26,47 +29,71 @@ object Shortcuts {
val shortCuts = mutableListOf<ShortcutInfo>()
val root = Shell.rootAccess()
val intent = context.intent(SplashActivity::class.java)
fun getIcon(id: Int): Icon {
return if (Build.VERSION.SDK_INT >= 26)
context.getBitmap(id).toAdaptiveIcon()
else
context.getBitmap(id).toIcon()
}
if (Utils.showSuperUser()) {
shortCuts.add(ShortcutInfo.Builder(context, "superuser")
shortCuts.add(
ShortcutInfo.Builder(context, "superuser")
.setShortLabel(context.getString(R.string.superuser))
.setIntent(Intent(intent)
.setIntent(
Intent(intent)
.putExtra(Const.Key.OPEN_SECTION, "superuser")
.setAction(Intent.ACTION_VIEW)
.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK))
.setIcon(Icon.createWithResource(context, resolveRes(DynAPK.SUPERUSER)))
.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK)
)
.setIcon(getIcon(R.drawable.sc_superuser))
.setRank(0)
.build())
.build()
)
}
if (root && Info.env.magiskHide) {
shortCuts.add(ShortcutInfo.Builder(context, "magiskhide")
shortCuts.add(
ShortcutInfo.Builder(context, "magiskhide")
.setShortLabel(context.getString(R.string.magiskhide))
.setIntent(Intent(intent)
.setIntent(
Intent(intent)
.putExtra(Const.Key.OPEN_SECTION, "magiskhide")
.setAction(Intent.ACTION_VIEW)
.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK))
.setIcon(Icon.createWithResource(context, resolveRes(DynAPK.MAGISKHIDE)))
.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK)
)
.setIcon(getIcon(R.drawable.sc_magiskhide))
.setRank(1)
.build())
.build()
)
}
if (!Config.coreOnly && root && Info.env.magiskVersionCode >= 0) {
shortCuts.add(ShortcutInfo.Builder(context, "modules")
shortCuts.add(
ShortcutInfo.Builder(context, "modules")
.setShortLabel(context.getString(R.string.modules))
.setIntent(Intent(intent)
.setIntent(
Intent(intent)
.putExtra(Const.Key.OPEN_SECTION, "modules")
.setAction(Intent.ACTION_VIEW)
.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK))
.setIcon(Icon.createWithResource(context, resolveRes(DynAPK.MODULES)))
.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK)
)
.setIcon(getIcon(R.drawable.sc_extension))
.setRank(3)
.build())
shortCuts.add(ShortcutInfo.Builder(context, "downloads")
.build()
)
shortCuts.add(
ShortcutInfo.Builder(context, "downloads")
.setShortLabel(context.getString(R.string.downloads))
.setIntent(Intent(intent)
.setIntent(
Intent(intent)
.putExtra(Const.Key.OPEN_SECTION, "downloads")
.setAction(Intent.ACTION_VIEW)
.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK))
.setIcon(Icon.createWithResource(context, resolveRes(DynAPK.DOWNLOAD)))
.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK)
)
.setIcon(getIcon(R.drawable.sc_cloud_download))
.setRank(2)
.build())
.build()
)
}
return shortCuts
}

View File

@ -11,19 +11,11 @@ import static android.os.Build.VERSION.SDK_INT;
public class DynAPK {
private static final int STUB_VERSION = 2;
private static final int STUB_VERSION = 3;
// Indices of the object array
private static final int STUB_VERSION_ENTRY = 0;
private static final int COMPONENT_MAP = 1;
private static final int RESOURCE_MAP = 2;
// Indices of the resource map
public static final int NOTIFICATION = 0;
public static final int DOWNLOAD = 1;
public static final int SUPERUSER = 2;
public static final int MODULES = 3;
public static final int MAGISKHIDE = 4;
private static File dynDir;
private static Method addAssetPath;
@ -53,15 +45,13 @@ public class DynAPK {
Data data = new Data();
data.version = (int) arr[STUB_VERSION_ENTRY];
data.componentMap = (Map<String, String>) arr[COMPONENT_MAP];
data.resourceMap = (int[]) arr[RESOURCE_MAP];
return data;
}
public static Object pack(Data data) {
Object[] arr = new Object[3];
Object[] arr = new Object[2];
arr[STUB_VERSION_ENTRY] = STUB_VERSION;
arr[COMPONENT_MAP] = data.componentMap;
arr[RESOURCE_MAP] = data.resourceMap;
return arr;
}
@ -76,6 +66,5 @@ public class DynAPK {
public static class Data {
public int version;
public Map<String, String> componentMap;
public int[] resourceMap;
}
}

View File

@ -3,8 +3,7 @@ package com.topjohnwu.magisk.obfuscate;
import java.util.HashMap;
import java.util.Map;
import static com.topjohnwu.magisk.DynAPK.*;
import static com.topjohnwu.magisk.R.drawable.*;
import static com.topjohnwu.magisk.DynAPK.Data;
public class Mapping {
private static Map<String, String> map = new HashMap<>();
@ -25,13 +24,6 @@ public class Mapping {
for (Map.Entry<String, String> e : map.entrySet()) {
data.componentMap.put(e.getValue(), e.getKey());
}
int[] res = new int[5];
res[NOTIFICATION] = ic_magisk_outline;
res[SUPERUSER] = sc_superuser;
res[MAGISKHIDE] = sc_magiskhide;
res[DOWNLOAD] = sc_cloud_download;
res[MODULES] = sc_extension;
data.resourceMap = res;
}
public static String get(String name) {