diff --git a/README.md b/README.md index 572ff4a..ab7f01a 100644 --- a/README.md +++ b/README.md @@ -1,42 +1,46 @@ -# Hide App from Recent Task List +# Replace Cursor -Simple module to hide any app from recent task list. +Replace mouse cursor with a custom one. -Designed in pure Kotlin & Jetpack Compose & Material Design 3. Can be a template for any Xposed module with a application selection list. +自定义包括鼠标指针、触控点在内的各种图片资源。 -## How to use +Note: You can use Magisk + [RRO](https://source.android.com/docs/core/runtime/rros) for better experience. +See `magisk` folder for more information. +(be aware of SELinux context, btw.) -> Tested on: Android 10 (AOSP), Android 11 (MIUI 12), Android 13 (AOSP), Android 13 (MIUI 14), Android 14 (AOSP); may work on [10 <= Android <= 14](http://aospxref.com/android-10.0.0_r47/xref/frameworks/base/services/core/java/com/android/server/wm/RecentTasks.java#1272) +## How to use / 用法 + +> Tested on: None 1. Select `System framework` (package name may be `android` or `system` or empty, [see this](https://github.com/LSPosed/LSPosed/releases/tag/v1.9.1)) in module scope and activate the module 2. Force stop module -3. Select the apps you want to hide from recent app list in module settings (if package list not shown, you can manually import / export settings to edit config) -4. Reboot (you MUST reboot when you modify the list, or changes will not be applied until next reboot) -5. If you need multi-user support, install this module only in main user, and use [Shizuku](https://shizuku.rikka.app/download/) to get app info from other users. +3. Add resources to change. Please make sure that image sizes are bigger than hotspot (cursor left-top corner / touch point), otherwise nothing will show. +4. Reboot (you MUST reboot when you modify anything, or changes will not be applied until next reboot) +5. Reverse engineer `/system/framework/framework-res.apk` to find out the resource ID of the cursor you want to replace. -## Module Scope +For MiPad users, install [MaxMiPad](https://github.com/Xposed-Modules-Repo/com.yifeplayte.maxmipadinput/releases/latest) and enable `No Magic Pointer`. -- android - -## Project URL +## Common resources / 常用资源 -Home URL: +From MIUI 13, Android 12. -Xposed Modules Repo URL: +| Resource ID | Desciption | HotSpot | +|--------------------|----------------------------------------------------------------|----------| +| pointer_spot_touch | Touch point | (22, 22) | +| pointer_arrow | Mouse Pointer (Arrow) | (5, 5) | +| pointer_hand | Mouse Pointer (Hand, for example when hover on sth. clickable) | (9, 4) | -## Technical Details +Mouse-related resource-id may have a `_large` suffix, used when `Accessibility` -> `Large mouse pointer`(`大号鼠标指针`) is enabled. -UI: Material Design 3 + Jetpack Compose + Kotlin. - -Hook: Hook `com.android.server.wm.RecentTasks.isVisibleRecentTask(com.android.server.wm.Task)`, `(callMethod(param.args[0], "getBaseIntent") as Intent).component?.packageName` is package name. +## Module Scope -## HELP ME IT DOESNT WORK!!! +- android -Please open a issue [here](https://github.com/Young-Lord/hideRecent/issues). Provide your Android version, `/system/framework/framework.jar` and all `/system/framework/framework{a number here}.jar` if exist. +## Project URL -I am not intended to support Android < 10, but anyone is free to [send a PR](https://github.com/Young-Lord/hideRecent/pulls) for Android < 10 support. +Home URL: -PR for refactoring is also appreciated. +Xposed Modules Repo URL: ## License @@ -44,20 +48,5 @@ Apache-2.0 License or MIT License are all OK. ## Thanks - - - (Apache-2.0 license) - -~~Original code from: ~~ refactored. - -[rootAVD](https://gitlab.com/newbit/rootAVD) - -## Why? - -出于隐私或便捷原因,有些时候我们总是想隐藏一些应用。 - -CrDroid 内置了这个功能,这是好的,然而并不是所有人都在用 CrDroid。 - -而且,国内的 ROM 的“最近任务列表”里划掉一个卡片,就等于杀死这个应用,这太蠢了!你也不想你的 Clash For Android 编辑完配置就挂了吧? - -Thanox 等一些应用也有这个功能,但只为了这个功能氪金并装一个闭源应用,怎么看都很怪。于是我买了 Thanox 订阅,然后写完这个模块后又卖了。 +- (doesn't work for me) +- (per-app configuration) diff --git a/app/build.gradle b/app/build.gradle index 56c7012..16744b6 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -4,11 +4,11 @@ plugins { } android { - namespace 'moe.lyniko.hiderecent' + namespace 'moe.lyniko.replacecursor' compileSdk 34 defaultConfig { - applicationId "moe.lyniko.hiderecent" + applicationId "moe.lyniko.replacecursor" minSdk 29 targetSdk 34 versionCode 202 @@ -51,17 +51,14 @@ android { dependencies { // module stuff compileOnly 'de.robv.android.xposed:api:82' - implementation 'org.lsposed.hiddenapibypass:hiddenapibypass:4.3' - def shizuku_version = '13.1.5' - implementation "dev.rikka.shizuku:api:$shizuku_version" - implementation "dev.rikka.shizuku:provider:$shizuku_version" + implementation 'com.google.code.gson:gson:2.8.9' implementation 'me.zhanghai.compose.preference:library:1.0.0' // ui stuff implementation "com.google.accompanist:accompanist-drawablepainter:0.28.0" implementation "androidx.navigation:navigation-compose:2.7.6" implementation 'androidx.core:core-ktx:1.12.0' - implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.6.2' + implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.7.0' implementation 'androidx.activity:activity-compose:1.8.2' implementation platform('androidx.compose:compose-bom:2022.10.00') implementation 'androidx.compose.ui:ui' diff --git a/app/magisk/META-INF/com/google/android/update-binary b/app/magisk/META-INF/com/google/android/update-binary new file mode 100644 index 0000000..28b48e5 --- /dev/null +++ b/app/magisk/META-INF/com/google/android/update-binary @@ -0,0 +1,33 @@ +#!/sbin/sh + +################# +# Initialization +################# + +umask 022 + +# echo before loading util_functions +ui_print() { echo "$1"; } + +require_new_magisk() { + ui_print "*******************************" + ui_print " Please install Magisk v20.4+! " + ui_print "*******************************" + exit 1 +} + +######################### +# Load util_functions.sh +######################### + +OUTFD=$2 +ZIPFILE=$3 + +mount /data 2>/dev/null + +[ -f /data/adb/magisk/util_functions.sh ] || require_new_magisk +. /data/adb/magisk/util_functions.sh +[ $MAGISK_VER_CODE -lt 20400 ] && require_new_magisk + +install_module +exit 0 diff --git a/app/magisk/META-INF/com/google/android/updater-script b/app/magisk/META-INF/com/google/android/updater-script new file mode 100644 index 0000000..11d5c96 --- /dev/null +++ b/app/magisk/META-INF/com/google/android/updater-script @@ -0,0 +1 @@ +#MAGISK diff --git a/app/magisk/README.md b/app/magisk/README.md new file mode 100644 index 0000000..281147b --- /dev/null +++ b/app/magisk/README.md @@ -0,0 +1,5 @@ +Example Magisk module for replacing cursor. + +Modified by LY at + +Change SELinux context: `su -c chcon u:object_r:vendor_overlay_file:s0 /data/adb/modules/pointer_replacer_rro/system/vendor/overlay/allusive_rro_sign.apk` diff --git a/app/magisk/module.prop b/app/magisk/module.prop new file mode 100644 index 0000000..968f073 --- /dev/null +++ b/app/magisk/module.prop @@ -0,0 +1,6 @@ +id=pointer_replacer_rro +name=Pointer Replacer RRO +version=v2.0 +versionCode=2 +author=thesandipv +description=Magisk Implementation of Pointer Replacer RRO, from https://github.com/sandipv22/pointer_replacer/ diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index 20ef656..deb55e5 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -19,6 +19,6 @@ # If you keep the line number information, uncomment this to # hide the original source file name. #-renamesourcefileattribute SourceFile --keep class moe.lyniko.hiderecent.MainHook +-keep class moe.lyniko.replacecursor.MainHook # -keep class rikka.shizuku.SystemServiceHelper # -keep class android.** { *; } \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 662c570..a199160 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -14,7 +14,7 @@ android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:theme="@style/Theme.HideRecentTask" - android:name="moe.lyniko.hiderecent.MyApplication" + android:name="moe.lyniko.replacecursor.MyApplication" tools:targetApi="31"> + android:enabled="true" + android:directBootAware="true" /> \ No newline at end of file diff --git a/app/src/main/assets/xposed_init b/app/src/main/assets/xposed_init index 6254130..672e325 100644 --- a/app/src/main/assets/xposed_init +++ b/app/src/main/assets/xposed_init @@ -1 +1 @@ -moe.lyniko.hiderecent.MainHook \ No newline at end of file +moe.lyniko.replacecursor.MainHook \ No newline at end of file diff --git a/app/src/main/java/moe/lyniko/hiderecent/MainActivity.kt b/app/src/main/java/moe/lyniko/hiderecent/MainActivity.kt deleted file mode 100644 index c23e24c..0000000 --- a/app/src/main/java/moe/lyniko/hiderecent/MainActivity.kt +++ /dev/null @@ -1,108 +0,0 @@ -package moe.lyniko.hiderecent - -// https://stackoverflow.com/a/63877349 -// https://stackoverflow.com/a/1109108 -import android.os.Bundle -import android.os.Process.myUserHandle -import android.util.Log -import android.widget.Toast -import androidx.activity.ComponentActivity -import androidx.activity.compose.setContent -import androidx.compose.foundation.layout.padding -import androidx.compose.material3.Scaffold -import androidx.compose.material3.SnackbarDuration -import androidx.compose.material3.SnackbarHost -import androidx.compose.material3.SnackbarHostState -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.ui.Modifier -import androidx.navigation.compose.rememberNavController -import kotlinx.coroutines.launch -import moe.lyniko.hiderecent.ui.AppNavHost -import moe.lyniko.hiderecent.ui.BottomNavigation -import moe.lyniko.hiderecent.ui.theme.MyApplicationTheme -import moe.lyniko.hiderecent.utils.PreferenceUtils -import moe.lyniko.hiderecent.utils.getIdByUserHandle -import moe.lyniko.hiderecent.utils.isShizukuAvailable -import moe.lyniko.hiderecent.utils.isShizukuNeeded -import kotlin.system.exitProcess - - -class MainActivity : ComponentActivity() { - private var snackbarHostState = SnackbarHostState() - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - try { - PreferenceUtils.getInstance(this) - } catch (e: SecurityException) { - Toast.makeText(this, getString(R.string.not_activated), Toast.LENGTH_LONG).show() - finish() - return - } - setContent { - MyApplicationTheme { - - val scope = rememberCoroutineScope() - val snackbarHostStateRemember = remember { snackbarHostState } - val navController = rememberNavController() - - Scaffold( - snackbarHost = { - SnackbarHost(hostState = snackbarHostStateRemember) - }, - bottomBar = { - BottomNavigation(navController = navController) - } - ) { innerPadding -> - AppNavHost( - navController = navController, - modifier = Modifier.padding(innerPadding) - ) - } - - // check main user - val userId = getUserId() - if (userId != 0) { - LaunchedEffect(snackbarHostState) { - scope.launch { - snackbarHostState.showSnackbar( - getString(R.string.main_user_only, userId), - actionLabel = getString(R.string.dismiss_notification), - duration = SnackbarDuration.Indefinite - ) - } - } - Log.w(BuildConfig.APPLICATION_ID, "Wrong user id: $userId") - } - - // check shizuku - if (isShizukuNeeded(this) && !isShizukuAvailable()) { - LaunchedEffect(snackbarHostState) { - scope.launch { - snackbarHostState.showSnackbar( - getString(R.string.shizuku_not_available_toast), - actionLabel = getString(R.string.dismiss_notification), - duration = SnackbarDuration.Indefinite - ) - } - } - Log.w(BuildConfig.APPLICATION_ID, "Shizuku not running") - } - } - } - } - -} - -fun getUserId(): Int { - val userHandle = myUserHandle() - var userId = 0 - try { - userId = getIdByUserHandle(userHandle) - } catch (e: Exception) { - Log.e(BuildConfig.APPLICATION_ID, "Error when getting user id: ${e.message}") - } - return userId -} \ No newline at end of file diff --git a/app/src/main/java/moe/lyniko/hiderecent/MainHook.kt b/app/src/main/java/moe/lyniko/hiderecent/MainHook.kt deleted file mode 100644 index 52b7c05..0000000 --- a/app/src/main/java/moe/lyniko/hiderecent/MainHook.kt +++ /dev/null @@ -1,54 +0,0 @@ -package moe.lyniko.hiderecent - -import android.content.Intent -import de.robv.android.xposed.IXposedHookLoadPackage -import de.robv.android.xposed.XC_MethodHook -import de.robv.android.xposed.XSharedPreferences -import de.robv.android.xposed.XposedHelpers.callMethod -import de.robv.android.xposed.XposedHelpers.findAndHookMethod -import de.robv.android.xposed.callbacks.XC_LoadPackage.LoadPackageParam -import de.robv.android.xposed.XposedBridge -import moe.lyniko.hiderecent.utils.PreferenceUtils - -class MainHook : IXposedHookLoadPackage { - - override fun handleLoadPackage(lpparam: LoadPackageParam) { - if (lpparam.packageName == "android") onAppHooked(lpparam) - } - - private fun onAppHooked(lpparam: LoadPackageParam) { - val visibleFilterHook: XC_MethodHook = object : XC_MethodHook() { - override fun beforeHookedMethod(param: MethodHookParam) { - val intent = callMethod(param.args[0], "getBaseIntent") as Intent - if (BuildConfig.DEBUG) { - XposedBridge.log("Hide - Current Intent: $intent") - XposedBridge.log("Hide - Current component: ${intent.component}") - XposedBridge.log("Hide - Current package: ${intent.component?.packageName}") - } - val packageName = intent.component?.packageName ?: return - if (packages.contains(packageName)) { - param.result = false - } - } - } - try { - findAndHookMethod( - "com.android.server.wm.RecentTasks", - lpparam.classLoader, - "isVisibleRecentTask", - "com.android.server.wm.Task", - visibleFilterHook - ) - } catch (ignored: Throwable) { - } - } - - private val packages: MutableSet - - init { - val xsp = - XSharedPreferences(BuildConfig.APPLICATION_ID, PreferenceUtils.functionalConfigName) - xsp.makeWorldReadable() - packages = PreferenceUtils.getPackageListFromPref(xsp) - } -} diff --git a/app/src/main/java/moe/lyniko/hiderecent/ui/HomeView.kt b/app/src/main/java/moe/lyniko/hiderecent/ui/HomeView.kt deleted file mode 100644 index 3b8aa2a..0000000 --- a/app/src/main/java/moe/lyniko/hiderecent/ui/HomeView.kt +++ /dev/null @@ -1,271 +0,0 @@ -package moe.lyniko.hiderecent.ui - -import moe.lyniko.hiderecent.BuildConfig -import moe.lyniko.hiderecent.R -import android.content.Context -import android.content.ContextWrapper -import android.os.Process.myUserHandle -import android.util.Log -import android.view.inputmethod.InputMethodManager -import androidx.activity.ComponentActivity -import androidx.compose.foundation.Image -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.text.KeyboardActions -import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Clear -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Scaffold -import androidx.compose.material3.SnackbarDuration -import androidx.compose.material3.SnackbarHost -import androidx.compose.material3.SnackbarHostState -import androidx.compose.material3.Surface -import androidx.compose.material3.Switch -import androidx.compose.material3.Text -import androidx.compose.material3.TextField -import androidx.compose.material3.TextFieldDefaults -import androidx.compose.material3.TopAppBar -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.MutableState -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.runtime.setValue -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.vector.ImageVector -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.vectorResource -import androidx.compose.ui.text.input.ImeAction -import androidx.compose.ui.unit.dp -import com.google.accompanist.drawablepainter.rememberDrawablePainter -import kotlinx.coroutines.launch -import moe.lyniko.hiderecent.ui.theme.MyApplicationTheme -import moe.lyniko.hiderecent.utils.AppUtils -import moe.lyniko.hiderecent.utils.ParsedPackage -import moe.lyniko.hiderecent.utils.PreferenceUtils -import moe.lyniko.hiderecent.utils.PreferenceUtils.Companion.ConfigKeys -import moe.lyniko.hiderecent.utils.getIdByUserHandle - -fun Context.getActivity(): ComponentActivity? = when (this) { - is ComponentActivity -> this - is ContextWrapper -> baseContext.getActivity() - else -> null -} - -private lateinit var appUtils: AppUtils -private lateinit var preferenceUtils: PreferenceUtils -private var searchContent: MutableState = mutableStateOf("") -private var showUserAppInsteadOfSystem: MutableState = mutableStateOf(true) -private var snackbarHostState = SnackbarHostState() - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -fun HomeView() { - val context = LocalContext.current - appUtils = AppUtils.getInstance(context) - preferenceUtils = PreferenceUtils.getInstance(context) - MyApplicationTheme { - var searchContentRemember by remember { searchContent } - var showUserAppInsteadOfSystemRemember by remember { showUserAppInsteadOfSystem } - val scope = rememberCoroutineScope() - val snackbarHostStateRemember = remember { snackbarHostState } - - Scaffold( - snackbarHost = { - SnackbarHost(hostState = snackbarHostStateRemember) - }, - topBar = { - TopAppBar( - title = { - TextField( - value = searchContentRemember, - onValueChange = { searchContentRemember = it }, - placeholder = { Text(context.getString(R.string.search_text)) }, - singleLine = true, - // action done - keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done), - keyboardActions = KeyboardActions( - onDone = { - context.getActivity()?.currentFocus?.let { view -> - val imm = - context.getSystemService(Context.INPUT_METHOD_SERVICE) as? InputMethodManager - imm?.hideSoftInputFromWindow(view.windowToken, 0) - } - } - ), - // disable background - colors = TextFieldDefaults.colors( - unfocusedContainerColor = Color.Transparent, - focusedContainerColor = Color.Transparent, - unfocusedIndicatorColor = Color.Transparent, - focusedIndicatorColor = Color.Transparent - ), - modifier = Modifier.fillMaxWidth() - ) - }, - actions = { - if (searchContentRemember.isNotEmpty()) { - IconButton(onClick = { - // clear search - searchContentRemember = "" - }) { - Icon( - imageVector = Icons.Filled.Clear, - contentDescription = context.getString(R.string.clear_search) - ) - } - } - IconButton(onClick = { - showUserAppInsteadOfSystemRemember = - !showUserAppInsteadOfSystem.value - }) { - Icon( - imageVector = ImageVector.vectorResource(R.drawable.baseline_swap_horiz_24), - contentDescription = context.getString(R.string.switch_app_type) - ) - } - }, - ) - }, - ) { innerPadding -> - AppListForPackages(getDisplayApps(), modifier = Modifier.padding(innerPadding)) - } - - // check main user - val userHandle = myUserHandle() - var userId = 0 - try { - userId = getIdByUserHandle(userHandle) - } catch (e: Exception) { - Log.e(BuildConfig.APPLICATION_ID, "Error when getting user id: ${e.message}") - } - if (userId != 0) { - LaunchedEffect(snackbarHostState) { - scope.launch { - snackbarHostState.showSnackbar( - context.getString(R.string.main_user_only, userId), - actionLabel = context.getString(R.string.dismiss_notification), - duration = SnackbarDuration.Indefinite - ) - } - } - Log.w( - BuildConfig.APPLICATION_ID, - "Wrong user id: ${getIdByUserHandle(userHandle)}" - ) - } - if (appUtils.parsedApps.isEmpty()) { - LaunchedEffect(snackbarHostState) { - scope.launch { - snackbarHostState.showSnackbar( - context.getString(R.string.apps_not_fetched), - actionLabel = context.getString(R.string.dismiss_notification), - duration = SnackbarDuration.Indefinite - ) - } - } - } else if (!appUtils.isActivitiesFetched() && preferenceUtils.managerPref.getBoolean( - ConfigKeys.HideNoActivityPackages.key, ConfigKeys.HideNoActivityPackages.default - ) - ) { - LaunchedEffect(snackbarHostState) { - scope.launch { - snackbarHostState.showSnackbar( - context.getString(R.string.activity_not_fetched), - actionLabel = context.getString(R.string.dismiss_notification), - duration = SnackbarDuration.Indefinite - ) - } - } - } - } -} - -private fun getDisplayApps(): List { - var appsFiltered = - appUtils.parsedApps.filter { showUserAppInsteadOfSystem.value xor it.isSystemApp } - val trimmedLowerCasedSearch = searchContent.value.trim().lowercase() - appsFiltered = if (trimmedLowerCasedSearch.length <= 1) { - appsFiltered - } else appsFiltered.filter { it.isLowerCasedSearchMatch(trimmedLowerCasedSearch) } - // sort by app name and package name - appsFiltered = appsFiltered.sortedWith(compareByDescending { - preferenceUtils.isPackageInList(it.packageName) - }.thenBy { it.appName }.thenBy { it.packageName }) - // appsFiltered = appsFiltered.sortedBy { it.packageName }.sortedBy { it.appName }.sortedBy{ !preferenceUtils.isPackageInList(it.packageName) } - return appsFiltered -} - -// https://jetpackcompose.cn/docs/tutorial/ -@Composable -private fun SingleAppCardForPackage(app: ParsedPackage) { - Row( - modifier = Modifier.padding(all = 8.dp) // 在我们的 Card 周围添加 padding - ) { - Image( - //image is app icon - painter = rememberDrawablePainter(app.appIcon), - contentDescription = null, - modifier = Modifier - .size(50.dp) // 改变 Image 元素的大小 - ) - Spacer(Modifier.padding(horizontal = 8.dp)) // 添加一个空的控件用来填充水平间距,设置 padding 为 8.dp - Column( - modifier = Modifier - .weight(1f) // 设置 Column 的 weight 为 1,使其占据剩余空间 - ) { - Text( - text = app.appName, - color = MaterialTheme.colorScheme.secondary, - style = MaterialTheme.typography.titleMedium - ) - Spacer(Modifier.padding(vertical = 4.dp)) - Text( - text = app.packageName, - style = MaterialTheme.typography.bodyMedium - ) - } - Column { - var checked by remember { mutableStateOf(preferenceUtils.isPackageInList(app.packageName)) } - Switch(checked = checked, onCheckedChange = { - if (it) { - preferenceUtils.addPackage(app.packageName) - } else { - preferenceUtils.removePackage(app.packageName) - } - // update checked state - checked = it - }) - } - } -} - -@Composable -private fun AppListForPackages(apps: List, modifier: Modifier = Modifier) { - Surface( - shape = MaterialTheme.shapes.medium, // 使用 MaterialTheme 自带的形状 - modifier = modifier - .fillMaxWidth(), - ) { - LazyColumn { - items( - count = apps.size, - key = { app_index -> apps[app_index].packageName } - ) { app_index -> - SingleAppCardForPackage(apps[app_index]) - } - } - } -} \ No newline at end of file diff --git a/app/src/main/java/moe/lyniko/hiderecent/ui/SettingsView.kt b/app/src/main/java/moe/lyniko/hiderecent/ui/SettingsView.kt deleted file mode 100644 index 2188ec1..0000000 --- a/app/src/main/java/moe/lyniko/hiderecent/ui/SettingsView.kt +++ /dev/null @@ -1,117 +0,0 @@ -package moe.lyniko.hiderecent.ui - -import android.content.ClipData -import android.content.ClipboardManager -import android.content.Context -import android.os.Build -import android.widget.Toast -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext -import me.zhanghai.compose.preference.ProvidePreferenceLocals -import me.zhanghai.compose.preference.getPreferenceFlow -import me.zhanghai.compose.preference.SwitchPreference -import moe.lyniko.hiderecent.R -import moe.lyniko.hiderecent.utils.PreferenceUtils -import moe.lyniko.hiderecent.utils.PreferenceUtils.Companion.ConfigKeys -import moe.lyniko.hiderecent.utils.isShizukuAvailable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.setValue -import me.zhanghai.compose.preference.Preference -import me.zhanghai.compose.preference.switchPreference - - -@Composable -fun SettingsView() { - val context = LocalContext.current - val managerPref = PreferenceUtils.getInstance(context).managerPref - ProvidePreferenceLocals( - flow = managerPref.getPreferenceFlow() - ) { - LazyColumn(modifier = Modifier.fillMaxSize()) { - item { - var switchMutableState by remember { - mutableStateOf( - managerPref.getBoolean( - ConfigKeys.ShowPackageForAllUser.key, - ConfigKeys.ShowPackageForAllUser.default - ) - ) - } - SwitchPreference( - value = switchMutableState, - onValueChange = { - switchMutableState = it - managerPref.edit() - .putBoolean(ConfigKeys.ShowPackageForAllUser.key, it) - .apply() - if(it) isShizukuAvailable() - }, - title = { Text(context.getString(R.string.show_package_for_all_user)) }, - summary = { Text(context.getString(R.string.show_package_for_all_user_summary)) }, - ) - } - switchPreference( - key=ConfigKeys.HideNoActivityPackages.key, - defaultValue = ConfigKeys.HideNoActivityPackages.default, - title = { Text(context.getString(R.string.hide_no_activity_packages)) }, - summary = { Text(context.getString(R.string.hide_no_activity_packages_summary)) }, - ) - item { - Preference( - title = { Text(context.getString(R.string.export_config)) }, - onClick = { - // clipboard - val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager - val clip = ClipData.newPlainText("config", PreferenceUtils.getInstance(context).packagesToString()) - try { - clipboard.setPrimaryClip(clip) - if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.S_V2) - Toast.makeText(context, context.getString(R.string.export_config_to_clipboard_success), Toast.LENGTH_SHORT).show() - } - catch (e: SecurityException) { - Toast.makeText(context, context.getString(R.string.export_config_to_clipboard_failed), Toast.LENGTH_SHORT).show() - e.printStackTrace() - } - } - ) - } - item { - Preference( - title = { Text(context.getString(R.string.import_config)) }, - onClick = { - // clipboard - val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager - - val clipboardData: CharSequence? - try { - clipboardData = - clipboard.primaryClip?.getItemAt(0)?.text - } - catch(e: SecurityException){ - Toast.makeText(context, context.getString(R.string.import_config_failed_perm), Toast.LENGTH_SHORT).show() - return@Preference - } - if(clipboardData.isNullOrEmpty()){ - Toast.makeText(context, context.getString(R.string.import_config_failed_empty), Toast.LENGTH_SHORT).show() - return@Preference - } - try{ - val changed = PreferenceUtils.getInstance(context).packagesFromString(clipboardData.toString()) - Toast.makeText(context, context.resources.getQuantityString(R.plurals.import_config_success_count, changed, changed), Toast.LENGTH_SHORT).show() - } - catch (e: NotImplementedError){ - Toast.makeText(context, context.getString(R.string.import_config_failed_wrong), Toast.LENGTH_SHORT).show() - e.printStackTrace() - } - } - ) - } - } - } -} \ No newline at end of file diff --git a/app/src/main/java/moe/lyniko/hiderecent/utils/AppUtils.kt b/app/src/main/java/moe/lyniko/hiderecent/utils/AppUtils.kt deleted file mode 100644 index 946c073..0000000 --- a/app/src/main/java/moe/lyniko/hiderecent/utils/AppUtils.kt +++ /dev/null @@ -1,201 +0,0 @@ -package moe.lyniko.hiderecent.utils - -import android.annotation.SuppressLint -import android.content.Context -import android.content.pm.PackageInfo -import android.content.pm.PackageManager -import android.graphics.drawable.Drawable -import android.os.Build -import android.os.IBinder -import org.lsposed.hiddenapibypass.HiddenApiBypass -import rikka.shizuku.ShizukuBinderWrapper -import rikka.shizuku.SystemServiceHelper -import moe.lyniko.hiderecent.utils.PreferenceUtils.Companion.ConfigKeys - -class AppUtils( - context: Context -) { - companion object { - - @Volatile - private var instance: AppUtils? = null - - fun getInstance(context: Context) = - instance ?: synchronized(this) { - instance ?: AppUtils(context).also { instance = it } - } - } - - // the package manager - private val packageManager: PackageManager = context.packageManager - private val baseGetInstalledPackagesFlags: Int = - PackageManager.GET_ACTIVITIES or PackageManager.GET_META_DATA - private var getInstalledPackagesFlags: Int = baseGetInstalledPackagesFlags - private val preferenceUtils = PreferenceUtils.getInstance(context) - - private fun removeActivitiesFetch() { - getInstalledPackagesFlags = - baseGetInstalledPackagesFlags and (baseGetInstalledPackagesFlags xor PackageManager.GET_ACTIVITIES) - } - private val allApps: List by lazy { - // catch random error caused by MIUI or something else - if(!preferenceUtils.managerPref.getBoolean(ConfigKeys.HideNoActivityPackages.key, ConfigKeys.HideNoActivityPackages.default)){ - removeActivitiesFetch() - } - try { - allAppsNoCatch - } catch (e: Exception) { - e.printStackTrace() - if(!isActivitiesFetched()){ - // skip another try if already failed - return@lazy listOf() - } - - // remove GET_ACTIVITIES flag - removeActivitiesFetch() - try{ - allAppsNoCatch - } - catch(e: Exception) { - e.printStackTrace() - listOf() - } - } - } - - fun isActivitiesFetched(): Boolean { - return (PackageManager.GET_ACTIVITIES and getInstalledPackagesFlags) != 0 - } - - // a list for all the apps, lazy init - private val allAppsNoCatch: List by lazy { - // get all the apps - if ( - preferenceUtils.managerPref.getBoolean( - ConfigKeys.ShowPackageForAllUser.key, - ConfigKeys.ShowPackageForAllUser.default - ) && isShizukuAvailable() - ) appForAllUser else appForSingleUser - } - - private val appForSingleUser: List by lazy { - packageManager.getInstalledPackages(getInstalledPackagesFlags) - } - - private val appForAllUser: List by lazy { - val users = getUserProfiles(context) - val apps = ArrayList() - for (user in users) { - apps.addAll( - getInstalledPackagesAsUser( - getInstalledPackagesFlags, - getIdByUserHandle(user) - ) - ) - } - apps - } - - private fun atLeastT(): Boolean { - return Build.VERSION.SDK_INT >= 33 - } - - @SuppressLint("PrivateApi") - private fun getInstalledPackagesAsUser( - @Suppress("SameParameterValue") flags: Int, - userId: Int - ): List { - // fuck android. - // https://www.xda-developers.com/implementing-shizuku/ - // Previous version: https://github.com/Young-Lord/hideRecent/commit/8f956002e1edbb95e2e3e945c28ec1a716596347 - // val iPmClass = Class.forName("android.content.pm.IPackageManager") - val iPmStub = Class.forName("android.content.pm.IPackageManager\$Stub") - val asInterfaceMethod = iPmStub.getMethod("asInterface", IBinder::class.java) - val iPmInstance = asInterfaceMethod.invoke( - null, - ShizukuBinderWrapper(SystemServiceHelper.getSystemService("package")) - ) - val iParceledListSliceClass = Class.forName("android.content.pm.ParceledListSlice") - val retAsInner: Any - if (atLeastT()) { - retAsInner = HiddenApiBypass.invoke( - iPmInstance::class.java, - iPmInstance, - "getInstalledPackages", - flags.toLong(), - userId - ) - } else { - retAsInner = HiddenApiBypass.invoke( - iPmInstance::class.java, - iPmInstance, - "getInstalledPackages", - flags, - userId - ) - } - @Suppress("UNCHECKED_CAST") - return HiddenApiBypass.invoke( - iParceledListSliceClass, - retAsInner, - "getList" - ) as List - } - - private val appsFiltered: List by lazy { - // get all the apps - val result = ArrayList() - allApps.forEach { - if (result.find { pkg -> pkg.packageName == it.packageName } == null && (!isActivitiesFetched() || !it.activities.isNullOrEmpty())) { - // filter for multi-user and filter those without activities - result.add(it) - } - } - result - } - - val parsedApps: List by lazy { - appsFiltered.map { ParsedPackage(it, packageManager) } - } -} - -class ParsedPackage( - private val pkg: PackageInfo, - private val packageManager: PackageManager -) { - // lazy init - val appName: String by lazy { - packageManager.getApplicationLabel(pkg.applicationInfo).toString() - } - val appIcon: Drawable by lazy { - pkg.applicationInfo.loadIcon(packageManager) - } - val packageName: String by lazy { - pkg.packageName - } - - @Suppress("unused") - val versionName: String by lazy { - pkg.versionName - } - - @Suppress("unused") - val versionCode: Long by lazy { - pkg.longVersionCode - } - val isSystemApp: Boolean by lazy { - (pkg.applicationInfo.flags and android.content.pm.ApplicationInfo.FLAG_SYSTEM) != 0 - } - private val packageNameLowerCase: String by lazy { - packageName.lowercase() - } - private val appNameLowerCase: String by lazy { - appName.lowercase() - } - - fun isLowerCasedSearchMatch(searchContent: String): Boolean { - return packageNameLowerCase.contains(searchContent) || appNameLowerCase.contains( - searchContent - ) - } -} \ No newline at end of file diff --git a/app/src/main/java/moe/lyniko/hiderecent/utils/MultiUserUtils.kt b/app/src/main/java/moe/lyniko/hiderecent/utils/MultiUserUtils.kt deleted file mode 100644 index efc3bc6..0000000 --- a/app/src/main/java/moe/lyniko/hiderecent/utils/MultiUserUtils.kt +++ /dev/null @@ -1,16 +0,0 @@ -package moe.lyniko.hiderecent.utils - -import android.content.Context -import android.os.UserHandle -import android.os.UserManager -import org.lsposed.hiddenapibypass.HiddenApiBypass - -fun getIdByUserHandle(userHandle: UserHandle): Int { - return HiddenApiBypass.invoke(UserHandle::class.java, userHandle, "getIdentifier"/*, args*/) as Int -} - -fun getUserProfiles(context: Context): List { - // https://stackoverflow.com/questions/14749504/android-usermanager-check-if-user-is-owner-admin - val userManager = context.getSystemService(Context.USER_SERVICE) as UserManager - return userManager.userProfiles -} diff --git a/app/src/main/java/moe/lyniko/hiderecent/utils/PreferenceUtils.kt b/app/src/main/java/moe/lyniko/hiderecent/utils/PreferenceUtils.kt deleted file mode 100644 index 7d3a87a..0000000 --- a/app/src/main/java/moe/lyniko/hiderecent/utils/PreferenceUtils.kt +++ /dev/null @@ -1,174 +0,0 @@ -package moe.lyniko.hiderecent.utils - -import android.annotation.SuppressLint -import android.content.Context -import android.content.SharedPreferences -import moe.lyniko.hiderecent.MyApplication -import moe.lyniko.hiderecent.R - -@SuppressLint("WorldReadableFiles") -class PreferenceUtils( // init context on constructor - context: Context -) { - // ------ 1. get several SharedPreferences (funcPref is the only accessible during Xposed inject) ------ - private var funcPref: SharedPreferences = try { - @Suppress("DEPRECATION") - context.getSharedPreferences(functionalConfigName, Context.MODE_WORLD_READABLE) - } catch (e: SecurityException) { - throw e - // Log.w("PreferenceUtil", "Fallback to Private SharedPref for error!!!: ${e.message}") - // context.getSharedPreferences(functionalConfigName, Context.MODE_PRIVATE) - } - - @Suppress("DEPRECATION") - var managerPref: SharedPreferences = - context.getSharedPreferences(managerConfigName, Context.MODE_WORLD_READABLE) - - @Suppress("DEPRECATION") - private val legacyFuncPref = context.getSharedPreferences(legacyConfigName, Context.MODE_WORLD_READABLE) - - // ------ 2. init packages ------ - private fun initPackageFromLegacyAndNew(funcPref: SharedPreferences, legacyPref: SharedPreferences) { - val legacyPackages = legacyPref.getString(legacyModeStringMode, "")?.removeSurrounding("#")?.split("##")?.toMutableSet() - val newPackages = getPackageListFromPref(funcPref) - if(newPackages.isEmpty() && !legacyPackages.isNullOrEmpty()) { - // remove legacy data only if only legacy one has data. - // Log.d("PreferenceUtil", "initPackageFromLegacyAndNew: $legacyPackages") - legacyPref.edit().remove(legacyModeStringMode).apply() - packages = legacyPackages - commitPackageList() - } else { - packages = newPackages - } - packages.remove("") // have no idea why this occurs. - } - - private lateinit var packages: MutableSet - - init { - initPackageFromLegacyAndNew(funcPref, legacyFuncPref) - } - companion object { - - @Volatile - private var instance: PreferenceUtils? = null - - fun getInstance(context: Context) = - instance ?: synchronized(this) { - instance ?: PreferenceUtils(context).also { instance = it } - } - - private const val packagesTag = "packages" - const val functionalConfigName = "functional_config" - const val managerConfigName = "manager_config" - private const val legacyConfigName = "config" - private const val legacyModeStringMode = "Mode" - - enum class ConfigKeys(val key: String, val default: Boolean) { - ShowPackageForAllUser("show_package_for_all_user", false), - HideNoActivityPackages("hide_no_activity_packages", true) - } - - fun getPackageListFromPref(pref: SharedPreferences): MutableSet { - val currentPackageSet = pref.getStringSet(packagesTag, HashSet()) - return currentPackageSet!!.toMutableSet() - } - } - - - private fun commitPackageList() { - funcPref.edit().putStringSet(packagesTag, packages).apply() - } - - fun addPackage(pkg: String): Int { - if(pkg.isEmpty() || pkg == "*") return 0 - val ret = if (packages.add(pkg)) 1 else 0 - // Log.w("PreferenceUtil", "addPackage: $pkg -> $ret") - commitPackageList() - return ret - } - - fun removePackage(pkg: String): Int { - val ret: Int - if (pkg == "*") { - ret = packages.size - packages.clear() - } else { - ret = if (packages.remove(pkg)) 1 else 0 - } - // Log.w("PreferenceUtil", "removePackage: $pkg -> $ret") - commitPackageList() - return ret - } - - fun isPackageInList(pkg: String): Boolean { - // Log.d("PreferenceUtil", "isPackageInList: $pkg -> ${packages.contains(pkg)}") - return packages.contains(pkg) - } - - fun packagesToString(version: Int = 1): String { - when (version) { - 1 -> { - var result = - "# version=$version\n# -* # ${MyApplication.resourcesPublic.getString(R.string.export_uncomment_hint)}\n" - packages.forEach { pkg -> - result += "+$pkg\n" - } - if (packages.isEmpty()){ - result += "# +com.example.package # ${MyApplication.resourcesPublic.getString(R.string.export_demo_hint)}\n" - } - return result - } - else -> throw NotImplementedError("Version $version is not implemented") - } - } - private fun validatePackageNameOrAsterisk(pkg: String): Boolean { - if (pkg == "*") return true - // https://stackoverflow.com/a/40772073 - @Suppress("RegExpSimplifiable") - return pkg.matches(Regex("^([A-Za-z]{1}[A-Za-z\\d_]*\\.)+[A-Za-z][A-Za-z\\d_]*\$")) - } - - fun packagesFromString(str: String): Int { - val lines = str.split("\n") - // read first line for version - val version: Int - var changed = 0 - try { - version = lines[0].split("=")[1].toInt() - } catch (e: Exception) { - e.printStackTrace() - throw NotImplementedError("Version is not specified") - } - when (version) { - 1 -> { - lines.forEach { line -> - // remove comments start with # - val lineWithoutComment = line.split("#")[0].trim() - // skip if empty - if (lineWithoutComment.isEmpty()) return@forEach - // get action & package name - val action = lineWithoutComment[0] - val currentPackage = lineWithoutComment.substring(1) - if (currentPackage.isEmpty()) return@forEach - if (!validatePackageNameOrAsterisk(currentPackage)) throw NotImplementedError("Invalid package name: $currentPackage") - @Suppress("LiftReturnOrAssignment") - when (action) { - '+' -> { - changed += addPackage(currentPackage) - } - '-' -> { - changed += removePackage(currentPackage) - } - else -> { - throw NotImplementedError("Action $action is not implemented") - } - } - } - } - - else -> throw NotImplementedError("Version $version is not implemented") - } - return changed - } -} \ No newline at end of file diff --git a/app/src/main/java/moe/lyniko/hiderecent/utils/ShizukuUtils.kt b/app/src/main/java/moe/lyniko/hiderecent/utils/ShizukuUtils.kt deleted file mode 100644 index 8501d83..0000000 --- a/app/src/main/java/moe/lyniko/hiderecent/utils/ShizukuUtils.kt +++ /dev/null @@ -1,43 +0,0 @@ -package moe.lyniko.hiderecent.utils - -import android.content.Context -import android.content.pm.PackageManager -import rikka.shizuku.Shizuku -import moe.lyniko.hiderecent.utils.PreferenceUtils.Companion.ConfigKeys - - - -fun isShizukuAvailable(): Boolean { - try { - Shizuku.pingBinder() - if (Shizuku.isPreV11()) { - return false - } - Shizuku.checkSelfPermission() - } catch (e: IllegalStateException) { - return false - } - return if (Shizuku.checkSelfPermission() == PackageManager.PERMISSION_GRANTED) { - true - } else if (Shizuku.shouldShowRequestPermissionRationale()) { - // Users choose "Deny and don't ask again" - false - } else { - // Request the permission - try { - Shizuku.requestPermission((Int.MIN_VALUE..Int.MAX_VALUE).random()) - } catch (e: IllegalStateException) { - // Shizuku not installed - @Suppress("UNUSED_EXPRESSION") - false - } - false - } -} - -fun isShizukuNeeded(context: Context): Boolean { - return PreferenceUtils.getInstance(context).managerPref.getBoolean( - ConfigKeys.ShowPackageForAllUser.key, - ConfigKeys.ShowPackageForAllUser.default, - ) -} \ No newline at end of file diff --git a/app/src/main/java/moe/lyniko/replacecursor/ImageProvider.kt b/app/src/main/java/moe/lyniko/replacecursor/ImageProvider.kt new file mode 100644 index 0000000..8510124 --- /dev/null +++ b/app/src/main/java/moe/lyniko/replacecursor/ImageProvider.kt @@ -0,0 +1,56 @@ +package moe.lyniko.replacecursor + +import android.annotation.SuppressLint +import android.content.ContentProvider +import android.content.ContentValues +import android.database.Cursor +import android.database.MatrixCursor +import android.net.Uri +import android.util.Log +import java.io.File + +class ImageProvider: ContentProvider() { + override fun onCreate(): Boolean { + return true + } + + // query: only get Uri last part as filename, send /data -> file + @SuppressLint("SdCardPath") + override fun query( + p0: Uri, + p1: Array?, + p2: String?, + p3: Array?, + p4: String? + ): Cursor { + Log.e("ReplaceCursor", "query: $p0") + // fetch Uri last part + val filename = p0.lastPathSegment + // get file path + val filePath = "/data/data/${BuildConfig.APPLICATION_ID}/files/$filename" + // read content + val fileContent = File(filePath).readBytes() + // make cursor + val cursor = MatrixCursor(arrayOf("data")) + // add row + cursor.addRow(arrayOf(fileContent)) + // return cursor + return cursor + } + + override fun getType(p0: Uri): String { + return "vnd.android.cursor.item/single" + } + + override fun insert(p0: Uri, p1: ContentValues?): Uri? { + return null + } + + override fun delete(p0: Uri, p1: String?, p2: Array?): Int { + return 0 + } + + override fun update(p0: Uri, p1: ContentValues?, p2: String?, p3: Array?): Int { + return 0 + } +} \ No newline at end of file diff --git a/app/src/main/java/moe/lyniko/replacecursor/MainActivity.kt b/app/src/main/java/moe/lyniko/replacecursor/MainActivity.kt new file mode 100644 index 0000000..b18fa52 --- /dev/null +++ b/app/src/main/java/moe/lyniko/replacecursor/MainActivity.kt @@ -0,0 +1,60 @@ +package moe.lyniko.replacecursor + +// https://stackoverflow.com/a/63877349 +// https://stackoverflow.com/a/1109108 +import android.os.Bundle +import android.widget.Toast +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Scaffold +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.SnackbarHostState +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Modifier +import androidx.navigation.compose.rememberNavController +import moe.lyniko.replacecursor.ui.AppNavHost +import moe.lyniko.replacecursor.ui.BottomNavigation +import moe.lyniko.replacecursor.ui.theme.MyApplicationTheme +import moe.lyniko.replacecursor.utils.PreferenceUtils + + +class MainActivity : ComponentActivity() { + private var snackbarHostState = SnackbarHostState() + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + try { + PreferenceUtils.getInstance(this) + } catch (e: SecurityException) { + e.printStackTrace() + Toast.makeText(this, getString(R.string.not_activated), Toast.LENGTH_LONG).show() + finish() + return + } + setContent { + MyApplicationTheme { + + val scope = rememberCoroutineScope() + val snackbarHostStateRemember = remember { snackbarHostState } + val navController = rememberNavController() + + Scaffold( + snackbarHost = { + SnackbarHost(hostState = snackbarHostStateRemember) + }, + bottomBar = { + BottomNavigation(navController = navController) + } + ) { innerPadding -> + AppNavHost( + navController = navController, + modifier = Modifier.padding(innerPadding) + ) + } + } + } + } + +} diff --git a/app/src/main/java/moe/lyniko/replacecursor/MainHook.kt b/app/src/main/java/moe/lyniko/replacecursor/MainHook.kt new file mode 100644 index 0000000..6058620 --- /dev/null +++ b/app/src/main/java/moe/lyniko/replacecursor/MainHook.kt @@ -0,0 +1,80 @@ +package moe.lyniko.replacecursor + +import android.annotation.SuppressLint +import android.app.AndroidAppHelper +import android.content.ContentResolver +import android.content.ContentUris +import android.content.ContentValues +import android.content.res.XResources +import android.graphics.drawable.Drawable +import android.net.Uri +import android.util.Log +import de.robv.android.xposed.IXposedHookZygoteInit +import de.robv.android.xposed.IXposedHookZygoteInit.StartupParam +import de.robv.android.xposed.SELinuxHelper +import de.robv.android.xposed.XSharedPreferences +import de.robv.android.xposed.XposedBridge +import moe.lyniko.replacecursor.utils.PreferenceUtils +import moe.lyniko.replacecursor.utils.ResourceHookEntry +import java.io.File + + +class MainHook : IXposedHookZygoteInit { + private var hooks: List + private var xsp: XSharedPreferences + @SuppressLint("SdCardPath") + private fun getFilePath(filename: String): String { + // sadly i don't know how to get context, so i have to use this + return "/data/data/${BuildConfig.APPLICATION_ID}/files/$filename" + } + + override fun initZygote(param: StartupParam) { + hooks.forEach { hook -> + if(!hook.enabled) return@forEach + try { + XposedBridge.log("ReplaceCursor - Setting ${hook.imageFile} for ${hook.resourceId}") + val imageBinary = PreferenceUtils.getImageBinaryFrom(xsp, hook.imageFile) + val drawable = Drawable.createFromStream(imageBinary.inputStream(), null)!! + XResources.setSystemWideReplacement( + "android", + "drawable", + hook.resourceId, + // get drawable from filePath + object : XResources.DrawableLoader() { + override fun newDrawable(xModuleResources: XResources, s: Int): Drawable { + /*val fileNameLastPart = File(hook.imageFile).name + val resolver: ContentResolver = + AndroidAppHelper.currentApplication().contentResolver + val uri = Uri.parse("content://moe.lyniko.replacecursor.ImageProvider/${fileNameLastPart}") + val cursor = resolver.query(uri, null, null, null, null)!! + cursor.moveToFirst() + val imageBinary = cursor.getBlob(0) + cursor.close() + val drawable = Drawable.createFromStream(imageBinary.inputStream(), null)*/ + // XposedBridge.log("ReplaceCursor - drawable: $drawable") + return drawable + } + } + ) + } + catch (e: Exception) { + e.printStackTrace() + } + } + /* Static Way: + val MODULE_PATH = param.modulePath; + val modRes = XModuleResources.createInstance(MODULE_PATH, null) + + XResources.setSystemWideReplacement("android", "drawable", "pointer_arrow", + modRes.fwd(R.drawable.arrow) + ) + */ + } + + init { + xsp = + XSharedPreferences(BuildConfig.APPLICATION_ID, PreferenceUtils.functionalConfigName) + xsp.makeWorldReadable() + hooks = PreferenceUtils.getResourceHooksFrom(xsp) + } +} diff --git a/app/src/main/java/moe/lyniko/hiderecent/MyApplication.kt b/app/src/main/java/moe/lyniko/replacecursor/MyApplication.kt similarity index 92% rename from app/src/main/java/moe/lyniko/hiderecent/MyApplication.kt rename to app/src/main/java/moe/lyniko/replacecursor/MyApplication.kt index f4ff4f3..75da3c9 100644 --- a/app/src/main/java/moe/lyniko/hiderecent/MyApplication.kt +++ b/app/src/main/java/moe/lyniko/replacecursor/MyApplication.kt @@ -1,4 +1,4 @@ -package moe.lyniko.hiderecent +package moe.lyniko.replacecursor import android.app.Application import android.content.res.Resources diff --git a/app/src/main/java/moe/lyniko/hiderecent/ui/AboutView.kt b/app/src/main/java/moe/lyniko/replacecursor/ui/AboutView.kt similarity index 97% rename from app/src/main/java/moe/lyniko/hiderecent/ui/AboutView.kt rename to app/src/main/java/moe/lyniko/replacecursor/ui/AboutView.kt index 30079f4..b743008 100644 --- a/app/src/main/java/moe/lyniko/hiderecent/ui/AboutView.kt +++ b/app/src/main/java/moe/lyniko/replacecursor/ui/AboutView.kt @@ -1,4 +1,4 @@ -package moe.lyniko.hiderecent.ui +package moe.lyniko.replacecursor.ui import android.content.Intent import android.net.Uri @@ -16,7 +16,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp -import moe.lyniko.hiderecent.R +import moe.lyniko.replacecursor.R @Composable diff --git a/app/src/main/java/moe/lyniko/hiderecent/ui/AppNavHost.kt b/app/src/main/java/moe/lyniko/replacecursor/ui/AppNavHost.kt similarity index 98% rename from app/src/main/java/moe/lyniko/hiderecent/ui/AppNavHost.kt rename to app/src/main/java/moe/lyniko/replacecursor/ui/AppNavHost.kt index 7d3a5c5..59dcd57 100644 --- a/app/src/main/java/moe/lyniko/hiderecent/ui/AppNavHost.kt +++ b/app/src/main/java/moe/lyniko/replacecursor/ui/AppNavHost.kt @@ -1,4 +1,4 @@ -package moe.lyniko.hiderecent.ui +package moe.lyniko.replacecursor.ui import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Home @@ -19,7 +19,7 @@ import androidx.navigation.NavHostController import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.currentBackStackEntryAsState -import moe.lyniko.hiderecent.R +import moe.lyniko.replacecursor.R sealed class NavigationItem(val route: String, val icon: ImageVector, val title: Int) { object Home : NavigationItem("Home", Icons.Filled.Home, title = R.string.title_home) diff --git a/app/src/main/java/moe/lyniko/replacecursor/ui/HomeView.kt b/app/src/main/java/moe/lyniko/replacecursor/ui/HomeView.kt new file mode 100644 index 0000000..7ea587b --- /dev/null +++ b/app/src/main/java/moe/lyniko/replacecursor/ui/HomeView.kt @@ -0,0 +1,262 @@ +package moe.lyniko.replacecursor.ui + +import android.annotation.SuppressLint +import android.app.Activity +import android.content.Context +import android.content.ContextWrapper +import android.content.Intent +import android.graphics.drawable.Drawable +import android.util.Log +import androidx.activity.ComponentActivity +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.ActivityResult +import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Clear +import androidx.compose.material3.Button +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.Switch +import androidx.compose.material3.Text +import androidx.compose.material3.TextField +import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.key +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.unit.dp +import com.google.accompanist.drawablepainter.rememberDrawablePainter +import moe.lyniko.replacecursor.BuildConfig +import moe.lyniko.replacecursor.ui.theme.MyApplicationTheme +import moe.lyniko.replacecursor.utils.PreferenceUtils +import moe.lyniko.replacecursor.utils.ResourceHookEntry +import java.lang.NullPointerException + +fun Context.getActivity(): ComponentActivity? = when (this) { + is ComponentActivity -> this + is ContextWrapper -> baseContext.getActivity() + else -> null +} + +fun isResourceIdValid(resourceId: String): Boolean { + return resourceId.matches(Regex("^[a-z0-9_]+$")) +} + +private lateinit var preferenceUtils: PreferenceUtils +private var snackbarHostState = SnackbarHostState() + +@Composable +fun HomeView() { + val context = LocalContext.current + val recomposeEntries: MutableState = remember { mutableIntStateOf(0) } + preferenceUtils = PreferenceUtils.getInstance(context) + MyApplicationTheme { + val snackbarHostStateRemember = remember { snackbarHostState } + + Scaffold( + snackbarHost = { + SnackbarHost(hostState = snackbarHostStateRemember) + }, + ) { innerPadding -> + Column(modifier = Modifier.padding(innerPadding)) { + key(recomposeEntries.value){ + MainEntries(recomposeEntries) + } + MainSettings(recomposeEntries) + } + } + } +} + +@SuppressLint("SetWorldReadable", "SdCardPath") +@Composable +private fun MainSettings(recomposeEntries: MutableState) { + val context = LocalContext.current + val activity = context.getActivity()!! + var resultFile by remember { mutableStateOf("") } + var resourceId by remember { mutableStateOf("") } + @Suppress("BlockingMethodInNonBlockingContext") val startForResult = + rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) { result: ActivityResult -> + if (result.resultCode == Activity.RESULT_OK) { + val uri = result.data?.data + if (uri != null) { + val takeFlags: Int = Intent.FLAG_GRANT_READ_URI_PERMISSION + activity.contentResolver.takePersistableUriPermission( + uri, + takeFlags + ) + // copy to data dir + val inputStream = activity.contentResolver.openInputStream(uri)!! + resultFile = "${resourceId}_${System.currentTimeMillis()}" + preferenceUtils.setImageBinary(resultFile, inputStream.readBytes()) + /* + // random hardcoded for xpsoed + val resultFileObject = File("/data/data/${BuildConfig.APPLICATION_ID}/files/").resolve(resultFile) + val outputStream = resultFileObject.outputStream() + inputStream!!.copyTo(outputStream) + outputStream.close() + resultFileObject.setReadable(true, false) + resultFile = resultFileObject.absolutePath + */ + inputStream.close() + // Log.e("HomeView", "resultFilename: $resultFile") + } + } + } + // a card with resource id input and image file input + + //1. display a card + Column( + modifier = Modifier + .fillMaxWidth() + .padding(8.dp) + ) { + //2. display text field for resource id + TextField( + value = resourceId, + onValueChange = { resourceId = it }, + label = { Text(text = "Resource ID") }, + modifier = Modifier.fillMaxWidth() + ) + //3. display Button for image file, use SAF to select + Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { + Button( + enabled = isResourceIdValid(resourceId), + onClick = { + // open saf to get image + val intent = activity.let { + Intent(Intent.ACTION_OPEN_DOCUMENT).apply { + addCategory(Intent.CATEGORY_OPENABLE) + type = "image/*" + } + } + startForResult.launch(intent) + }) { + Text(text = "Select Image File") + } + //4. display Button for add + Button( + enabled = isResourceIdValid(resourceId) && resultFile.isNotEmpty(), + onClick = { + // add to preferenceUtils + preferenceUtils.addResourceHook( + ResourceHookEntry( + resourceId, + resultFile, + true, + 0 + ) + ) + // refresh + resourceId = "" + resultFile = "" + recomposeEntries.value += 1 + }) { + Text(text = "Add") + } + } + } +} + +@Composable +private fun MainEntries(recomposeEntries: MutableState) { + val hooks by remember { mutableStateOf(preferenceUtils.resourceHooks) } + hooks.forEach { + SingleEntry(it, recomposeEntries) + } +} + +@Composable +private fun SingleEntry(res: ResourceHookEntry, recomposeEntries: MutableState) { + // val activity = LocalContext.current.getActivity()!! + + // get resource_name, image from preferenceUtils, display them along with a switch + // when switch is toggled, update preferenceUtils + + val imageBinary: ByteArray + try{ + imageBinary = preferenceUtils.getImageBinary(res.imageFile) + // Log.e("HomeView", "imageBinary: ${imageBinary.size}") + } + catch (e: NullPointerException) { + e.printStackTrace() + Log.w(BuildConfig.APPLICATION_ID, "image ${res.imageFile} not found, removed") + preferenceUtils.removeResourceHook(res) + return + } + + //1. display a card + Row(modifier=Modifier.fillMaxWidth()){ + //2. display image + // Log.e("HomeView", "res.imageFile: ${res.imageFile}") + Image( + painter = rememberDrawablePainter( + drawable = Drawable.createFromStream( + imageBinary.inputStream(), + null + ) + ), + contentDescription = null, + modifier = Modifier.size(48.dp) + ) + //3. display resource id + Column( + modifier = Modifier.align(Alignment.CenterVertically), + ) { + Text(text = res.resourceId, style = MaterialTheme.typography.titleLarge) + } + Spacer(Modifier.weight(1f)) + //4. display switch + var checked by remember { mutableStateOf(res.enabled) } + Switch(checked = checked, onCheckedChange = { + res.enabled = it + preferenceUtils.updateResourceHook(res) + checked = it + }) + //5. delete button + IconButton(onClick = { + preferenceUtils.removeResourceHook(res) + recomposeEntries.value += 1 + }) { + Icon( + imageVector = Icons.Default.Clear, + contentDescription = null + ) + } + /* + Test Content Provider + val resolver = activity.contentResolver + val uri = Uri.parse("content://moe.lyniko.replacecursor.ImageProvider/${File(res.imageFile).name}") + val cursor = resolver.query(uri, null, null, null, null) + if (cursor != null) { + cursor.moveToFirst() + val imageBinary = cursor.getBlob(0) + cursor.close() + Log.e("HomeView", "imageBinary: ${imageBinary.size}") + Text(text = "imageBinary: ${imageBinary.size}") + } + else { + Log.e("HomeView", "cursor is null") + Text(text = "cursor is null") + } + */ + } +} \ No newline at end of file diff --git a/app/src/main/java/moe/lyniko/replacecursor/ui/SettingsView.kt b/app/src/main/java/moe/lyniko/replacecursor/ui/SettingsView.kt new file mode 100644 index 0000000..40b2c7b --- /dev/null +++ b/app/src/main/java/moe/lyniko/replacecursor/ui/SettingsView.kt @@ -0,0 +1,34 @@ +package moe.lyniko.replacecursor.ui + +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import me.zhanghai.compose.preference.ProvidePreferenceLocals +import me.zhanghai.compose.preference.getPreferenceFlow +import me.zhanghai.compose.preference.switchPreference +import moe.lyniko.replacecursor.R +import moe.lyniko.replacecursor.utils.PreferenceUtils +import moe.lyniko.replacecursor.utils.PreferenceUtils.Companion.ConfigKeys + + +@Composable +fun SettingsView() { + val context = LocalContext.current + val managerPref = PreferenceUtils.getInstance(context).managerPref + ProvidePreferenceLocals( + flow = managerPref.getPreferenceFlow() + ) { + Text(text = "Currently empty.") +// LazyColumn(modifier = Modifier.fillMaxSize()) { +// switchPreference( +// key=ConfigKeys.HideNoActivityPackages.key, +// defaultValue = ConfigKeys.HideNoActivityPackages.default, +// title = { Text(context.getString(R.string.hide_no_activity_packages)) }, +// summary = { Text(context.getString(R.string.hide_no_activity_packages_summary)) }, +// ) +// } + } +} \ No newline at end of file diff --git a/app/src/main/java/moe/lyniko/hiderecent/ui/theme/Color.kt b/app/src/main/java/moe/lyniko/replacecursor/ui/theme/Color.kt similarity index 85% rename from app/src/main/java/moe/lyniko/hiderecent/ui/theme/Color.kt rename to app/src/main/java/moe/lyniko/replacecursor/ui/theme/Color.kt index aa06187..8dc8a06 100644 --- a/app/src/main/java/moe/lyniko/hiderecent/ui/theme/Color.kt +++ b/app/src/main/java/moe/lyniko/replacecursor/ui/theme/Color.kt @@ -1,4 +1,4 @@ -package moe.lyniko.hiderecent.ui.theme +package moe.lyniko.replacecursor.ui.theme import androidx.compose.ui.graphics.Color diff --git a/app/src/main/java/moe/lyniko/hiderecent/ui/theme/Theme.kt b/app/src/main/java/moe/lyniko/replacecursor/ui/theme/Theme.kt similarity index 98% rename from app/src/main/java/moe/lyniko/hiderecent/ui/theme/Theme.kt rename to app/src/main/java/moe/lyniko/replacecursor/ui/theme/Theme.kt index 58e4b6e..eac6dc5 100644 --- a/app/src/main/java/moe/lyniko/hiderecent/ui/theme/Theme.kt +++ b/app/src/main/java/moe/lyniko/replacecursor/ui/theme/Theme.kt @@ -1,4 +1,4 @@ -package moe.lyniko.hiderecent.ui.theme +package moe.lyniko.replacecursor.ui.theme import android.app.Activity import android.os.Build diff --git a/app/src/main/java/moe/lyniko/hiderecent/ui/theme/Type.kt b/app/src/main/java/moe/lyniko/replacecursor/ui/theme/Type.kt similarity index 95% rename from app/src/main/java/moe/lyniko/hiderecent/ui/theme/Type.kt rename to app/src/main/java/moe/lyniko/replacecursor/ui/theme/Type.kt index 8dd65cd..bf39d80 100644 --- a/app/src/main/java/moe/lyniko/hiderecent/ui/theme/Type.kt +++ b/app/src/main/java/moe/lyniko/replacecursor/ui/theme/Type.kt @@ -1,4 +1,4 @@ -package moe.lyniko.hiderecent.ui.theme +package moe.lyniko.replacecursor.ui.theme import androidx.compose.material3.Typography import androidx.compose.ui.text.TextStyle diff --git a/app/src/main/java/moe/lyniko/replacecursor/utils/PreferenceUtils.kt b/app/src/main/java/moe/lyniko/replacecursor/utils/PreferenceUtils.kt new file mode 100644 index 0000000..2e9ad54 --- /dev/null +++ b/app/src/main/java/moe/lyniko/replacecursor/utils/PreferenceUtils.kt @@ -0,0 +1,130 @@ +package moe.lyniko.replacecursor.utils + +import android.annotation.SuppressLint +import android.content.Context +import android.content.SharedPreferences +import android.util.Base64 +import android.util.Log + +class ResourceHookEntry(resource_id: String, image_file: String, var enabled: Boolean, + var version: Int +) { + var resourceId: String = resource_id + var imageFile: String = image_file + + fun toPerfString(): String { + return "$resourceId$delimiter$imageFile$delimiter$enabled$delimiter$version" + } + companion object { + const val delimiter = ":" + fun fromPerfString(perfString: String): ResourceHookEntry { + val split = perfString.split(delimiter, limit = 4) + return ResourceHookEntry(split[0], split[1], split[2].toBoolean(), split[3].toInt()) + } + } +} + +@SuppressLint("WorldReadableFiles") +class PreferenceUtils( // init context on constructor + context: Context +) { + // ------ 1. get several SharedPreferences (funcPref is the only accessible during Xposed inject) ------ + private var funcPref: SharedPreferences = try { + @Suppress("DEPRECATION") + context.getSharedPreferences(functionalConfigName, Context.MODE_WORLD_READABLE) + } catch (e: SecurityException) { + throw e + // Log.w("PreferenceUtil", "Fallback to Private SharedPref for error!!!: ${e.message}") + // context.getSharedPreferences(functionalConfigName, Context.MODE_PRIVATE) + } + + var managerPref: SharedPreferences = + context.getSharedPreferences(managerConfigName, Context.MODE_PRIVATE) + var resourceHooks: List + + companion object { + @Volatile + private var instance: PreferenceUtils? = null + + fun getInstance(context: Context) = + instance ?: synchronized(this) { + instance ?: PreferenceUtils(context).also { instance = it } + } + + const val functionalConfigName = "functional_config" + const val managerConfigName = "manager_config" + const val resourceHookConfigName = "resource_hook_config" + const val imageBinaryPrefix = "image_binary_" + + enum class ConfigKeys(val key: String, val default: Boolean) { + // HideNoActivityPackages("hide_no_activity_packages", true) + } + + fun getResourceHooksFrom(preferences: SharedPreferences): List { + val resourceHooks = + preferences.getStringSet(resourceHookConfigName, null) ?: return listOf() + return resourceHooks.map { ResourceHookEntry.fromPerfString(it) } + } + + fun getImageBinaryFrom(preferences: SharedPreferences, filename: String): ByteArray { + val base64 = preferences.getString(imageBinaryPrefix + filename, null)!! + return Base64.decode(base64, Base64.NO_WRAP) + } + } + + // ------ 2. get/set several SharedPreferences ------ + private fun initResourceHooks(): List { + return getResourceHooksFrom(funcPref) + } + + init { + resourceHooks = initResourceHooks() + } + + private fun saveResourceHooks() { + // Log.e("PreferenceUtils", "saveResourceHooks: $resourceHooks") + val resourceHooksString = resourceHooks.map { it.toPerfString() }.toSet() + funcPref.edit().putStringSet(resourceHookConfigName, resourceHooksString).apply() + } + + fun addResourceHook(resourceHook: ResourceHookEntry) { + // Log.e("PreferenceUtils", "addResourceHook: $resourceHook") + resourceHooks = resourceHooks + resourceHook + saveResourceHooks() + } + + fun removeResourceHook(resourceHook: ResourceHookEntry) { + // Log.e("PreferenceUtils", "removeResourceHook: $resourceHook") + removeImageBinary(resourceHook.imageFile) + resourceHooks = resourceHooks - resourceHook + saveResourceHooks() + } + + private fun getResourceHook(resourceId: String): ResourceHookEntry? { + return resourceHooks.find { it.resourceId == resourceId } + } + + fun updateResourceHook(resourceHook: ResourceHookEntry) { + // Log.e("PreferenceUtils", "updateResourceHook: $resourceHook") + val oldResourceHook = getResourceHook(resourceHook.resourceId) + if (oldResourceHook == null) { + addResourceHook(resourceHook) + } else { + removeResourceHook(oldResourceHook) + addResourceHook(resourceHook) + } + } + @SuppressLint("ApplySharedPref") + fun setImageBinary(filename: String, data: ByteArray) { + // Log.e("PreferenceUtils", "setImageBinary: $filename") + funcPref.edit().putString(imageBinaryPrefix+filename, Base64.encodeToString(data, Base64.NO_WRAP)).apply() + } + fun getImageBinary(filename: String): ByteArray { + // Log.e("PreferenceUtils", "getImageBinary: $filename") + return getImageBinaryFrom(funcPref, filename) + } + private fun removeImageBinary(filename: String) { + // Log.e("PreferenceUtils", "removeImageBinary: $filename") + funcPref.edit().remove(imageBinaryPrefix+filename).apply() + } +} \ No newline at end of file diff --git a/app/src/main/res/drawable/arrow.png b/app/src/main/res/drawable/arrow.png new file mode 100644 index 0000000..3a9bd39 Binary files /dev/null and b/app/src/main/res/drawable/arrow.png differ diff --git a/app/src/main/res/drawable/arrow_icon.xml b/app/src/main/res/drawable/arrow_icon.xml new file mode 100644 index 0000000..5eda937 --- /dev/null +++ b/app/src/main/res/drawable/arrow_icon.xml @@ -0,0 +1,4 @@ + diff --git a/app/src/main/res/drawable/baseline_swap_horiz_24.xml b/app/src/main/res/drawable/baseline_swap_horiz_24.xml deleted file mode 100644 index 9652082..0000000 --- a/app/src/main/res/drawable/baseline_swap_horiz_24.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 85e1cb9..1dce463 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -1,15 +1,17 @@ - 隐藏最近任务 - 从最近任务列表中隐藏特定任务 + 更改指针图标 + + 主页 + 设置 + 关于 + 选择图标 + 系统应用 用户应用 搜索… 在显示 用户应用/系统应用 间切换。 请先在 LSPosed 中激活并强制停止本模块。 - 主页 - 设置 - 关于 清除搜索框 此模块仅能在主用户(用户0)中使用。当前用户为$1%d。 Shizuku / Sui 未运行,仅显示当前用户的应用。 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 2ead704..976e63c 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,18 +1,21 @@ - https://github.com/Young-Lord/hideRecent - Hide Recent Task - Hide specific apps from recent task list + https://github.com/Young-Lord/replaceCursor + Replace Cursor + + Home + Settings + About + Select icon + SystemApp UserApp Search… Switch display of user / system apps. Please activate this module in LSPosed Manager, then force stop. + android - Home - Settings - About Clear search box Shizuku / Sui not working, only current user\'s packages are displayed. This module should be used in main user (user 0) only. Current user is %1$d.