diff --git a/daemon/src/main/java/org/lsposed/lspd/service/ConfigManager.java b/daemon/src/main/java/org/lsposed/lspd/service/ConfigManager.java index b9b3b972..0be41025 100644 --- a/daemon/src/main/java/org/lsposed/lspd/service/ConfigManager.java +++ b/daemon/src/main/java/org/lsposed/lspd/service/ConfigManager.java @@ -91,7 +91,7 @@ public class ConfigManager { private boolean verboseLog = true; private boolean dexObfuscate = false; - private boolean autoAddShortcut = true; + private boolean enableStatusNotification = true; private String miscPath = null; private int managerUid = -1; @@ -228,11 +228,13 @@ public class ConfigManager { dexObfuscate = bool != null && (boolean) bool; bool = config.get("enable_auto_add_shortcut"); - if (bool == null) { - updateModulePrefs("lspd", 0, "config", "enable_auto_add_shortcut", true); - bool = true; + if (bool != null) { + // TODO: remove + updateModulePrefs("lspd", 0, "config", "enable_auto_add_shortcut", null); } - autoAddShortcut = (boolean) bool; + + bool = config.get("enable_status_notification"); + enableStatusNotification = bool == null || (boolean) bool; // Don't migrate to ConfigFileManager, as XSharedPreferences will be restored soon String string = (String) config.get("misc_path"); @@ -906,14 +908,14 @@ public class ConfigManager { return bool != null && (boolean) bool; } - public boolean isAddShortcut() { - Log.d(TAG, "Auto add shortcut=" + autoAddShortcut); - return autoAddShortcut; + public boolean enableStatusNotification() { + Log.d(TAG, "show status notification = " + enableStatusNotification); + return enableStatusNotification; } - public void setAddShortcut(boolean on) { - updateModulePrefs("lspd", 0, "config", "enable_auto_add_shortcut", on); - this.autoAddShortcut = on; + public void setEnableStatusNotification(boolean enable) { + updateModulePrefs("lspd", 0, "config", "enable_status_notification", enable); + enableStatusNotification = enable; } public ParcelFileDescriptor getManagerApk() { diff --git a/daemon/src/main/java/org/lsposed/lspd/service/LSPManagerService.java b/daemon/src/main/java/org/lsposed/lspd/service/LSPManagerService.java index cc4d1f15..76ea3d82 100644 --- a/daemon/src/main/java/org/lsposed/lspd/service/LSPManagerService.java +++ b/daemon/src/main/java/org/lsposed/lspd/service/LSPManagerService.java @@ -24,33 +24,18 @@ import static org.lsposed.lspd.service.ServiceManager.TAG; import static org.lsposed.lspd.service.ServiceManager.getExecutorService; import android.annotation.SuppressLint; -import android.app.INotificationManager; import android.app.IServiceConnection; -import android.app.Notification; -import android.app.NotificationChannel; -import android.app.NotificationManager; -import android.app.PendingIntent; import android.content.AttributionSource; import android.content.ComponentName; import android.content.Context; import android.content.Intent; -import android.content.pm.LauncherApps; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; -import android.content.pm.ParceledListSlice; import android.content.pm.ResolveInfo; -import android.content.pm.ShortcutInfo; -import android.content.pm.ShortcutManager; import android.content.pm.VersionedPackage; -import android.graphics.Bitmap; -import android.graphics.Canvas; -import android.graphics.Color; -import android.graphics.drawable.Icon; import android.net.Uri; import android.os.Build; import android.os.Bundle; -import android.os.Handler; -import android.os.HandlerThread; import android.os.IBinder; import android.os.ParcelFileDescriptor; import android.os.RemoteException; @@ -64,19 +49,14 @@ import android.view.IWindowManager; import androidx.annotation.NonNull; import org.lsposed.daemon.BuildConfig; -import org.lsposed.daemon.R; import org.lsposed.lspd.ILSPManagerService; import org.lsposed.lspd.models.Application; import org.lsposed.lspd.models.UserInfo; -import org.lsposed.lspd.util.FakeContext; import org.lsposed.lspd.util.Utils; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; -import java.lang.reflect.InvocationTargetException; -import java.util.Collections; -import java.util.HashMap; import java.util.LinkedList; import java.util.List; import java.util.UUID; @@ -88,23 +68,9 @@ import rikka.parcelablelist.ParcelableListSlice; public class LSPManagerService extends ILSPManagerService.Stub { // this maybe useful when obtaining the manager binder private static String RANDOM_UUID = null; - private static final String SHORTCUT_ID = "org.lsposed.manager.shortcut"; - public static final String CHANNEL_ID = "lsposed"; - public static final String CHANNEL_NAME = "LSPosed Manager"; - public static final int CHANNEL_IMP = NotificationManager.IMPORTANCE_HIGH; - private static final HashMap notificationIds = new HashMap<>(); - private static int previousNotificationId = 2000; - - private static final HandlerThread worker = new HandlerThread("manager worker"); - private static final Handler workerHandler; private static Intent managerIntent = null; - static { - worker.start(); - workerHandler = new Handler(worker.getLooper()); - } - public class ManagerGuard implements IBinder.DeathRecipient { private final @NonNull IBinder binder; @@ -162,162 +128,47 @@ public class LSPManagerService extends ILSPManagerService.Stub { LSPManagerService() { } - private static Icon getIcon(int res) { - var icon = ConfigFileManager.getResources().getDrawable(res, ConfigFileManager.getResources().newTheme()); - var bitmap = Bitmap.createBitmap(icon.getIntrinsicWidth(), icon.getIntrinsicHeight(), Bitmap.Config.ARGB_8888); - icon.setBounds(0, 0, icon.getIntrinsicWidth(), icon.getIntrinsicHeight()); - icon.draw(new Canvas(bitmap)); - return Icon.createWithBitmap(bitmap); - } - - private static Icon getManagerIcon() { + private static Intent getManagerIntent() { + if (managerIntent != null) return managerIntent; try { - return getIcon(R.mipmap.ic_launcher); - } catch (Throwable e) { - return getIcon(R.drawable.ic_launcher); - } - } - - private static Icon getNotificationIcon() { - return getIcon(R.drawable.ic_outline_extension_24); - } - - static Intent getManagerIntent() { - try { - if (managerIntent == null) { - var intent = PackageService.getLaunchIntentForPackage(BuildConfig.MANAGER_INJECTED_PKG_NAME); - if (intent == null) { - var pkgInfo = PackageService.getPackageInfo(BuildConfig.MANAGER_INJECTED_PKG_NAME, PackageManager.GET_ACTIVITIES, 0); - if (pkgInfo != null && pkgInfo.activities != null && pkgInfo.activities.length > 0) { - for (var activityInfo : pkgInfo.activities) { - if (activityInfo.processName.equals(activityInfo.packageName)) { - intent = new Intent(); - intent.setComponent(new ComponentName(activityInfo.packageName, activityInfo.name)); - intent.setAction(Intent.ACTION_MAIN); - break; - } + var intent = PackageService.getLaunchIntentForPackage(BuildConfig.MANAGER_INJECTED_PKG_NAME); + if (intent == null) { + var pkgInfo = PackageService.getPackageInfo(BuildConfig.MANAGER_INJECTED_PKG_NAME, PackageManager.GET_ACTIVITIES, 0); + if (pkgInfo != null && pkgInfo.activities != null && pkgInfo.activities.length > 0) { + for (var activityInfo : pkgInfo.activities) { + if (activityInfo.processName.equals(activityInfo.packageName)) { + intent = new Intent(); + intent.setComponent(new ComponentName(activityInfo.packageName, activityInfo.name)); + intent.setAction(Intent.ACTION_MAIN); + break; } } } - if (intent != null) { - if (intent.getCategories() != null) intent.getCategories().clear(); - intent.addCategory("org.lsposed.manager.LAUNCH_MANAGER"); - intent.setPackage(BuildConfig.MANAGER_INJECTED_PKG_NAME); - managerIntent = (Intent) intent.clone(); - } } - } catch (Throwable e) { + if (intent != null) { + if (intent.getCategories() != null) intent.getCategories().clear(); + intent.addCategory("org.lsposed.manager.LAUNCH_MANAGER"); + intent.setPackage(BuildConfig.MANAGER_INJECTED_PKG_NAME); + managerIntent = new Intent(intent); + } + } catch (RemoteException e) { Log.e(TAG, "get Intent", e); } return managerIntent; - } - public static PendingIntent getNotificationIntent(String modulePackageName, int moduleUserId) { + static void openManager(Uri withData) { + var intent = getManagerIntent(); + if (intent == null) return; + intent = new Intent(intent); + intent.setData(withData); try { - var intent = (Intent) getManagerIntent().clone(); - intent.setData(new Uri.Builder().scheme("module").encodedAuthority(modulePackageName + ":" + moduleUserId).build()); - return PendingIntent.getActivity(new FakeContext(), 0, intent, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE); - } catch (Throwable e) { - Log.e(TAG, "get notification intent", e); - return null; - } - } - - private static String getNotificationIdKey(String modulePackageName, int moduleUserId) { - return modulePackageName + ":" + moduleUserId; - } - - private static int getAutoIncrementNotificationId() { - // previousNotificationId start with 2001 - var idValue = previousNotificationId++; - // Templates that may conflict with system ids after 2000 - // Copied from https://android.googlesource.com/platform/frameworks/base/+/master/proto/src/system_messages.proto - var NOTE_NETWORK_AVAILABLE = 17303299; - var NOTE_REMOTE_BUGREPORT = 678432343; - var NOTE_STORAGE_PUBLIC = 0x53505542; - var NOTE_STORAGE_PRIVATE = 0x53505256; - var NOTE_STORAGE_DISK = 0x5344534b; - var NOTE_STORAGE_MOVE = 0x534d4f56; - // If auto created id is conflict, recreate it - if (idValue == NOTE_NETWORK_AVAILABLE || - idValue == NOTE_REMOTE_BUGREPORT || - idValue == NOTE_STORAGE_PUBLIC || - idValue == NOTE_STORAGE_PRIVATE || - idValue == NOTE_STORAGE_DISK || - idValue == NOTE_STORAGE_MOVE) { - return getAutoIncrementNotificationId(); - } else { - return idValue; - } - } - - private static int pushAndGetNotificationId(String modulePackageName, int moduleUserId) { - var idKey = getNotificationIdKey(modulePackageName, moduleUserId); - // If there is a new notification, put a new notification id into map - return notificationIds.computeIfAbsent(idKey, key -> getAutoIncrementNotificationId()); - } - - public static void showNotification(String modulePackageName, - int moduleUserId, - boolean enabled, - boolean systemModule) { - try { - var context = new FakeContext(); - var userInfo = UserService.getUserInfo(moduleUserId); - String userName = userInfo != null ? userInfo.name : String.valueOf(moduleUserId); - String title = context.getString(enabled ? systemModule ? - R.string.xposed_module_updated_notification_title_system : - R.string.xposed_module_updated_notification_title : - R.string.module_is_not_activated_yet); - String content = context.getString(enabled ? systemModule ? - R.string.xposed_module_updated_notification_content_system : - R.string.xposed_module_updated_notification_content : - (moduleUserId == 0 ? - R.string.module_is_not_activated_yet_main_user_detailed : - R.string.module_is_not_activated_yet_multi_user_detailed), modulePackageName, userName); - - var style = new Notification.BigTextStyle(); - style.bigText(content); - - var notification = new Notification.Builder(context, CHANNEL_ID) - .setContentTitle(title) - .setContentText(content) - .setSmallIcon(getNotificationIcon()) - .setColor(Color.BLUE) - .setContentIntent(getNotificationIntent(modulePackageName, moduleUserId)) - .setAutoCancel(true) - .setStyle(style) - .build(); - notification.extras.putString("android.substName", "LSPosed"); - var im = INotificationManager.Stub.asInterface(android.os.ServiceManager.getService("notification")); - final NotificationChannel channel = - new NotificationChannel(CHANNEL_ID, CHANNEL_NAME, CHANNEL_IMP); - im.createNotificationChannels("android", - new ParceledListSlice<>(Collections.singletonList(channel))); - im.enqueueNotificationWithTag("android", "android", modulePackageName, - pushAndGetNotificationId(modulePackageName, moduleUserId), - notification, 0); - } catch (Throwable e) { - Log.e(TAG, "post notification", e); - } - } - - public static void cancelNotification(String modulePackageName, int moduleUserId) { - try { - var idKey = getNotificationIdKey(modulePackageName, moduleUserId); - var idValue = notificationIds.get(idKey); - if (idValue == null) return; - var im = INotificationManager.Stub.asInterface(android.os.ServiceManager.getService("notification")); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { - im.cancelNotificationWithTag("android", "android", modulePackageName, idValue, 0); - } else { - im.cancelNotificationWithTag("android", modulePackageName, idValue, 0); - } - // Remove the notification id when the notification is canceled or current module app was uninstalled - notificationIds.remove(idKey); - } catch (Throwable e) { - Log.e(TAG, "cancel notification", e); + var userInfo = ActivityManagerService.getCurrentUser(); + if (userInfo == null || userInfo.id != 0) return; + ActivityManagerService.startActivityAsUserWithFeature("android", null, + intent, intent.getType(), null, null, 0, 0, null, null, 0); + } catch (RemoteException e) { + Log.e(TAG, "open manager", e); } } @@ -338,75 +189,11 @@ public class LSPManagerService extends ILSPManagerService.Stub { null, null, 0, null, null, null, -1, null, true, false, 0); - } catch (Throwable t) { + } catch (RemoteException t) { Log.e(TAG, "Broadcast to manager failed: ", t); } } - public static void createOrUpdateShortcut(boolean force) { - workerHandler.post(() -> createOrUpdateShortcutInternal(force, true)); - } - - public static void createOrUpdateShortcut(boolean force, boolean shouldCreate) { - workerHandler.post(() -> createOrUpdateShortcutInternal(force, shouldCreate)); - } - - private synchronized static void createOrUpdateShortcutInternal(boolean force, boolean shouldCreate) { - try { - while (!UserService.isUserUnlocked(0)) { - Log.d(TAG, "user is not yet unlocked, waiting for 1s..."); - Thread.sleep(1000); - } - var smCtor = ShortcutManager.class.getDeclaredConstructor(Context.class); - smCtor.setAccessible(true); - var context = new FakeContext("com.android.settings"); - var sm = smCtor.newInstance(context); - if (!sm.isRequestPinShortcutSupported()) { - Log.d(TAG, "pinned shortcut not supported, skipping"); - return; - } - var intent = getManagerIntent(); - var settingIntent = PackageService.getLaunchIntentForPackage("com.android.settings"); - var componentName = settingIntent != null ? settingIntent.getComponent() : new ComponentName("com.android.settings", "android.__dummy__"); - var shortcut = new ShortcutInfo.Builder(context, SHORTCUT_ID) - .setShortLabel("LSPosed") - .setLongLabel("LSPosed") - .setIntent(intent) - .setActivity(componentName) - .setCategories(intent.getCategories()) - .setIcon(getManagerIcon()) - .build(); - - for (var shortcutInfo : sm.getPinnedShortcuts()) { - if (SHORTCUT_ID.equals(shortcutInfo.getId()) && shortcutInfo.isPinned()) { - var shortcutIntent = sm.createShortcutResultIntent(shortcutInfo); - var request = (LauncherApps.PinItemRequest) shortcutIntent.getParcelableExtra(LauncherApps.EXTRA_PIN_ITEM_REQUEST); - var requestInfo = request.getShortcutInfo(); - // https://cs.android.com/android/platform/superproject/+/android-8.1.0_r1:frameworks/base/services/core/java/com/android/server/pm/ShortcutRequestPinProcessor.java;drc=4ad6b57700bef4c484021f49e018117046562e6b;l=337 - if (requestInfo.isPinned()) { - Log.d(TAG, "shortcut exists, updating"); - sm.updateShortcuts(Collections.singletonList(shortcut)); - return; - } - } - } - var configManager = ConfigManager.getInstance(); - if (!force && configManager.isManagerInstalled()) { - Log.d(TAG, "Manager has installed, skip adding shortcut"); - return; - } - // Only existing shortcuts are updated when system settings - // are changed and no new shortcuts are requested - if (!force && !shouldCreate) return; - if (configManager.isAddShortcut()) { - sm.requestPinShortcut(shortcut, null); - Log.d(TAG, "done add shortcut"); - } - } catch (Throwable e) { - Log.e(TAG, "add shortcut", e); - } - } - public ManagerGuard guardSnapshot() { var snapshot = guard; return snapshot != null && snapshot.isAlive() ? snapshot : null; @@ -509,7 +296,7 @@ public class LSPManagerService extends ILSPManagerService.Stub { ActivityManagerService.forceStopPackage(packageName, 0); Log.d(TAG, "stopped old package"); if (addUUID) { - intent = (Intent) intent.clone(); + intent = new Intent(intent); RANDOM_UUID = UUID.randomUUID().toString(); intent.addCategory(RANDOM_UUID); } @@ -618,17 +405,6 @@ public class LSPManagerService extends ILSPManagerService.Stub { return ConfigManager.getInstance().disableModule(packageName); } - @Override - public boolean isAddShortcut() { - return ConfigManager.getInstance().isAddShortcut(); - } - - @Override - public void setAddShortcut(boolean enabled) { - ConfigManager.getInstance().setAddShortcut(enabled); - if (enabled) createOrUpdateShortcut(true); - } - @Override public boolean isVerboseLog() { return ConfigManager.getInstance().verboseLog(); @@ -646,7 +422,7 @@ public class LSPManagerService extends ILSPManagerService.Stub { @Override public ParcelFileDescriptor getModulesLog() { - workerHandler.post(() -> ServiceManager.getLogcatService().checkLogFile()); + ServiceManager.getLogcatService().checkLogFile(); return ConfigManager.getInstance().getModulesLog(); } @@ -674,10 +450,13 @@ public class LSPManagerService extends ILSPManagerService.Stub { @Override public boolean uninstallPackage(String packageName, int userId) throws RemoteException { try { - if (ActivityManagerService.startUserInBackground(userId)) - return PackageService.uninstallPackage(new VersionedPackage(packageName, PackageManager.VERSION_CODE_HIGHEST), userId); - else return false; - } catch (InterruptedException | InvocationTargetException | NoSuchMethodException | InstantiationException | IllegalAccessException e) { + if (ActivityManagerService.startUserInBackground(userId)) { + var pkg = new VersionedPackage(packageName, PackageManager.VERSION_CODE_HIGHEST); + return PackageService.uninstallPackage(pkg, userId); + } else { + return false; + } + } catch (InterruptedException | ReflectiveOperationException e) { Log.e(TAG, e.getMessage(), e); return false; } @@ -727,7 +506,7 @@ public class LSPManagerService extends ILSPManagerService.Stub { if (parent < 0) return -1; if (currentUser.id != parent) { if (!ActivityManagerService.switchUser(parent)) return -1; - var window = android.os.ServiceManager.getService("window"); + var window = android.os.ServiceManager.getService(Context.WINDOW_SERVICE); if (window != null) { var wm = IWindowManager.Stub.asInterface(window); wm.lockNow(null); @@ -759,9 +538,9 @@ public class LSPManagerService extends ILSPManagerService.Stub { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { contentProvider.call(new AttributionSource.Builder(1000).setPackageName("android").build(), "settings", "PUT_global", "show_hidden_icon_apps_enabled", args); - } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + } else if (Build.VERSION.SDK_INT == Build.VERSION_CODES.R) { contentProvider.call("android", null, "settings", "PUT_global", "show_hidden_icon_apps_enabled", args); - } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + } else if (Build.VERSION.SDK_INT == Build.VERSION_CODES.Q) { contentProvider.call("android", "settings", "PUT_global", "show_hidden_icon_apps_enabled", args); } } catch (NoSuchMethodError e) { @@ -784,12 +563,6 @@ public class LSPManagerService extends ILSPManagerService.Stub { stopAndStartActivity(BuildConfig.MANAGER_INJECTED_PKG_NAME, intent, false); } - @Override - public void createShortcut() { - createOrUpdateShortcut(true); - setAddShortcut(true); - } - @Override public List getDenyListPackages() { return ConfigManager.getInstance().getDenyListPackages(); @@ -826,6 +599,26 @@ public class LSPManagerService extends ILSPManagerService.Stub { PackageService.clearApplicationProfileData(packageName); } + @Override + public Intent getLaunchIntentForManager() { + return getManagerIntent(); + } + + @Override + public boolean enableStatusNotification() { + return ConfigManager.getInstance().enableStatusNotification(); + } + + @Override + public void setEnableStatusNotification(boolean enable) { + ConfigManager.getInstance().setEnableStatusNotification(enable); + if (enable) { + LSPNotificationManager.notifyStatusNotification(); + } else { + LSPNotificationManager.cancelStatusNotification(); + } + } + @Override public boolean performDexOptMode(String packageName) throws RemoteException { return PackageService.performDexOptMode(packageName); diff --git a/daemon/src/main/java/org/lsposed/lspd/service/LSPNotificationManager.java b/daemon/src/main/java/org/lsposed/lspd/service/LSPNotificationManager.java new file mode 100644 index 00000000..e2a08396 --- /dev/null +++ b/daemon/src/main/java/org/lsposed/lspd/service/LSPNotificationManager.java @@ -0,0 +1,225 @@ +package org.lsposed.lspd.service; + +import static org.lsposed.lspd.service.ServiceManager.TAG; + +import android.app.INotificationManager; +import android.app.Notification; +import android.app.NotificationChannel; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.content.Context; +import android.content.Intent; +import android.content.pm.ParceledListSlice; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.drawable.AdaptiveIconDrawable; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.Drawable; +import android.graphics.drawable.Icon; +import android.graphics.drawable.LayerDrawable; +import android.net.Uri; +import android.os.Build; +import android.os.IBinder; +import android.os.RemoteException; +import android.util.Log; + +import org.lsposed.daemon.R; +import org.lsposed.lspd.util.FakeContext; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.UUID; + +public class LSPNotificationManager { + private static final String UPDATED_CHANNEL_ID = "lsposed_module_updated"; + private static final String STATUS_CHANNEL_ID = "lsposed_status"; + private static final int STATUS_NOTIFICATION_ID = 2000; + + private static final HashMap notificationIds = new HashMap<>(); + private static int previousNotificationId = STATUS_NOTIFICATION_ID; + + static String openManagerAction = UUID.randomUUID().toString(); + + private static INotificationManager notificationManager = null; + private static IBinder binder = null; + + private static final IBinder.DeathRecipient recipient = new IBinder.DeathRecipient() { + @Override + public void binderDied() { + Log.w(TAG, "nm is dead"); + binder.unlinkToDeath(this, 0); + binder = null; + notificationManager = null; + } + }; + + private static INotificationManager getNotificationManager() throws RemoteException { + if (binder == null || notificationManager == null) { + binder = android.os.ServiceManager.getService(Context.NOTIFICATION_SERVICE); + binder.linkToDeath(recipient, 0); + notificationManager = INotificationManager.Stub.asInterface(binder); + } + return notificationManager; + } + + private static Bitmap getBitmap(int id) { + var r = ConfigFileManager.getResources(); + var res = r.getDrawable(id, r.newTheme()); + if (res instanceof BitmapDrawable) { + return ((BitmapDrawable) res).getBitmap(); + } else { + if (res instanceof AdaptiveIconDrawable) { + var layers = new Drawable[]{((AdaptiveIconDrawable) res).getBackground(), + ((AdaptiveIconDrawable) res).getForeground()}; + res = new LayerDrawable(layers); + } + var bitmap = Bitmap.createBitmap(res.getIntrinsicWidth(), + res.getIntrinsicHeight(), Bitmap.Config.ARGB_8888); + var canvas = new Canvas(bitmap); + res.setBounds(0, 0, canvas.getWidth(), canvas.getHeight()); + res.draw(canvas); + return bitmap; + } + } + + private static Icon getNotificationIcon() { + return Icon.createWithBitmap(getBitmap(R.drawable.ic_notification)); + } + + private static void createNotificationChannel(INotificationManager nm) throws RemoteException { + var context = new FakeContext(); + var list = new ArrayList(); + + var updated = new NotificationChannel(UPDATED_CHANNEL_ID, + context.getString(R.string.module_updated_channel_name), + NotificationManager.IMPORTANCE_HIGH); + updated.setShowBadge(false); + list.add(updated); + + var status = new NotificationChannel(STATUS_CHANNEL_ID, + context.getString(R.string.status_channel_name), + NotificationManager.IMPORTANCE_MIN); + status.setShowBadge(false); + list.add(status); + + nm.createNotificationChannelsForPackage("android", 1000, new ParceledListSlice<>(list)); + } + + static void notifyStatusNotification() { + var intent = new Intent(openManagerAction); + var context = new FakeContext(); + int flags = PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE; + var notification = new Notification.Builder(context, STATUS_CHANNEL_ID) + .setContentTitle(context.getString(R.string.lsposed_running_notification_title)) + .setContentText(context.getString(R.string.lsposed_running_notification_content)) + .setSmallIcon(getNotificationIcon()) + .setContentIntent(PendingIntent.getBroadcast(context, 1, intent, flags)) + .setVisibility(Notification.VISIBILITY_SECRET) + .setColor(0xFFF48FB1) + .setOngoing(true) + .setAutoCancel(false) + .build(); + notification.extras.putString("android.substName", "LSPosed"); + try { + var nm = getNotificationManager(); + createNotificationChannel(nm); + nm.enqueueNotificationWithTag("android", "android", null, + STATUS_NOTIFICATION_ID, notification, 0); + } catch (RemoteException e) { + Log.e(TAG, "notifyStatusNotification: ", e); + } + } + + static void cancelStatusNotification() { + try { + var nm = getNotificationManager(); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + nm.cancelNotificationWithTag("android", "android", null, STATUS_NOTIFICATION_ID, 0); + } else { + nm.cancelNotificationWithTag("android", null, STATUS_NOTIFICATION_ID, 0); + } + } catch (RemoteException e) { + Log.e(TAG, "cancelStatusNotification: ", e); + } + } + + private static PendingIntent getModuleIntent(String modulePackageName, int moduleUserId) { + var intent = new Intent(openManagerAction); + intent.setData(new Uri.Builder().scheme("module").encodedAuthority(modulePackageName + ":" + moduleUserId).build()); + int flags = PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE; + return PendingIntent.getBroadcast(new FakeContext(), 3, intent, flags); + } + + private static String getNotificationIdKey(String modulePackageName, int moduleUserId) { + return modulePackageName + ":" + moduleUserId; + } + + private static int pushAndGetNotificationId(String modulePackageName, int moduleUserId) { + var idKey = getNotificationIdKey(modulePackageName, moduleUserId); + // previousNotificationId start with 2001 + // https://android.googlesource.com/platform/frameworks/base/+/master/proto/src/system_messages.proto + // https://android.googlesource.com/platform/system/core/+/master/libcutils/include/private/android_filesystem_config.h + // (AID_APP_END - AID_APP_START) x10 = 100000 < NOTE_NETWORK_AVAILABLE + return notificationIds.computeIfAbsent(idKey, key -> previousNotificationId++); + } + + static void notifyModuleUpdated(String modulePackageName, + int moduleUserId, + boolean enabled, + boolean systemModule) { + try { + var context = new FakeContext(); + var userInfo = UserService.getUserInfo(moduleUserId); + String userName = userInfo != null ? userInfo.name : String.valueOf(moduleUserId); + String title = context.getString(enabled ? systemModule ? + R.string.xposed_module_updated_notification_title_system : + R.string.xposed_module_updated_notification_title : + R.string.module_is_not_activated_yet); + String content = context.getString(enabled ? systemModule ? + R.string.xposed_module_updated_notification_content_system : + R.string.xposed_module_updated_notification_content : + (moduleUserId == 0 ? + R.string.module_is_not_activated_yet_main_user_detailed : + R.string.module_is_not_activated_yet_multi_user_detailed), modulePackageName, userName); + + var style = new Notification.BigTextStyle(); + style.bigText(content); + + var notification = new Notification.Builder(context, UPDATED_CHANNEL_ID) + .setContentTitle(title) + .setContentText(content) + .setSmallIcon(getNotificationIcon()) + .setContentIntent(getModuleIntent(modulePackageName, moduleUserId)) + .setVisibility(Notification.VISIBILITY_SECRET) + .setColor(0xFFF48FB1) + .setAutoCancel(true) + .setStyle(style) + .build(); + notification.extras.putString("android.substName", "LSPosed"); + var nm = getNotificationManager(); + createNotificationChannel(nm); + nm.enqueueNotificationWithTag("android", "android", modulePackageName, + pushAndGetNotificationId(modulePackageName, moduleUserId), + notification, 0); + } catch (Throwable e) { + Log.e(TAG, "notify module updated", e); + } + } + + static void cancelUpdatedNotification(String modulePackageName, int moduleUserId) { + try { + var idKey = getNotificationIdKey(modulePackageName, moduleUserId); + var idValue = notificationIds.get(idKey); + if (idValue == null) return; + var nm = getNotificationManager(); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + nm.cancelNotificationWithTag("android", "android", modulePackageName, idValue, 0); + } else { + nm.cancelNotificationWithTag("android", modulePackageName, idValue, 0); + } + notificationIds.remove(idKey); + } catch (RemoteException e) { + Log.e(TAG, "cancel notification", e); + } + } +} diff --git a/daemon/src/main/java/org/lsposed/lspd/service/LSPosedService.java b/daemon/src/main/java/org/lsposed/lspd/service/LSPosedService.java index e54d867a..866979ea 100644 --- a/daemon/src/main/java/org/lsposed/lspd/service/LSPosedService.java +++ b/daemon/src/main/java/org/lsposed/lspd/service/LSPosedService.java @@ -33,11 +33,14 @@ import android.net.Uri; import android.os.Binder; import android.os.Bundle; import android.os.IBinder; +import android.os.RemoteException; import android.util.Log; import org.lsposed.daemon.BuildConfig; import java.util.Arrays; +import java.util.List; +import java.util.function.Consumer; public class LSPosedService extends ILSPosedService.Stub { private static final int AID_NOBODY = 9999; @@ -77,7 +80,7 @@ public class LSPosedService extends ILSPosedService.Stub { * However, PACKAGE_REMOVED will be triggered by `pm hide`, so we use UID_REMOVED instead. */ - synchronized public void dispatchPackageChanged(Intent intent) { + private void dispatchPackageChanged(Intent intent) { if (intent == null) return; int uid = intent.getIntExtra(Intent.EXTRA_UID, AID_NOBODY); if (uid == AID_NOBODY || uid <= 0) return; @@ -109,13 +112,15 @@ public class LSPosedService extends ILSPosedService.Stub { isXposedModule = true; broadcastAndShowNotification(moduleName, userId, intent, true); } - // Anyway, canceled the notification - if (moduleName != null) LSPManagerService.cancelNotification(moduleName, userId); + if (moduleName != null) { + LSPNotificationManager.cancelUpdatedNotification(moduleName, userId); + } break; } case Intent.ACTION_PACKAGE_REMOVED: - // Anyway, canceled the notification - if (moduleName != null) LSPManagerService.cancelNotification(moduleName, userId); + if (moduleName != null) { + LSPNotificationManager.cancelUpdatedNotification(moduleName, userId); + } break; case Intent.ACTION_PACKAGE_ADDED: case Intent.ACTION_PACKAGE_CHANGED: { @@ -159,12 +164,7 @@ public class LSPosedService extends ILSPosedService.Stub { if (BuildConfig.DEFAULT_MANAGER_PACKAGE_NAME.equals(moduleName) && userId == 0) { Log.d(TAG, "Manager updated"); - try { - ConfigManager.getInstance().updateManager(removed); - LSPManagerService.createOrUpdateShortcut(false); - } catch (Throwable e) { - Log.e(TAG, Log.getStackTraceString(e)); - } + ConfigManager.getInstance().updateManager(removed); } } @@ -183,225 +183,136 @@ public class LSPosedService extends ILSPosedService.Stub { scope.parallelStream().anyMatch(app -> app.packageName.equals("android")); boolean enabled = Arrays.asList(enabledModules).contains(packageName); if (!(Intent.ACTION_UID_REMOVED.equals(action) || Intent.ACTION_PACKAGE_FULLY_REMOVED.equals(action) || allUsers)) - LSPManagerService.showNotification(packageName, userId, enabled, systemModule); + LSPNotificationManager.notifyModuleUpdated(packageName, userId, enabled, systemModule); } } - synchronized public void dispatchUserChanged(Intent intent) { + private void dispatchUserChanged(Intent intent) { if (intent == null) return; int uid = intent.getIntExtra(EXTRA_USER_HANDLE, AID_NOBODY); if (uid == AID_NOBODY || uid <= 0) return; + LSPManagerService.broadcastIntent(intent); + } + + private void dispatchBootCompleted(Intent intent) { try { - LSPManagerService.broadcastIntent(intent); - } catch (Throwable e) { - Log.e(TAG, "dispatch user info changed", e); + var am = ActivityManagerService.getActivityManager(); + if (am != null) am.setActivityController(null, false); + } catch (RemoteException e) { + Log.e(TAG, "setActivityController", e); + } + var configManager = ConfigManager.getInstance(); + if (configManager.enableStatusNotification()) { + LSPNotificationManager.notifyStatusNotification(); } } - synchronized public void dispatchUserUnlocked(Intent intent) { - try { - LSPManagerService.createOrUpdateShortcut(false); - } catch (Throwable e) { - Log.e(TAG, "dispatch user unlocked", e); - } + private void dispatchConfigurationChanged(Intent intent) { + ConfigFileManager.reloadConfiguration(); } - synchronized public void dispatchConfigurationChanged(Intent intent) { - try { - ConfigFileManager.reloadConfiguration(); - LSPManagerService.createOrUpdateShortcut(false, false); - } catch (Throwable e) { - Log.e(TAG, "dispatch configuration changed", e); - } + private void dispatchSecretCodeReceive(Intent i) { + LSPManagerService.openManager(null); } - synchronized public void dispatchSecretCodeReceive() { - Intent intent = LSPManagerService.getManagerIntent(); - try { - var userInfo = ActivityManagerService.getCurrentUser(); - if (userInfo != null) { - var userId = userInfo.id; - if (userId == 0) { - ActivityManagerService.startActivityAsUserWithFeature("android", null, - intent, intent.getType(), null, null, 0, 0, null, null, userId); - LSPManagerService.createOrUpdateShortcut(false); + private void dispatchOpenManager(Intent intent) { + LSPManagerService.openManager(intent.getData()); + } + + private void registerReceiver(List filters, String requiredPermission, int userId, Consumer task) { + var receiver = new IIntentReceiver.Stub() { + @Override + public void performReceive(Intent intent, int resultCode, String data, Bundle extras, boolean ordered, boolean sticky, int sendingUser) { + getExecutorService().submit(() -> task.accept(intent)); + if (!ordered) return; + try { + ActivityManagerService.finishReceiver(this, resultCode, data, extras, false, intent.getFlags()); + } catch (RemoteException e) { + Log.e(TAG, "finish receiver", e); } } - } catch (Throwable e) { - Log.e(TAG, "dispatch secret code received", e); + }; + try { + for (var filter : filters) { + ActivityManagerService.registerReceiver("android", null, receiver, filter, requiredPermission, userId, 0); + } + } catch (RemoteException e) { + Log.e(TAG, "register receiver", e); } } + private void registerReceiver(List filters, int userId, Consumer task) { + registerReceiver(filters, null, userId, task); + } + private void registerPackageReceiver() { - try { - IntentFilter packageFilter = new IntentFilter(); - packageFilter.addAction(Intent.ACTION_PACKAGE_ADDED); - packageFilter.addAction(Intent.ACTION_PACKAGE_CHANGED); - packageFilter.addAction(Intent.ACTION_PACKAGE_FULLY_REMOVED); - packageFilter.addDataScheme("package"); + var packageFilter = new IntentFilter(); + packageFilter.addAction(Intent.ACTION_PACKAGE_ADDED); + packageFilter.addAction(Intent.ACTION_PACKAGE_CHANGED); + packageFilter.addAction(Intent.ACTION_PACKAGE_FULLY_REMOVED); + packageFilter.addDataScheme("package"); - IntentFilter uidFilter = new IntentFilter(); - uidFilter.addAction(Intent.ACTION_UID_REMOVED); + var uidFilter = new IntentFilter(Intent.ACTION_UID_REMOVED); - var receiver = new IIntentReceiver.Stub() { - @Override - public void performReceive(Intent intent, int resultCode, String data, Bundle extras, boolean ordered, boolean sticky, int sendingUser) { - getExecutorService().submit(() -> dispatchPackageChanged(intent)); - if (!ordered) return; - try { - ActivityManagerService.finishReceiver(this, resultCode, data, extras, false, intent.getFlags()); - } catch (Throwable e) { - Log.e(TAG, "finish receiver", e); - } - } - }; - - ActivityManagerService.registerReceiver("android", null, receiver, packageFilter, null, -1, 0); - ActivityManagerService.registerReceiver("android", null, receiver, uidFilter, null, -1, 0); - } catch (Throwable e) { - Log.e(TAG, "register package receiver", e); - } + registerReceiver(List.of(packageFilter, uidFilter), -1, this::dispatchPackageChanged); Log.d(TAG, "registered package receiver"); } - private void registerUnlockReceiver() { - try { - IntentFilter intentFilter = new IntentFilter(); - intentFilter.addAction(Intent.ACTION_USER_UNLOCKED); - - ActivityManagerService.registerReceiver("android", null, new IIntentReceiver.Stub() { - @Override - public void performReceive(Intent intent, int resultCode, String data, Bundle extras, boolean ordered, boolean sticky, int sendingUser) { - getExecutorService().submit(() -> dispatchUserUnlocked(intent)); - if (!ordered) return; - try { - ActivityManagerService.finishReceiver(this, resultCode, data, extras, false, intent.getFlags()); - } catch (Throwable e) { - Log.e(TAG, "finish receiver", e); - } - } - }, intentFilter, null, 0, 0); - } catch (Throwable e) { - Log.e(TAG, "register unlock receiver", e); - } - Log.d(TAG, "registered unlock receiver"); - } - private void registerConfigurationReceiver() { - try { - IntentFilter intentFilter = new IntentFilter(); - intentFilter.addAction(Intent.ACTION_CONFIGURATION_CHANGED); + var intentFilter = new IntentFilter(Intent.ACTION_CONFIGURATION_CHANGED); - ActivityManagerService.registerReceiver("android", null, new IIntentReceiver.Stub() { - @Override - public void performReceive(Intent intent, int resultCode, String data, Bundle extras, boolean ordered, boolean sticky, int sendingUser) { - getExecutorService().submit(() -> dispatchConfigurationChanged(intent)); - if (!ordered) return; - try { - ActivityManagerService.finishReceiver(this, resultCode, data, extras, false, intent.getFlags()); - } catch (Throwable e) { - Log.e(TAG, "finish receiver", e); - } - } - }, intentFilter, null, 0, 0); - } catch (Throwable e) { - Log.e(TAG, "register configuration receiver", e); - } + registerReceiver(List.of(intentFilter), 0, this::dispatchConfigurationChanged); Log.d(TAG, "registered configuration receiver"); } private void registerSecretCodeReceiver() { - try { - IntentFilter intentFilter = new IntentFilter(); - intentFilter.addAction("android.provider.Telephony.SECRET_CODE"); - intentFilter.addDataAuthority("5776733", null); - intentFilter.addDataScheme("android_secret_code"); + IntentFilter intentFilter = new IntentFilter(); + intentFilter.addAction("android.provider.Telephony.SECRET_CODE"); + intentFilter.addAction("android.telephony.action.SECRET_CODE"); + intentFilter.addDataAuthority("5776733", null); + intentFilter.addDataScheme("android_secret_code"); - ActivityManagerService.registerReceiver("android", null, new IIntentReceiver.Stub() { - @Override - public void performReceive(Intent intent, int resultCode, String data, Bundle extras, boolean ordered, boolean sticky, int sendingUser) { - getExecutorService().submit(() -> dispatchSecretCodeReceive()); - if (!ordered) return; - try { - ActivityManagerService.finishReceiver(this, resultCode, data, extras, false, intent.getFlags()); - } catch (Throwable e) { - Log.e(TAG, "finish receiver", e); - } - } - }, intentFilter, null, 0, 0); - } catch (Throwable e) { - Log.e(TAG, "register secret code receiver", e); - } + registerReceiver(List.of(intentFilter), 0, this::dispatchSecretCodeReceive); Log.d(TAG, "registered secret code receiver"); } private void registerBootCompleteReceiver() { - try { - IntentFilter intentFilter = new IntentFilter(); - intentFilter.addAction(Intent.ACTION_LOCKED_BOOT_COMPLETED); + var intentFilter = new IntentFilter(Intent.ACTION_LOCKED_BOOT_COMPLETED); - ActivityManagerService.registerReceiver("android", null, new IIntentReceiver.Stub() { - @Override - public void performReceive(Intent intent, int resultCode, String data, Bundle extras, boolean ordered, boolean sticky, int sendingUser) { - getExecutorService().submit(() -> { - try { - var am = ActivityManagerService.getActivityManager(); - if (am != null) am.setActivityController(null, false); - } catch (Throwable e) { - Log.e(TAG, "setActivityController", e); - } - }); - if (!ordered) return; - try { - ActivityManagerService.finishReceiver(this, resultCode, data, extras, false, intent.getFlags()); - } catch (Throwable e) { - Log.e(TAG, "finish receiver", e); - } - } - }, intentFilter, null, 0, 0); - } catch (Throwable e) { - Log.e(TAG, "register boot receiver", e); - } + registerReceiver(List.of(intentFilter), 0, this::dispatchBootCompleted); Log.d(TAG, "registered boot receiver"); } private void registerUserChangeReceiver() { - try { - IntentFilter userFilter = new IntentFilter(); - userFilter.addAction(ACTION_USER_ADDED); - userFilter.addAction(ACTION_USER_REMOVED); + var userFilter = new IntentFilter(); + userFilter.addAction(ACTION_USER_ADDED); + userFilter.addAction(ACTION_USER_REMOVED); - var receiver = new IIntentReceiver.Stub() { - @Override - public void performReceive(Intent intent, int resultCode, String data, Bundle extras, boolean ordered, boolean sticky, int sendingUser) { - getExecutorService().submit(() -> dispatchUserChanged(intent)); - if (!ordered) return; - try { - ActivityManagerService.finishReceiver(this, resultCode, data, extras, false, intent.getFlags()); - } catch (Throwable e) { - Log.e(TAG, "finish receiver", e); - } - } - }; - - ActivityManagerService.registerReceiver("android", null, receiver, userFilter, null, -1, 0); - } catch (Throwable e) { - Log.e(TAG, "register user info change receiver", e); - } + registerReceiver(List.of(userFilter), -1, this::dispatchUserChanged); Log.d(TAG, "registered user info change receiver"); } + private void registerOpenManagerReceiver() { + var intentFilter = new IntentFilter(LSPNotificationManager.openManagerAction); + var moduleFilter = new IntentFilter(intentFilter); + moduleFilter.addDataScheme("module"); + + registerReceiver(List.of(intentFilter, moduleFilter), + "android.permission.BRICK", 0, this::dispatchOpenManager); + Log.d(TAG, "registered open manager receiver"); + } + @Override public void dispatchSystemServerContext(IBinder activityThread, IBinder activityToken, String api) { Log.d(TAG, "received system context"); ConfigManager.getInstance().setApi(api); ActivityManagerService.onSystemServerContext(IApplicationThread.Stub.asInterface(activityThread), activityToken); registerPackageReceiver(); - registerUnlockReceiver(); registerConfigurationReceiver(); registerSecretCodeReceiver(); registerBootCompleteReceiver(); registerUserChangeReceiver(); + registerOpenManagerReceiver(); } @Override diff --git a/daemon/src/main/java/org/lsposed/lspd/service/ServiceManager.java b/daemon/src/main/java/org/lsposed/lspd/service/ServiceManager.java index 0afac332..8d118172 100644 --- a/daemon/src/main/java/org/lsposed/lspd/service/ServiceManager.java +++ b/daemon/src/main/java/org/lsposed/lspd/service/ServiceManager.java @@ -54,7 +54,7 @@ public class ServiceManager { private static LogcatService logcatService = null; private static Dex2OatService dex2OatService = null; - private static final ExecutorService executorService = Executors.newCachedThreadPool(); + private static final ExecutorService executorService = Executors.newSingleThreadExecutor(); @RequiresApi(Build.VERSION_CODES.Q) public static Dex2OatService getDex2OatService() { diff --git a/daemon/src/main/res/drawable/ic_notification.xml b/daemon/src/main/res/drawable/ic_notification.xml new file mode 100644 index 00000000..5ef738b4 --- /dev/null +++ b/daemon/src/main/res/drawable/ic_notification.xml @@ -0,0 +1,28 @@ + + + + + diff --git a/daemon/src/main/res/values/strings.xml b/daemon/src/main/res/values/strings.xml index f624bf33..31d6dbdf 100644 --- a/daemon/src/main/res/values/strings.xml +++ b/daemon/src/main/res/values/strings.xml @@ -7,4 +7,8 @@ %s has been updated, please force stop and restart apps in its scope Xposed module updated, system reboot required %s has been updated, since the scope contains System Framework, required reboot to apply changes + Module update complete + LSPosed status + LSPosed loaded + Tap the notification to open manager diff --git a/hiddenapi/stubs/src/main/java/android/app/INotificationManager.java b/hiddenapi/stubs/src/main/java/android/app/INotificationManager.java index e65fc206..965e3f94 100644 --- a/hiddenapi/stubs/src/main/java/android/app/INotificationManager.java +++ b/hiddenapi/stubs/src/main/java/android/app/INotificationManager.java @@ -17,7 +17,12 @@ public interface INotificationManager extends IInterface { @RequiresApi(30) void cancelNotificationWithTag(String pkg, String opPkg, String tag, int id, int userId) throws RemoteException; - void createNotificationChannels(String pkg, ParceledListSlice channelsList) throws RemoteException; + void createNotificationChannelsForPackage(String pkg, int uid, ParceledListSlice channelsList) throws RemoteException; + + @RequiresApi(30) + NotificationChannel getNotificationChannelForPackage(String pkg, int uid, String channelId, String conversationId, boolean includeDeleted) throws RemoteException; + + NotificationChannel getNotificationChannelForPackage(String pkg, int uid, String channelId, boolean includeDeleted) throws RemoteException; abstract class Stub extends Binder implements INotificationManager { public static INotificationManager asInterface(IBinder obj) { diff --git a/services/manager-service/src/main/aidl/org/lsposed/lspd/ILSPManagerService.aidl b/services/manager-service/src/main/aidl/org/lsposed/lspd/ILSPManagerService.aidl index 2aee4460..282835c6 100644 --- a/services/manager-service/src/main/aidl/org/lsposed/lspd/ILSPManagerService.aidl +++ b/services/manager-service/src/main/aidl/org/lsposed/lspd/ILSPManagerService.aidl @@ -70,12 +70,6 @@ interface ILSPManagerService { void restartFor(in Intent intent) = 35; - void createShortcut() = 36; - - boolean isAddShortcut() = 37; - - void setAddShortcut(boolean enabled) = 38; - oneway void flashZip(String zipPath, in ParcelFileDescriptor outputStream) = 39; boolean performDexOptMode(String packageName) = 40; @@ -89,4 +83,10 @@ interface ILSPManagerService { int getDex2OatWrapperCompatibility() = 44; void clearApplicationProfileData(in String packageName) = 45; + + Intent getLaunchIntentForManager() = 46; + + boolean enableStatusNotification() = 47; + + void setEnableStatusNotification(boolean enable) = 48; }