├── .github └── workflows │ └── build.yml ├── .gitignore ├── LICENSE ├── README.md ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ ├── assets │ └── xposed_init │ ├── java │ └── moe │ │ └── lyniko │ │ └── hiderecent │ │ ├── MainActivity.kt │ │ ├── MainHook.kt │ │ ├── MyApplication.kt │ │ ├── ui │ │ ├── AboutView.kt │ │ ├── AppNavHost.kt │ │ ├── HomeView.kt │ │ ├── SettingsView.kt │ │ └── theme │ │ │ ├── Color.kt │ │ │ ├── Theme.kt │ │ │ └── Type.kt │ │ └── utils │ │ ├── AppUtils.kt │ │ ├── MultiUserUtils.kt │ │ ├── PreferenceUtils.kt │ │ └── ShizukuUtils.kt │ └── res │ ├── drawable-v24 │ └── ic_launcher_foreground.xml │ ├── drawable │ ├── baseline_swap_horiz_24.xml │ └── ic_launcher_background.xml │ ├── mipmap-anydpi-v26 │ ├── ic_launcher.xml │ └── ic_launcher_round.xml │ ├── mipmap-hdpi │ ├── ic_launcher.webp │ └── ic_launcher_round.webp │ ├── mipmap-mdpi │ ├── ic_launcher.webp │ └── ic_launcher_round.webp │ ├── mipmap-xhdpi │ ├── ic_launcher.webp │ └── ic_launcher_round.webp │ ├── mipmap-xxhdpi │ ├── ic_launcher.webp │ └── ic_launcher_round.webp │ ├── mipmap-xxxhdpi │ ├── ic_launcher.webp │ └── ic_launcher_round.webp │ ├── values-zh-rCN │ └── strings.xml │ ├── values │ ├── colors.xml │ ├── dimens.xml │ ├── strings.xml │ └── themes.xml │ └── xml │ ├── backup_rules.xml │ └── data_extraction_rules.xml ├── assets └── image │ └── preview.jpg ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── settings.gradle /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | paths-ignore: 7 | - '.github/ISSUE_TEMPLATE/**' 8 | - '.github/workflows/issue.yml' 9 | - '**.md' 10 | workflow_dispatch: 11 | 12 | jobs: 13 | build: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - name: Checkout the code 17 | uses: actions/checkout@v3 18 | 19 | - name: Set up JDK 20 | uses: actions/setup-java@v3 21 | with: 22 | java-version: '17' 23 | distribution: 'temurin' 24 | 25 | - name: Cache gradle 26 | uses: actions/cache@v3 27 | with: 28 | path: | 29 | ~/.gradle/caches 30 | ~/.gradle/wrapper 31 | key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle') }} 32 | restore-keys: ${{ runner.os }}-gradle- 33 | 34 | - name: Build the app 35 | run: | 36 | if [ ! -f "gradlew" ]; then gradle wrapper; fi 37 | chmod +x gradlew 38 | ./gradlew assemble --stacktrace 39 | 40 | - name: Upload APK 41 | uses: actions/upload-artifact@v4 42 | with: 43 | name: my-build-apk 44 | path: app/build/outputs/apk 45 | 46 | - name: Upload mapping 47 | uses: actions/upload-artifact@v4 48 | with: 49 | name: mapping 50 | path: app/build/outputs/mapping 51 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Gradle files 2 | .gradle/ 3 | build/ 4 | 5 | # Local configuration file (sdk path, etc) 6 | local.properties 7 | 8 | # Log/OS Files 9 | *.log 10 | 11 | # Android Studio generated files and folders 12 | captures/ 13 | .externalNativeBuild/ 14 | .cxx/ 15 | *.apk 16 | output.json 17 | 18 | # IntelliJ 19 | *.iml 20 | .idea/ 21 | misc.xml 22 | deploymentTargetDropDown.xml 23 | render.experimental.xml 24 | 25 | # Keystore files 26 | *.jks 27 | *.keystore 28 | 29 | # Google Services (e.g. APIs or Firebase) 30 | google-services.json 31 | 32 | # Android Profiling 33 | *.hprof -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 LY 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Hide App from Recent Task List 2 | 3 | Simple module to hide any app from recent task list. 4 | 5 | Designed in pure Kotlin & Jetpack Compose & Material Design 3. Can be a template for any Xposed module with an application selection list. 6 | 7 | ![UI Screenshot](https://github.com/Young-Lord/hideRecent/raw/master/assets/image/preview.jpg) 8 | 9 | ## How to use 10 | 11 | > 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) 12 | 13 | 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 14 | 2. Force stop module 15 | 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) 16 | 4. Reboot (you MUST reboot when you modify the list, or changes will not be applied until next reboot) 17 | 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. 18 | 19 | ## Module Scope 20 | 21 | - android 22 | 23 | ## Project URL 24 | 25 | Home URL: 26 | 27 | Xposed Modules Repo URL: 28 | 29 | ## Technical Details 30 | 31 | UI: Material Design 3 + Jetpack Compose + Kotlin. 32 | 33 | 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. 34 | 35 | ## HELP ME IT DOESNT WORK!!! 36 | 37 | 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. 38 | 39 | 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. 40 | 41 | PR for refactoring is also appreciated. 42 | 43 | ## License 44 | 45 | Apache-2.0 License or MIT License are all OK. 46 | 47 | ## Thanks 48 | 49 | 50 | 51 | (Apache-2.0 license) 52 | 53 | ~~Original code from: ~~ refactored. 54 | 55 | [rootAVD](https://gitlab.com/newbit/rootAVD) 56 | 57 | ## Why? 58 | 59 | 出于隐私或便捷原因,有些时候我们总是想隐藏一些应用。 60 | 61 | CrDroid 内置了这个功能,这是好的,然而并不是所有人都在用 CrDroid。 62 | 63 | 而且,国内的 ROM 的“最近任务列表”里划掉一个卡片,就等于杀死这个应用,这太蠢了!你也不想你的 Clash For Android 编辑完配置就挂了吧? 64 | 65 | Thanox 等一些应用也有这个功能,但只为了这个功能氪金并装一个闭源应用,怎么看都很怪。于是我买了 Thanox 订阅,然后写完这个模块后又卖了。 66 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'com.android.application' 3 | id 'org.jetbrains.kotlin.android' 4 | } 5 | 6 | android { 7 | namespace 'moe.lyniko.hiderecent' 8 | compileSdk 34 9 | 10 | defaultConfig { 11 | applicationId "moe.lyniko.hiderecent" 12 | minSdk 29 13 | targetSdk 34 14 | versionCode 210 15 | versionName "2.1.0" 16 | } 17 | 18 | buildTypes { 19 | release { 20 | zipAlignEnabled true 21 | minifyEnabled true 22 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 23 | signingConfig signingConfigs.debug 24 | } 25 | debug { 26 | applicationIdSuffix '.debug' 27 | versionNameSuffix '-debug' 28 | } 29 | } 30 | compileOptions { 31 | sourceCompatibility JavaVersion.VERSION_1_8 32 | targetCompatibility JavaVersion.VERSION_1_8 33 | } 34 | kotlinOptions { 35 | jvmTarget = '1.8' 36 | } 37 | buildFeatures { 38 | buildConfig true 39 | compose true 40 | } 41 | composeOptions { 42 | kotlinCompilerExtensionVersion '1.4.3' 43 | } 44 | packagingOptions { 45 | resources { 46 | excludes += '/META-INF/{AL2.0,LGPL2.1}' 47 | } 48 | } 49 | } 50 | 51 | dependencies { 52 | // module stuff 53 | compileOnly 'de.robv.android.xposed:api:82' 54 | implementation 'org.lsposed.hiddenapibypass:hiddenapibypass:4.3' 55 | def shizuku_version = '13.1.5' 56 | implementation "dev.rikka.shizuku:api:$shizuku_version" 57 | implementation "dev.rikka.shizuku:provider:$shizuku_version" 58 | implementation 'me.zhanghai.compose.preference:library:1.0.0' 59 | 60 | // ui stuff 61 | implementation "com.google.accompanist:accompanist-drawablepainter:0.28.0" 62 | implementation "androidx.navigation:navigation-compose:2.7.6" 63 | implementation 'androidx.core:core-ktx:1.12.0' 64 | implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.6.2' 65 | implementation 'androidx.activity:activity-compose:1.8.2' 66 | implementation platform('androidx.compose:compose-bom:2022.10.00') 67 | implementation 'androidx.compose.ui:ui' 68 | implementation 'androidx.compose.ui:ui-graphics' 69 | implementation 'androidx.compose.ui:ui-tooling-preview' 70 | implementation 'androidx.compose.material3:material3' 71 | 72 | 73 | debugImplementation 'androidx.compose.ui:ui-tooling' 74 | debugImplementation 'androidx.compose.ui:ui-test-manifest' 75 | } 76 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | -keep class moe.lyniko.hiderecent.MainHook 23 | # -keep class rikka.shizuku.SystemServiceHelper 24 | # -keep class android.** { *; } -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 7 | 8 | 19 | 22 | 25 | 28 | 31 | 34 | 37 | 38 | 39 | 40 | 41 | 42 | 47 | 48 | 49 | 50 | 51 | 52 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /app/src/main/assets/xposed_init: -------------------------------------------------------------------------------- 1 | moe.lyniko.hiderecent.MainHook -------------------------------------------------------------------------------- /app/src/main/java/moe/lyniko/hiderecent/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package moe.lyniko.hiderecent 2 | 3 | // https://stackoverflow.com/a/63877349 4 | // https://stackoverflow.com/a/1109108 5 | import android.os.Bundle 6 | import android.os.Process.myUserHandle 7 | import android.util.Log 8 | import android.widget.Toast 9 | import androidx.activity.ComponentActivity 10 | import androidx.activity.compose.setContent 11 | import androidx.compose.foundation.layout.padding 12 | import androidx.compose.material3.Scaffold 13 | import androidx.compose.material3.SnackbarDuration 14 | import androidx.compose.material3.SnackbarHost 15 | import androidx.compose.material3.SnackbarHostState 16 | import androidx.compose.runtime.LaunchedEffect 17 | import androidx.compose.runtime.remember 18 | import androidx.compose.runtime.rememberCoroutineScope 19 | import androidx.compose.ui.Modifier 20 | import androidx.navigation.compose.rememberNavController 21 | import kotlinx.coroutines.launch 22 | import moe.lyniko.hiderecent.ui.AppNavHost 23 | import moe.lyniko.hiderecent.ui.BottomNavigation 24 | import moe.lyniko.hiderecent.ui.theme.MyApplicationTheme 25 | import moe.lyniko.hiderecent.utils.PreferenceUtils 26 | import moe.lyniko.hiderecent.utils.getIdByUserHandle 27 | import moe.lyniko.hiderecent.utils.isShizukuAvailable 28 | import moe.lyniko.hiderecent.utils.isShizukuNeeded 29 | import kotlin.system.exitProcess 30 | 31 | 32 | class MainActivity : ComponentActivity() { 33 | private var snackbarHostState = SnackbarHostState() 34 | 35 | override fun onCreate(savedInstanceState: Bundle?) { 36 | super.onCreate(savedInstanceState) 37 | try { 38 | PreferenceUtils.getInstance(this) 39 | } catch (e: SecurityException) { 40 | Toast.makeText(this, getString(R.string.not_activated), Toast.LENGTH_LONG).show() 41 | finish() 42 | return 43 | } 44 | setContent { 45 | MyApplicationTheme { 46 | 47 | val scope = rememberCoroutineScope() 48 | val snackbarHostStateRemember = remember { snackbarHostState } 49 | val navController = rememberNavController() 50 | 51 | Scaffold( 52 | snackbarHost = { 53 | SnackbarHost(hostState = snackbarHostStateRemember) 54 | }, 55 | bottomBar = { 56 | BottomNavigation(navController = navController) 57 | } 58 | ) { innerPadding -> 59 | AppNavHost( 60 | navController = navController, 61 | modifier = Modifier.padding(innerPadding) 62 | ) 63 | } 64 | 65 | // check main user 66 | val userId = getUserId() 67 | if (userId != 0) { 68 | LaunchedEffect(snackbarHostState) { 69 | scope.launch { 70 | snackbarHostState.showSnackbar( 71 | getString(R.string.main_user_only, userId), 72 | actionLabel = getString(R.string.dismiss_notification), 73 | duration = SnackbarDuration.Indefinite 74 | ) 75 | } 76 | } 77 | Log.w(BuildConfig.APPLICATION_ID, "Wrong user id: $userId") 78 | } 79 | 80 | // check shizuku 81 | if (isShizukuNeeded(this) && !isShizukuAvailable()) { 82 | LaunchedEffect(snackbarHostState) { 83 | scope.launch { 84 | snackbarHostState.showSnackbar( 85 | getString(R.string.shizuku_not_available_toast), 86 | actionLabel = getString(R.string.dismiss_notification), 87 | duration = SnackbarDuration.Indefinite 88 | ) 89 | } 90 | } 91 | Log.w(BuildConfig.APPLICATION_ID, "Shizuku not running") 92 | } 93 | } 94 | } 95 | } 96 | 97 | } 98 | 99 | fun getUserId(): Int { 100 | val userHandle = myUserHandle() 101 | var userId = 0 102 | try { 103 | userId = getIdByUserHandle(userHandle) 104 | } catch (e: Exception) { 105 | Log.e(BuildConfig.APPLICATION_ID, "Error when getting user id: ${e.message}") 106 | } 107 | return userId 108 | } -------------------------------------------------------------------------------- /app/src/main/java/moe/lyniko/hiderecent/MainHook.kt: -------------------------------------------------------------------------------- 1 | package moe.lyniko.hiderecent 2 | 3 | import android.content.Intent 4 | import de.robv.android.xposed.IXposedHookLoadPackage 5 | import de.robv.android.xposed.XC_MethodHook 6 | import de.robv.android.xposed.XSharedPreferences 7 | import de.robv.android.xposed.XposedHelpers.callMethod 8 | import de.robv.android.xposed.XposedHelpers.findAndHookMethod 9 | import de.robv.android.xposed.callbacks.XC_LoadPackage.LoadPackageParam 10 | import de.robv.android.xposed.XposedBridge 11 | import moe.lyniko.hiderecent.utils.PreferenceUtils 12 | 13 | class MainHook : IXposedHookLoadPackage { 14 | 15 | override fun handleLoadPackage(lpparam: LoadPackageParam) { 16 | if (lpparam.packageName == "android") onAppHooked(lpparam) 17 | } 18 | 19 | private fun onAppHooked(lpparam: LoadPackageParam) { 20 | val visibleFilterHook: XC_MethodHook = object : XC_MethodHook() { 21 | override fun beforeHookedMethod(param: MethodHookParam) { 22 | val intent = callMethod(param.args[0], "getBaseIntent") as Intent 23 | if (BuildConfig.DEBUG) { 24 | XposedBridge.log("Hide - Current Intent: $intent") 25 | XposedBridge.log("Hide - Current component: ${intent.component}") 26 | XposedBridge.log("Hide - Current package: ${intent.component?.packageName}") 27 | } 28 | val packageName = intent.component?.packageName ?: return 29 | if (packages.contains(packageName)) { 30 | param.result = false 31 | } 32 | } 33 | } 34 | try { 35 | findAndHookMethod( 36 | "com.android.server.wm.RecentTasks", 37 | lpparam.classLoader, 38 | "isVisibleRecentTask", 39 | "com.android.server.wm.Task", 40 | visibleFilterHook 41 | ) 42 | } catch (ignored: Throwable) { 43 | } 44 | } 45 | 46 | private val packages: MutableSet 47 | 48 | init { 49 | val xsp = 50 | XSharedPreferences(BuildConfig.APPLICATION_ID, PreferenceUtils.functionalConfigName) 51 | xsp.makeWorldReadable() 52 | packages = PreferenceUtils.getPackageListFromPref(xsp) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /app/src/main/java/moe/lyniko/hiderecent/MyApplication.kt: -------------------------------------------------------------------------------- 1 | package moe.lyniko.hiderecent 2 | 3 | import android.app.Application 4 | import android.content.res.Resources 5 | 6 | // https://stackoverflow.com/a/54686443/22911792 7 | class MyApplication : Application() { 8 | companion object { 9 | lateinit var instance: Application 10 | lateinit var resourcesPublic: Resources 11 | } 12 | 13 | override fun onCreate() { 14 | super.onCreate() 15 | instance = this 16 | resourcesPublic = resources 17 | } 18 | } -------------------------------------------------------------------------------- /app/src/main/java/moe/lyniko/hiderecent/ui/AboutView.kt: -------------------------------------------------------------------------------- 1 | package moe.lyniko.hiderecent.ui 2 | 3 | import android.content.Intent 4 | import android.net.Uri 5 | import androidx.compose.foundation.layout.Column 6 | import androidx.compose.foundation.layout.Row 7 | import androidx.compose.foundation.layout.fillMaxSize 8 | import androidx.compose.foundation.layout.fillMaxWidth 9 | import androidx.compose.foundation.layout.padding 10 | import androidx.compose.foundation.lazy.LazyColumn 11 | import androidx.compose.material3.MaterialTheme 12 | import androidx.compose.material3.Surface 13 | import androidx.compose.material3.Text 14 | import androidx.compose.runtime.Composable 15 | import androidx.compose.ui.Modifier 16 | import androidx.compose.ui.platform.LocalContext 17 | import androidx.compose.ui.text.style.TextAlign 18 | import androidx.compose.ui.unit.dp 19 | import moe.lyniko.hiderecent.R 20 | 21 | 22 | @Composable 23 | fun AboutView() { 24 | // center a clickable to open project URL 25 | LazyColumn(modifier = Modifier.fillMaxSize()) { 26 | item { 27 | // card 28 | val context = LocalContext.current 29 | Surface( 30 | shape = MaterialTheme.shapes.medium, // 使用 MaterialTheme 自带的形状 31 | shadowElevation = 5.dp, 32 | modifier = Modifier 33 | .padding(all = 8.dp) 34 | .fillMaxWidth(), 35 | onClick = { 36 | // open project URL 37 | val browserIntent = 38 | Intent( 39 | Intent.ACTION_VIEW, 40 | Uri.parse(context.getString(R.string.project_url)) 41 | ) 42 | context.startActivity(browserIntent) 43 | } 44 | ) { 45 | Column { 46 | Row( 47 | modifier = Modifier.padding(all = 8.dp) 48 | ) { 49 | // url notice 50 | Text( 51 | text = "URL", 52 | textAlign = TextAlign.Center 53 | ) 54 | } 55 | Row( 56 | modifier = Modifier.padding(all = 8.dp) 57 | ) { 58 | // url 59 | Text( 60 | text = LocalContext.current.getString(R.string.project_url), 61 | textAlign = TextAlign.Center 62 | ) 63 | } 64 | } 65 | } 66 | } 67 | } 68 | } -------------------------------------------------------------------------------- /app/src/main/java/moe/lyniko/hiderecent/ui/AppNavHost.kt: -------------------------------------------------------------------------------- 1 | package moe.lyniko.hiderecent.ui 2 | 3 | import androidx.compose.material.icons.Icons 4 | import androidx.compose.material.icons.filled.Home 5 | import androidx.compose.material.icons.filled.Info 6 | import androidx.compose.material.icons.filled.Settings 7 | import androidx.compose.material3.Icon 8 | import androidx.compose.material3.NavigationBar 9 | import androidx.compose.material3.NavigationBarItem 10 | import androidx.compose.material3.Text 11 | import androidx.compose.runtime.Composable 12 | import androidx.compose.runtime.getValue 13 | import androidx.compose.ui.Modifier 14 | import androidx.compose.ui.graphics.vector.ImageVector 15 | import androidx.compose.ui.platform.LocalContext 16 | import androidx.navigation.NavDestination.Companion.hierarchy 17 | import androidx.navigation.NavGraph.Companion.findStartDestination 18 | import androidx.navigation.NavHostController 19 | import androidx.navigation.compose.NavHost 20 | import androidx.navigation.compose.composable 21 | import androidx.navigation.compose.currentBackStackEntryAsState 22 | import moe.lyniko.hiderecent.R 23 | 24 | sealed class NavigationItem(val route: String, val icon: ImageVector, val title: Int) { 25 | object Home : NavigationItem("Home", Icons.Filled.Home, title = R.string.title_home) 26 | object Settings : 27 | NavigationItem("Settings", Icons.Filled.Settings, title = R.string.title_settings) 28 | 29 | object About : NavigationItem("About", Icons.Filled.Info, title = R.string.title_about) 30 | } 31 | 32 | @Composable 33 | fun BottomNavigation( 34 | navController: NavHostController 35 | ) { 36 | val items = listOf( 37 | NavigationItem.Home, 38 | NavigationItem.Settings, 39 | NavigationItem.About, 40 | ) 41 | 42 | NavigationBar { 43 | val navBackStackEntry by navController.currentBackStackEntryAsState() 44 | val currentDestination = navBackStackEntry?.destination 45 | items.forEach { item -> 46 | // Log.w("BottomNavigation", "item: $item; currentDestination: $currentDestination") 47 | NavigationBarItem( 48 | // Text that shows bellow the icon 49 | label = { 50 | Text(text = LocalContext.current.getString(item.title)) 51 | }, 52 | // The icon resource 53 | icon = { 54 | Icon( 55 | item.icon, 56 | contentDescription = null 57 | ) 58 | }, 59 | selected = currentDestination?.hierarchy?.any { it.route == item.route } == true, 60 | onClick = { 61 | navController.navigate(item.route) { 62 | // https://medium.com/@KaushalVasava/navigation-in-jetpack-compose-full-guide-beginner-to-advanced-950c1133740 63 | // Pop up to the start destination of the graph to 64 | // avoid building up a large stack of destinations 65 | // on the back stack as users select items 66 | popUpTo(navController.graph.findStartDestination().id) { 67 | saveState = true 68 | } 69 | // Avoid multiple copies of the same destination when 70 | // re-selecting the same item 71 | launchSingleTop = true 72 | // Restore state when re-selecting a previously selected item 73 | restoreState = true 74 | } 75 | }, 76 | ) 77 | } 78 | } 79 | } 80 | 81 | @Composable 82 | fun AppNavHost( 83 | modifier: Modifier = Modifier, 84 | navController: NavHostController, 85 | startDestination: String = NavigationItem.Home.route, 86 | ) { 87 | NavHost( 88 | modifier = modifier, 89 | navController = navController, 90 | startDestination = startDestination 91 | ) { 92 | composable(NavigationItem.Home.route) { 93 | HomeView() 94 | } 95 | composable(NavigationItem.Settings.route) { 96 | SettingsView() 97 | } 98 | composable(NavigationItem.About.route) { 99 | AboutView() 100 | } 101 | } 102 | } -------------------------------------------------------------------------------- /app/src/main/java/moe/lyniko/hiderecent/ui/HomeView.kt: -------------------------------------------------------------------------------- 1 | package moe.lyniko.hiderecent.ui 2 | 3 | import moe.lyniko.hiderecent.BuildConfig 4 | import moe.lyniko.hiderecent.R 5 | import android.content.Context 6 | import android.content.ContextWrapper 7 | import android.os.Process.myUserHandle 8 | import android.util.Log 9 | import android.view.inputmethod.InputMethodManager 10 | import androidx.activity.ComponentActivity 11 | import androidx.compose.foundation.Image 12 | import androidx.compose.foundation.layout.Column 13 | import androidx.compose.foundation.layout.Row 14 | import androidx.compose.foundation.layout.Spacer 15 | import androidx.compose.foundation.layout.fillMaxWidth 16 | import androidx.compose.foundation.layout.padding 17 | import androidx.compose.foundation.layout.size 18 | import androidx.compose.foundation.lazy.LazyColumn 19 | import androidx.compose.foundation.text.KeyboardActions 20 | import androidx.compose.foundation.text.KeyboardOptions 21 | import androidx.compose.material.icons.Icons 22 | import androidx.compose.material.icons.filled.Clear 23 | import androidx.compose.material3.ExperimentalMaterial3Api 24 | import androidx.compose.material3.Icon 25 | import androidx.compose.material3.IconButton 26 | import androidx.compose.material3.MaterialTheme 27 | import androidx.compose.material3.Scaffold 28 | import androidx.compose.material3.SnackbarDuration 29 | import androidx.compose.material3.SnackbarHost 30 | import androidx.compose.material3.SnackbarHostState 31 | import androidx.compose.material3.Surface 32 | import androidx.compose.material3.Switch 33 | import androidx.compose.material3.Text 34 | import androidx.compose.material3.TextField 35 | import androidx.compose.material3.TextFieldDefaults 36 | import androidx.compose.material3.TopAppBar 37 | import androidx.compose.runtime.Composable 38 | import androidx.compose.runtime.LaunchedEffect 39 | import androidx.compose.runtime.MutableState 40 | import androidx.compose.runtime.getValue 41 | import androidx.compose.runtime.mutableStateOf 42 | import androidx.compose.runtime.remember 43 | import androidx.compose.runtime.rememberCoroutineScope 44 | import androidx.compose.runtime.setValue 45 | import androidx.compose.ui.Modifier 46 | import androidx.compose.ui.graphics.Color 47 | import androidx.compose.ui.graphics.vector.ImageVector 48 | import androidx.compose.ui.platform.LocalContext 49 | import androidx.compose.ui.res.vectorResource 50 | import androidx.compose.ui.text.input.ImeAction 51 | import androidx.compose.ui.unit.dp 52 | import com.google.accompanist.drawablepainter.rememberDrawablePainter 53 | import kotlinx.coroutines.launch 54 | import moe.lyniko.hiderecent.ui.theme.MyApplicationTheme 55 | import moe.lyniko.hiderecent.utils.AppUtils 56 | import moe.lyniko.hiderecent.utils.ParsedPackage 57 | import moe.lyniko.hiderecent.utils.PreferenceUtils 58 | import moe.lyniko.hiderecent.utils.PreferenceUtils.Companion.ConfigKeys 59 | import moe.lyniko.hiderecent.utils.getIdByUserHandle 60 | 61 | fun Context.getActivity(): ComponentActivity? = when (this) { 62 | is ComponentActivity -> this 63 | is ContextWrapper -> baseContext.getActivity() 64 | else -> null 65 | } 66 | 67 | private lateinit var appUtils: AppUtils 68 | private lateinit var preferenceUtils: PreferenceUtils 69 | private var searchContent: MutableState = mutableStateOf("") 70 | private var showUserAppInsteadOfSystem: MutableState = mutableStateOf(true) 71 | private var snackbarHostState = SnackbarHostState() 72 | 73 | @OptIn(ExperimentalMaterial3Api::class) 74 | @Composable 75 | fun HomeView() { 76 | val context = LocalContext.current 77 | appUtils = AppUtils.getInstance(context) 78 | preferenceUtils = PreferenceUtils.getInstance(context) 79 | MyApplicationTheme { 80 | var searchContentRemember by remember { searchContent } 81 | var showUserAppInsteadOfSystemRemember by remember { showUserAppInsteadOfSystem } 82 | val scope = rememberCoroutineScope() 83 | val snackbarHostStateRemember = remember { snackbarHostState } 84 | 85 | Scaffold( 86 | snackbarHost = { 87 | SnackbarHost(hostState = snackbarHostStateRemember) 88 | }, 89 | topBar = { 90 | TopAppBar( 91 | title = { 92 | TextField( 93 | value = searchContentRemember, 94 | onValueChange = { searchContentRemember = it }, 95 | placeholder = { Text(context.getString(R.string.search_text)) }, 96 | singleLine = true, 97 | // action done 98 | keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done), 99 | keyboardActions = KeyboardActions( 100 | onDone = { 101 | context.getActivity()?.currentFocus?.let { view -> 102 | val imm = 103 | context.getSystemService(Context.INPUT_METHOD_SERVICE) as? InputMethodManager 104 | imm?.hideSoftInputFromWindow(view.windowToken, 0) 105 | } 106 | } 107 | ), 108 | // disable background 109 | colors = TextFieldDefaults.colors( 110 | unfocusedContainerColor = Color.Transparent, 111 | focusedContainerColor = Color.Transparent, 112 | unfocusedIndicatorColor = Color.Transparent, 113 | focusedIndicatorColor = Color.Transparent 114 | ), 115 | modifier = Modifier.fillMaxWidth() 116 | ) 117 | }, 118 | actions = { 119 | if (searchContentRemember.isNotEmpty()) { 120 | IconButton(onClick = { 121 | // clear search 122 | searchContentRemember = "" 123 | }) { 124 | Icon( 125 | imageVector = Icons.Filled.Clear, 126 | contentDescription = context.getString(R.string.clear_search) 127 | ) 128 | } 129 | } 130 | IconButton(onClick = { 131 | showUserAppInsteadOfSystemRemember = 132 | !showUserAppInsteadOfSystem.value 133 | }) { 134 | Icon( 135 | imageVector = ImageVector.vectorResource(R.drawable.baseline_swap_horiz_24), 136 | contentDescription = context.getString(R.string.switch_app_type) 137 | ) 138 | } 139 | }, 140 | ) 141 | }, 142 | ) { innerPadding -> 143 | AppListForPackages(getDisplayApps(), modifier = Modifier.padding(innerPadding)) 144 | } 145 | 146 | // check main user 147 | val userHandle = myUserHandle() 148 | var userId = 0 149 | try { 150 | userId = getIdByUserHandle(userHandle) 151 | } catch (e: Exception) { 152 | Log.e(BuildConfig.APPLICATION_ID, "Error when getting user id: ${e.message}") 153 | } 154 | if (userId != 0) { 155 | LaunchedEffect(snackbarHostState) { 156 | scope.launch { 157 | snackbarHostState.showSnackbar( 158 | context.getString(R.string.main_user_only, userId), 159 | actionLabel = context.getString(R.string.dismiss_notification), 160 | duration = SnackbarDuration.Indefinite 161 | ) 162 | } 163 | } 164 | Log.w( 165 | BuildConfig.APPLICATION_ID, 166 | "Wrong user id: ${getIdByUserHandle(userHandle)}" 167 | ) 168 | } 169 | if (appUtils.parsedApps.isEmpty()) { 170 | LaunchedEffect(snackbarHostState) { 171 | scope.launch { 172 | snackbarHostState.showSnackbar( 173 | context.getString(R.string.apps_not_fetched), 174 | actionLabel = context.getString(R.string.dismiss_notification), 175 | duration = SnackbarDuration.Indefinite 176 | ) 177 | } 178 | } 179 | } else if (!appUtils.isActivitiesFetched() && preferenceUtils.managerPref.getBoolean( 180 | ConfigKeys.HideNoActivityPackages.key, ConfigKeys.HideNoActivityPackages.default 181 | ) 182 | ) { 183 | LaunchedEffect(snackbarHostState) { 184 | scope.launch { 185 | snackbarHostState.showSnackbar( 186 | context.getString(R.string.activity_not_fetched), 187 | actionLabel = context.getString(R.string.dismiss_notification), 188 | duration = SnackbarDuration.Indefinite 189 | ) 190 | } 191 | } 192 | } 193 | } 194 | } 195 | 196 | private fun getDisplayApps(): List { 197 | var appsFiltered = 198 | appUtils.parsedApps.filter { showUserAppInsteadOfSystem.value xor it.isSystemApp } 199 | val trimmedLowerCasedSearch = searchContent.value.trim().lowercase() 200 | appsFiltered = if (trimmedLowerCasedSearch.length <= 1) { 201 | appsFiltered 202 | } else appsFiltered.filter { it.isLowerCasedSearchMatch(trimmedLowerCasedSearch) } 203 | // sort by app name and package name 204 | appsFiltered = appsFiltered.sortedWith(compareByDescending { 205 | preferenceUtils.isPackageInList(it.packageName) 206 | }.thenBy { it.appName }.thenBy { it.packageName }) 207 | // appsFiltered = appsFiltered.sortedBy { it.packageName }.sortedBy { it.appName }.sortedBy{ !preferenceUtils.isPackageInList(it.packageName) } 208 | return appsFiltered 209 | } 210 | 211 | // https://jetpackcompose.cn/docs/tutorial/ 212 | @Composable 213 | private fun SingleAppCardForPackage(app: ParsedPackage) { 214 | Row( 215 | modifier = Modifier.padding(all = 8.dp) // 在我们的 Card 周围添加 padding 216 | ) { 217 | Image( 218 | //image is app icon 219 | painter = rememberDrawablePainter(app.appIcon), 220 | contentDescription = null, 221 | modifier = Modifier 222 | .size(50.dp) // 改变 Image 元素的大小 223 | ) 224 | Spacer(Modifier.padding(horizontal = 8.dp)) // 添加一个空的控件用来填充水平间距,设置 padding 为 8.dp 225 | Column( 226 | modifier = Modifier 227 | .weight(1f) // 设置 Column 的 weight 为 1,使其占据剩余空间 228 | ) { 229 | Text( 230 | text = app.appName, 231 | color = MaterialTheme.colorScheme.secondary, 232 | style = MaterialTheme.typography.titleMedium 233 | ) 234 | Spacer(Modifier.padding(vertical = 4.dp)) 235 | Text( 236 | text = app.packageName, 237 | style = MaterialTheme.typography.bodyMedium 238 | ) 239 | } 240 | Column { 241 | var checked by remember { mutableStateOf(preferenceUtils.isPackageInList(app.packageName)) } 242 | Switch(checked = checked, onCheckedChange = { 243 | if (it) { 244 | preferenceUtils.addPackage(app.packageName) 245 | } else { 246 | preferenceUtils.removePackage(app.packageName) 247 | } 248 | // update checked state 249 | checked = it 250 | }) 251 | } 252 | } 253 | } 254 | 255 | @Composable 256 | private fun AppListForPackages(apps: List, modifier: Modifier = Modifier) { 257 | Surface( 258 | shape = MaterialTheme.shapes.medium, // 使用 MaterialTheme 自带的形状 259 | modifier = modifier 260 | .fillMaxWidth(), 261 | ) { 262 | LazyColumn { 263 | items( 264 | count = apps.size, 265 | key = { app_index -> apps[app_index].packageName } 266 | ) { app_index -> 267 | SingleAppCardForPackage(apps[app_index]) 268 | } 269 | } 270 | } 271 | } -------------------------------------------------------------------------------- /app/src/main/java/moe/lyniko/hiderecent/ui/SettingsView.kt: -------------------------------------------------------------------------------- 1 | package moe.lyniko.hiderecent.ui 2 | 3 | import android.content.ClipData 4 | import android.content.ClipboardManager 5 | import android.content.ComponentName 6 | import android.content.Context 7 | import android.content.pm.PackageManager 8 | import android.os.Build 9 | import android.widget.Toast 10 | import androidx.compose.foundation.layout.fillMaxSize 11 | import androidx.compose.foundation.lazy.LazyColumn 12 | import androidx.compose.material3.Text 13 | import androidx.compose.runtime.Composable 14 | import androidx.compose.runtime.mutableStateOf 15 | import androidx.compose.runtime.remember 16 | import androidx.compose.ui.Modifier 17 | import androidx.compose.ui.platform.LocalContext 18 | import me.zhanghai.compose.preference.ProvidePreferenceLocals 19 | import me.zhanghai.compose.preference.getPreferenceFlow 20 | import me.zhanghai.compose.preference.SwitchPreference 21 | import moe.lyniko.hiderecent.BuildConfig 22 | import moe.lyniko.hiderecent.R 23 | import moe.lyniko.hiderecent.utils.PreferenceUtils 24 | import moe.lyniko.hiderecent.utils.PreferenceUtils.Companion.ConfigKeys 25 | import moe.lyniko.hiderecent.utils.isShizukuAvailable 26 | import androidx.compose.runtime.getValue 27 | import androidx.compose.runtime.setValue 28 | import me.zhanghai.compose.preference.Preference 29 | import me.zhanghai.compose.preference.switchPreference 30 | 31 | 32 | @Composable 33 | fun SettingsView() { 34 | val context = LocalContext.current 35 | val managerPref = PreferenceUtils.getInstance(context).managerPref 36 | ProvidePreferenceLocals( 37 | flow = managerPref.getPreferenceFlow() 38 | ) { 39 | LazyColumn(modifier = Modifier.fillMaxSize()) { 40 | item { 41 | var switchMutableState by remember { 42 | mutableStateOf( 43 | managerPref.getBoolean( 44 | ConfigKeys.ShowPackageForAllUser.key, 45 | ConfigKeys.ShowPackageForAllUser.default 46 | ) 47 | ) 48 | } 49 | SwitchPreference( 50 | value = switchMutableState, 51 | onValueChange = { 52 | switchMutableState = it 53 | managerPref.edit() 54 | .putBoolean(ConfigKeys.ShowPackageForAllUser.key, it) 55 | .apply() 56 | if(it) isShizukuAvailable() 57 | }, 58 | title = { Text(context.getString(R.string.show_package_for_all_user)) }, 59 | summary = { Text(context.getString(R.string.show_package_for_all_user_summary)) }, 60 | ) 61 | } 62 | switchPreference( 63 | key=ConfigKeys.HideNoActivityPackages.key, 64 | defaultValue = ConfigKeys.HideNoActivityPackages.default, 65 | title = { Text(context.getString(R.string.hide_no_activity_packages)) }, 66 | summary = { Text(context.getString(R.string.hide_no_activity_packages_summary)) }, 67 | ) 68 | item { 69 | Preference( 70 | title = { Text(context.getString(R.string.export_config)) }, 71 | onClick = { 72 | val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager 73 | val clip = ClipData.newPlainText("config", PreferenceUtils.getInstance(context).packagesToString()) 74 | try { 75 | clipboard.setPrimaryClip(clip) 76 | if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.S_V2) 77 | Toast.makeText(context, context.getString(R.string.export_config_to_clipboard_success), Toast.LENGTH_SHORT).show() 78 | } 79 | catch (e: SecurityException) { 80 | Toast.makeText(context, context.getString(R.string.export_config_to_clipboard_failed), Toast.LENGTH_SHORT).show() 81 | e.printStackTrace() 82 | } 83 | } 84 | ) 85 | } 86 | item { 87 | Preference( 88 | title = { Text(context.getString(R.string.import_config)) }, 89 | onClick = { 90 | val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager 91 | 92 | val clipboardData: CharSequence? 93 | try { 94 | clipboardData = 95 | clipboard.primaryClip?.getItemAt(0)?.text 96 | } 97 | catch(e: SecurityException){ 98 | Toast.makeText(context, context.getString(R.string.import_config_failed_perm), Toast.LENGTH_SHORT).show() 99 | return@Preference 100 | } 101 | if(clipboardData.isNullOrEmpty()){ 102 | Toast.makeText(context, context.getString(R.string.import_config_failed_empty), Toast.LENGTH_SHORT).show() 103 | return@Preference 104 | } 105 | try{ 106 | val changed = PreferenceUtils.getInstance(context).packagesFromString(clipboardData.toString()) 107 | Toast.makeText(context, context.resources.getQuantityString(R.plurals.import_config_success_count, changed, changed), Toast.LENGTH_SHORT).show() 108 | } 109 | catch (e: NotImplementedError){ 110 | Toast.makeText(context, context.getString(R.string.import_config_failed_wrong), Toast.LENGTH_SHORT).show() 111 | e.printStackTrace() 112 | } 113 | } 114 | ) 115 | } 116 | item { 117 | val appHomeComponent = ComponentName(BuildConfig.APPLICATION_ID, BuildConfig.APPLICATION_ID + ".LauncherActivity") 118 | var switchMutableState by remember { 119 | mutableStateOf( 120 | context.packageManager?.getComponentEnabledSetting( 121 | appHomeComponent 122 | ) == PackageManager.COMPONENT_ENABLED_STATE_DISABLED 123 | ) 124 | } 125 | SwitchPreference( 126 | value = switchMutableState, 127 | onValueChange = { 128 | switchMutableState = it 129 | context.packageManager?.setComponentEnabledSetting( 130 | appHomeComponent, 131 | if (it) PackageManager.COMPONENT_ENABLED_STATE_DISABLED else PackageManager.COMPONENT_ENABLED_STATE_ENABLED, 132 | PackageManager.DONT_KILL_APP 133 | ) 134 | }, 135 | title = { Text(context.getString(R.string.hide_icon_in_launcher)) }, 136 | ) 137 | } 138 | } 139 | } 140 | } -------------------------------------------------------------------------------- /app/src/main/java/moe/lyniko/hiderecent/ui/theme/Color.kt: -------------------------------------------------------------------------------- 1 | package moe.lyniko.hiderecent.ui.theme 2 | 3 | import androidx.compose.ui.graphics.Color 4 | 5 | val Purple80 = Color(0xFFD0BCFF) 6 | val PurpleGrey80 = Color(0xFFCCC2DC) 7 | val Pink80 = Color(0xFFEFB8C8) 8 | 9 | val Purple40 = Color(0xFF6650a4) 10 | val PurpleGrey40 = Color(0xFF625b71) 11 | val Pink40 = Color(0xFF7D5260) -------------------------------------------------------------------------------- /app/src/main/java/moe/lyniko/hiderecent/ui/theme/Theme.kt: -------------------------------------------------------------------------------- 1 | package moe.lyniko.hiderecent.ui.theme 2 | 3 | import android.app.Activity 4 | import android.os.Build 5 | import androidx.compose.foundation.isSystemInDarkTheme 6 | import androidx.compose.material3.MaterialTheme 7 | import androidx.compose.material3.darkColorScheme 8 | import androidx.compose.material3.dynamicDarkColorScheme 9 | import androidx.compose.material3.dynamicLightColorScheme 10 | import androidx.compose.material3.lightColorScheme 11 | import androidx.compose.runtime.Composable 12 | import androidx.compose.runtime.SideEffect 13 | import androidx.compose.ui.graphics.toArgb 14 | import androidx.compose.ui.platform.LocalContext 15 | import androidx.compose.ui.platform.LocalView 16 | import androidx.core.view.WindowCompat 17 | 18 | private val DarkColorScheme = darkColorScheme( 19 | primary = Purple80, 20 | secondary = PurpleGrey80, 21 | tertiary = Pink80 22 | ) 23 | 24 | private val LightColorScheme = lightColorScheme( 25 | primary = Purple40, 26 | secondary = PurpleGrey40, 27 | tertiary = Pink40 28 | 29 | /* Other default colors to override 30 | background = Color(0xFFFFFBFE), 31 | surface = Color(0xFFFFFBFE), 32 | onPrimary = Color.White, 33 | onSecondary = Color.White, 34 | onTertiary = Color.White, 35 | onBackground = Color(0xFF1C1B1F), 36 | onSurface = Color(0xFF1C1B1F), 37 | */ 38 | ) 39 | 40 | @Composable 41 | fun MyApplicationTheme( 42 | darkTheme: Boolean = isSystemInDarkTheme(), 43 | // Dynamic color is available on Android 12+ 44 | dynamicColor: Boolean = true, 45 | content: @Composable () -> Unit 46 | ) { 47 | val colorScheme = when { 48 | dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { 49 | val context = LocalContext.current 50 | if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) 51 | } 52 | 53 | darkTheme -> DarkColorScheme 54 | else -> LightColorScheme 55 | } 56 | val view = LocalView.current 57 | if (!view.isInEditMode) { 58 | SideEffect { 59 | val window = (view.context as Activity).window 60 | window.statusBarColor = colorScheme.primary.toArgb() 61 | WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = darkTheme 62 | } 63 | } 64 | 65 | MaterialTheme( 66 | colorScheme = colorScheme, 67 | typography = Typography, 68 | content = content 69 | ) 70 | } -------------------------------------------------------------------------------- /app/src/main/java/moe/lyniko/hiderecent/ui/theme/Type.kt: -------------------------------------------------------------------------------- 1 | package moe.lyniko.hiderecent.ui.theme 2 | 3 | import androidx.compose.material3.Typography 4 | import androidx.compose.ui.text.TextStyle 5 | import androidx.compose.ui.text.font.FontFamily 6 | import androidx.compose.ui.text.font.FontWeight 7 | import androidx.compose.ui.unit.sp 8 | 9 | // Set of Material typography styles to start with 10 | val Typography = Typography( 11 | bodyLarge = TextStyle( 12 | fontFamily = FontFamily.Default, 13 | fontWeight = FontWeight.Normal, 14 | fontSize = 16.sp, 15 | lineHeight = 24.sp, 16 | letterSpacing = 0.5.sp 17 | ) 18 | /* Other default text styles to override 19 | titleLarge = TextStyle( 20 | fontFamily = FontFamily.Default, 21 | fontWeight = FontWeight.Normal, 22 | fontSize = 22.sp, 23 | lineHeight = 28.sp, 24 | letterSpacing = 0.sp 25 | ), 26 | labelSmall = TextStyle( 27 | fontFamily = FontFamily.Default, 28 | fontWeight = FontWeight.Medium, 29 | fontSize = 11.sp, 30 | lineHeight = 16.sp, 31 | letterSpacing = 0.5.sp 32 | ) 33 | */ 34 | ) -------------------------------------------------------------------------------- /app/src/main/java/moe/lyniko/hiderecent/utils/AppUtils.kt: -------------------------------------------------------------------------------- 1 | package moe.lyniko.hiderecent.utils 2 | 3 | import android.annotation.SuppressLint 4 | import android.content.Context 5 | import android.content.pm.PackageInfo 6 | import android.content.pm.PackageManager 7 | import android.graphics.drawable.Drawable 8 | import android.os.Build 9 | import android.os.IBinder 10 | import org.lsposed.hiddenapibypass.HiddenApiBypass 11 | import rikka.shizuku.ShizukuBinderWrapper 12 | import rikka.shizuku.SystemServiceHelper 13 | import moe.lyniko.hiderecent.utils.PreferenceUtils.Companion.ConfigKeys 14 | 15 | class AppUtils( 16 | context: Context 17 | ) { 18 | companion object { 19 | 20 | @Volatile 21 | private var instance: AppUtils? = null 22 | 23 | fun getInstance(context: Context) = 24 | instance ?: synchronized(this) { 25 | instance ?: AppUtils(context).also { instance = it } 26 | } 27 | } 28 | 29 | // the package manager 30 | private val packageManager: PackageManager = context.packageManager 31 | private val baseGetInstalledPackagesFlags: Int = 32 | PackageManager.GET_ACTIVITIES or PackageManager.GET_META_DATA 33 | private var getInstalledPackagesFlags: Int = baseGetInstalledPackagesFlags 34 | private val preferenceUtils = PreferenceUtils.getInstance(context) 35 | 36 | private fun removeActivitiesFetch() { 37 | getInstalledPackagesFlags = 38 | baseGetInstalledPackagesFlags and (baseGetInstalledPackagesFlags xor PackageManager.GET_ACTIVITIES) 39 | } 40 | private val allApps: List by lazy { 41 | // catch random error caused by MIUI or something else 42 | if(!preferenceUtils.managerPref.getBoolean(ConfigKeys.HideNoActivityPackages.key, ConfigKeys.HideNoActivityPackages.default)){ 43 | removeActivitiesFetch() 44 | } 45 | try { 46 | allAppsNoCatch 47 | } catch (e: Exception) { 48 | e.printStackTrace() 49 | if(!isActivitiesFetched()){ 50 | // skip another try if already failed 51 | return@lazy listOf() 52 | } 53 | 54 | // remove GET_ACTIVITIES flag 55 | removeActivitiesFetch() 56 | try{ 57 | allAppsNoCatch 58 | } 59 | catch(e: Exception) { 60 | e.printStackTrace() 61 | listOf() 62 | } 63 | } 64 | } 65 | 66 | fun isActivitiesFetched(): Boolean { 67 | return (PackageManager.GET_ACTIVITIES and getInstalledPackagesFlags) != 0 68 | } 69 | 70 | // a list for all the apps, lazy init 71 | private val allAppsNoCatch: List by lazy { 72 | // get all the apps 73 | if ( 74 | preferenceUtils.managerPref.getBoolean( 75 | ConfigKeys.ShowPackageForAllUser.key, 76 | ConfigKeys.ShowPackageForAllUser.default 77 | ) && isShizukuAvailable() 78 | ) appForAllUser else appForSingleUser 79 | } 80 | 81 | private val appForSingleUser: List by lazy { 82 | packageManager.getInstalledPackages(getInstalledPackagesFlags) 83 | } 84 | 85 | private val appForAllUser: List by lazy { 86 | val users = getUserProfiles(context) 87 | val apps = ArrayList() 88 | for (user in users) { 89 | apps.addAll( 90 | getInstalledPackagesAsUser( 91 | getInstalledPackagesFlags, 92 | getIdByUserHandle(user) 93 | ) 94 | ) 95 | } 96 | apps 97 | } 98 | 99 | private fun atLeastT(): Boolean { 100 | return Build.VERSION.SDK_INT >= 33 101 | } 102 | 103 | @SuppressLint("PrivateApi") 104 | private fun getInstalledPackagesAsUser( 105 | @Suppress("SameParameterValue") flags: Int, 106 | userId: Int 107 | ): List { 108 | // fuck android. 109 | // https://www.xda-developers.com/implementing-shizuku/ 110 | // Previous version: https://github.com/Young-Lord/hideRecent/commit/8f956002e1edbb95e2e3e945c28ec1a716596347 111 | // val iPmClass = Class.forName("android.content.pm.IPackageManager") 112 | val iPmStub = Class.forName("android.content.pm.IPackageManager\$Stub") 113 | val asInterfaceMethod = iPmStub.getMethod("asInterface", IBinder::class.java) 114 | val iPmInstance = asInterfaceMethod.invoke( 115 | null, 116 | ShizukuBinderWrapper(SystemServiceHelper.getSystemService("package")) 117 | ) 118 | val iParceledListSliceClass = Class.forName("android.content.pm.ParceledListSlice") 119 | val retAsInner: Any 120 | if (atLeastT()) { 121 | retAsInner = HiddenApiBypass.invoke( 122 | iPmInstance::class.java, 123 | iPmInstance, 124 | "getInstalledPackages", 125 | flags.toLong(), 126 | userId 127 | ) 128 | } else { 129 | retAsInner = HiddenApiBypass.invoke( 130 | iPmInstance::class.java, 131 | iPmInstance, 132 | "getInstalledPackages", 133 | flags, 134 | userId 135 | ) 136 | } 137 | @Suppress("UNCHECKED_CAST") 138 | return HiddenApiBypass.invoke( 139 | iParceledListSliceClass, 140 | retAsInner, 141 | "getList" 142 | ) as List 143 | } 144 | 145 | private val appsFiltered: List by lazy { 146 | // get all the apps 147 | val result = ArrayList() 148 | allApps.forEach { 149 | if (result.find { pkg -> pkg.packageName == it.packageName } == null && (!isActivitiesFetched() || !it.activities.isNullOrEmpty())) { 150 | // filter for multi-user and filter those without activities 151 | result.add(it) 152 | } 153 | } 154 | result 155 | } 156 | 157 | val parsedApps: List by lazy { 158 | appsFiltered.map { ParsedPackage(it, packageManager) } 159 | } 160 | } 161 | 162 | class ParsedPackage( 163 | private val pkg: PackageInfo, 164 | private val packageManager: PackageManager 165 | ) { 166 | // lazy init 167 | val appName: String by lazy { 168 | packageManager.getApplicationLabel(pkg.applicationInfo).toString() 169 | } 170 | val appIcon: Drawable by lazy { 171 | pkg.applicationInfo.loadIcon(packageManager) 172 | } 173 | val packageName: String by lazy { 174 | pkg.packageName 175 | } 176 | 177 | @Suppress("unused") 178 | val versionName: String by lazy { 179 | pkg.versionName 180 | } 181 | 182 | @Suppress("unused") 183 | val versionCode: Long by lazy { 184 | pkg.longVersionCode 185 | } 186 | val isSystemApp: Boolean by lazy { 187 | (pkg.applicationInfo.flags and android.content.pm.ApplicationInfo.FLAG_SYSTEM) != 0 188 | } 189 | private val packageNameLowerCase: String by lazy { 190 | packageName.lowercase() 191 | } 192 | private val appNameLowerCase: String by lazy { 193 | appName.lowercase() 194 | } 195 | 196 | fun isLowerCasedSearchMatch(searchContent: String): Boolean { 197 | return packageNameLowerCase.contains(searchContent) || appNameLowerCase.contains( 198 | searchContent 199 | ) 200 | } 201 | } -------------------------------------------------------------------------------- /app/src/main/java/moe/lyniko/hiderecent/utils/MultiUserUtils.kt: -------------------------------------------------------------------------------- 1 | package moe.lyniko.hiderecent.utils 2 | 3 | import android.content.Context 4 | import android.os.UserHandle 5 | import android.os.UserManager 6 | import org.lsposed.hiddenapibypass.HiddenApiBypass 7 | 8 | fun getIdByUserHandle(userHandle: UserHandle): Int { 9 | return HiddenApiBypass.invoke(UserHandle::class.java, userHandle, "getIdentifier"/*, args*/) as Int 10 | } 11 | 12 | fun getUserProfiles(context: Context): List { 13 | // https://stackoverflow.com/questions/14749504/android-usermanager-check-if-user-is-owner-admin 14 | val userManager = context.getSystemService(Context.USER_SERVICE) as UserManager 15 | return userManager.userProfiles 16 | } 17 | -------------------------------------------------------------------------------- /app/src/main/java/moe/lyniko/hiderecent/utils/PreferenceUtils.kt: -------------------------------------------------------------------------------- 1 | package moe.lyniko.hiderecent.utils 2 | 3 | import android.annotation.SuppressLint 4 | import android.content.Context 5 | import android.content.SharedPreferences 6 | import moe.lyniko.hiderecent.MyApplication 7 | import moe.lyniko.hiderecent.R 8 | 9 | @SuppressLint("WorldReadableFiles") 10 | class PreferenceUtils( // init context on constructor 11 | context: Context 12 | ) { 13 | // ------ 1. get several SharedPreferences (funcPref is the only accessible during Xposed inject) ------ 14 | private var funcPref: SharedPreferences = try { 15 | @Suppress("DEPRECATION") 16 | context.getSharedPreferences(functionalConfigName, Context.MODE_WORLD_READABLE) 17 | } catch (e: SecurityException) { 18 | throw e 19 | // Log.w("PreferenceUtil", "Fallback to Private SharedPref for error!!!: ${e.message}") 20 | // context.getSharedPreferences(functionalConfigName, Context.MODE_PRIVATE) 21 | } 22 | 23 | @Suppress("DEPRECATION") 24 | var managerPref: SharedPreferences = 25 | context.getSharedPreferences(managerConfigName, Context.MODE_WORLD_READABLE) 26 | 27 | @Suppress("DEPRECATION") 28 | private val legacyFuncPref = context.getSharedPreferences(legacyConfigName, Context.MODE_WORLD_READABLE) 29 | 30 | // ------ 2. init packages ------ 31 | private fun initPackageFromLegacyAndNew(funcPref: SharedPreferences, legacyPref: SharedPreferences) { 32 | val legacyPackages = legacyPref.getString(legacyModeStringMode, "")?.removeSurrounding("#")?.split("##")?.toMutableSet() 33 | val newPackages = getPackageListFromPref(funcPref) 34 | if(newPackages.isEmpty() && !legacyPackages.isNullOrEmpty()) { 35 | // remove legacy data only if only legacy one has data. 36 | // Log.d("PreferenceUtil", "initPackageFromLegacyAndNew: $legacyPackages") 37 | legacyPref.edit().remove(legacyModeStringMode).apply() 38 | packages = legacyPackages 39 | commitPackageList() 40 | } else { 41 | packages = newPackages 42 | } 43 | packages.remove("") // have no idea why this occurs. 44 | } 45 | 46 | private lateinit var packages: MutableSet 47 | 48 | init { 49 | initPackageFromLegacyAndNew(funcPref, legacyFuncPref) 50 | } 51 | companion object { 52 | 53 | @Volatile 54 | private var instance: PreferenceUtils? = null 55 | 56 | fun getInstance(context: Context) = 57 | instance ?: synchronized(this) { 58 | instance ?: PreferenceUtils(context).also { instance = it } 59 | } 60 | 61 | private const val packagesTag = "packages" 62 | const val functionalConfigName = "functional_config" 63 | const val managerConfigName = "manager_config" 64 | private const val legacyConfigName = "config" 65 | private const val legacyModeStringMode = "Mode" 66 | 67 | enum class ConfigKeys(val key: String, val default: Boolean) { 68 | ShowPackageForAllUser("show_package_for_all_user", false), 69 | HideNoActivityPackages("hide_no_activity_packages", true) 70 | } 71 | 72 | fun getPackageListFromPref(pref: SharedPreferences): MutableSet { 73 | val currentPackageSet = pref.getStringSet(packagesTag, HashSet()) 74 | return currentPackageSet!!.toMutableSet() 75 | } 76 | } 77 | 78 | 79 | private fun commitPackageList() { 80 | funcPref.edit().putStringSet(packagesTag, packages).apply() 81 | } 82 | 83 | fun addPackage(pkg: String): Int { 84 | if(pkg.isEmpty() || pkg == "*") return 0 85 | val ret = if (packages.add(pkg)) 1 else 0 86 | // Log.w("PreferenceUtil", "addPackage: $pkg -> $ret") 87 | commitPackageList() 88 | return ret 89 | } 90 | 91 | fun removePackage(pkg: String): Int { 92 | val ret: Int 93 | if (pkg == "*") { 94 | ret = packages.size 95 | packages.clear() 96 | } else { 97 | ret = if (packages.remove(pkg)) 1 else 0 98 | } 99 | // Log.w("PreferenceUtil", "removePackage: $pkg -> $ret") 100 | commitPackageList() 101 | return ret 102 | } 103 | 104 | fun isPackageInList(pkg: String): Boolean { 105 | // Log.d("PreferenceUtil", "isPackageInList: $pkg -> ${packages.contains(pkg)}") 106 | return packages.contains(pkg) 107 | } 108 | 109 | fun packagesToString(version: Int = 1): String { 110 | when (version) { 111 | 1 -> { 112 | var result = 113 | "# version=$version\n# -* # ${MyApplication.resourcesPublic.getString(R.string.export_uncomment_hint)}\n" 114 | packages.forEach { pkg -> 115 | result += "+$pkg\n" 116 | } 117 | if (packages.isEmpty()){ 118 | result += "# +com.example.package # ${MyApplication.resourcesPublic.getString(R.string.export_demo_hint)}\n" 119 | } 120 | return result 121 | } 122 | else -> throw NotImplementedError("Version $version is not implemented") 123 | } 124 | } 125 | private fun validatePackageNameOrAsterisk(pkg: String): Boolean { 126 | if (pkg == "*") return true 127 | // https://stackoverflow.com/a/40772073 128 | @Suppress("RegExpSimplifiable") 129 | return pkg.matches(Regex("^([A-Za-z]{1}[A-Za-z\\d_]*\\.)+[A-Za-z][A-Za-z\\d_]*\$")) 130 | } 131 | 132 | fun packagesFromString(str: String): Int { 133 | val lines = str.split("\n") 134 | // read first line for version 135 | val version: Int 136 | var changed = 0 137 | try { 138 | version = lines[0].split("=")[1].toInt() 139 | } catch (e: Exception) { 140 | e.printStackTrace() 141 | throw NotImplementedError("Version is not specified") 142 | } 143 | when (version) { 144 | 1 -> { 145 | lines.forEach { line -> 146 | // remove comments start with # 147 | val lineWithoutComment = line.split("#")[0].trim() 148 | // skip if empty 149 | if (lineWithoutComment.isEmpty()) return@forEach 150 | // get action & package name 151 | val action = lineWithoutComment[0] 152 | val currentPackage = lineWithoutComment.substring(1) 153 | if (currentPackage.isEmpty()) return@forEach 154 | if (!validatePackageNameOrAsterisk(currentPackage)) throw NotImplementedError("Invalid package name: $currentPackage") 155 | @Suppress("LiftReturnOrAssignment") 156 | when (action) { 157 | '+' -> { 158 | changed += addPackage(currentPackage) 159 | } 160 | '-' -> { 161 | changed += removePackage(currentPackage) 162 | } 163 | else -> { 164 | throw NotImplementedError("Action $action is not implemented") 165 | } 166 | } 167 | } 168 | } 169 | 170 | else -> throw NotImplementedError("Version $version is not implemented") 171 | } 172 | return changed 173 | } 174 | } -------------------------------------------------------------------------------- /app/src/main/java/moe/lyniko/hiderecent/utils/ShizukuUtils.kt: -------------------------------------------------------------------------------- 1 | package moe.lyniko.hiderecent.utils 2 | 3 | import android.content.Context 4 | import android.content.pm.PackageManager 5 | import rikka.shizuku.Shizuku 6 | import moe.lyniko.hiderecent.utils.PreferenceUtils.Companion.ConfigKeys 7 | 8 | 9 | 10 | fun isShizukuAvailable(): Boolean { 11 | try { 12 | Shizuku.pingBinder() 13 | if (Shizuku.isPreV11()) { 14 | return false 15 | } 16 | Shizuku.checkSelfPermission() 17 | } catch (e: IllegalStateException) { 18 | return false 19 | } 20 | return if (Shizuku.checkSelfPermission() == PackageManager.PERMISSION_GRANTED) { 21 | true 22 | } else if (Shizuku.shouldShowRequestPermissionRationale()) { 23 | // Users choose "Deny and don't ask again" 24 | false 25 | } else { 26 | // Request the permission 27 | try { 28 | Shizuku.requestPermission((Int.MIN_VALUE..Int.MAX_VALUE).random()) 29 | } catch (e: IllegalStateException) { 30 | // Shizuku not installed 31 | @Suppress("UNUSED_EXPRESSION") 32 | false 33 | } 34 | false 35 | } 36 | } 37 | 38 | fun isShizukuNeeded(context: Context): Boolean { 39 | return PreferenceUtils.getInstance(context).managerPref.getBoolean( 40 | ConfigKeys.ShowPackageForAllUser.key, 41 | ConfigKeys.ShowPackageForAllUser.default, 42 | ) 43 | } -------------------------------------------------------------------------------- /app/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 15 | 18 | 21 | 22 | 23 | 24 | 30 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/baseline_swap_horiz_24.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 15 | 20 | 25 | 30 | 35 | 40 | 45 | 50 | 55 | 60 | 65 | 70 | 75 | 80 | 85 | 90 | 95 | 100 | 105 | 110 | 115 | 120 | 125 | 130 | 135 | 140 | 145 | 150 | 155 | 160 | 165 | 170 | 171 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Young-Lord/hideRecent/00eb603836b09be92132476bb19a6ae9a06034b8/app/src/main/res/mipmap-hdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Young-Lord/hideRecent/00eb603836b09be92132476bb19a6ae9a06034b8/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Young-Lord/hideRecent/00eb603836b09be92132476bb19a6ae9a06034b8/app/src/main/res/mipmap-mdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Young-Lord/hideRecent/00eb603836b09be92132476bb19a6ae9a06034b8/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Young-Lord/hideRecent/00eb603836b09be92132476bb19a6ae9a06034b8/app/src/main/res/mipmap-xhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Young-Lord/hideRecent/00eb603836b09be92132476bb19a6ae9a06034b8/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Young-Lord/hideRecent/00eb603836b09be92132476bb19a6ae9a06034b8/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Young-Lord/hideRecent/00eb603836b09be92132476bb19a6ae9a06034b8/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Young-Lord/hideRecent/00eb603836b09be92132476bb19a6ae9a06034b8/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Young-Lord/hideRecent/00eb603836b09be92132476bb19a6ae9a06034b8/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/values-zh-rCN/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 隐藏最近任务 4 | 从最近任务列表中隐藏特定任务 5 | 系统应用 6 | 用户应用 7 | 搜索… 8 | 在显示 用户应用/系统应用 间切换。 9 | 请先在 LSPosed 中激活并强制停止本模块。 10 | 主页 11 | 设置 12 | 关于 13 | 清除搜索框 14 | 此模块仅能在主用户(用户0)中使用。当前用户为$1%d。 15 | Shizuku / Sui 未运行,仅显示当前用户的应用。 16 | 基于 Activity 的应用列表过滤器未激活,因此显示了不出现在最近应用列表中的应用。这可能是你的系统的 bug。 17 | 获取应用列表失败。你仍然可以通过在设置页面导入或导出包名来编辑设置。 18 | 显示来自所有用户的应用 19 | 需要 Shizuku 或 Sui。重启应用生效。 20 | 基于 Activity 信息过滤应用 21 | 从本模块的应用列表中隐藏不会显示在“最近任务列表”中的应用。重启应用生效。 22 | 从启动器(桌面)隐藏此模块 23 | 忽略 24 | 导入配置至剪贴板 25 | 导出配置至剪贴板 26 | 配置已导出至剪贴板。 27 | 无法导出配置至剪贴板。请检查你是否给予了对应权限。 28 | 导入配置失败。(剪贴板为空) 29 | 导入配置失败。请检查你是否给予了对应权限。 30 | 导入配置失败。配置内容格式有误。 31 | 导入配置成功。 32 | 导入配置前,删掉此行的首个`# `以重置已有配置 33 | 本行为配置格式的示例,删除`# `表示为`com.example.package`启用隐藏 34 | 35 | 成功更改 %1$d 条配置。 36 | 37 | 38 | -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #FFBB86FC 4 | #FF6200EE 5 | #FF3700B3 6 | #FF03DAC5 7 | #FF018786 8 | #FF000000 9 | #FFFFFFFF 10 | -------------------------------------------------------------------------------- /app/src/main/res/values/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 16dp 4 | 16dp 5 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | https://github.com/Young-Lord/hideRecent 3 | Hide Recent Task 4 | Hide specific apps from recent task list 5 | SystemApp 6 | UserApp 7 | Search… 8 | Switch display of user / system apps. 9 | Please activate this module in LSPosed Manager, then force stop. 10 | 11 | android 12 | 13 | Home 14 | Settings 15 | About 16 | Clear search box 17 | Shizuku / Sui not working, only current user\'s packages are displayed. 18 | This module should be used in main user (user 0) only. Current user is %1$d. 19 | Activity filter is not running properly, so non-applicable apps are shown. This may be a bug of your ROM. 20 | Fetching application list failed. You can still edit settings by importing and exporting package names. 21 | Show applications of all users. 22 | Require Shizuku / Sui to work. Restart module after changing this. 23 | Exclude applications by Activity 24 | Hide applications not displayed in Recent Tasks from module\'s application list. Restart module after changing this. 25 | Hide module in launcher 26 | DISMISS 27 | Import Config from Clipboard 28 | Export Config to Clipboard 29 | Config exported to clipboard. 30 | Failed to export config to clipboard. Please check your permission. 31 | Failed. Clipboard is empty. 32 | Failed. Please check your permission. 33 | Failed. Wrong config format. 34 | Config imported. 35 | uncomment this line to reset before importing 36 | this line is a demo for config format, remove `# ` means enabling for `com.example.package` 37 | 38 | %1$d config entry changed. 39 | %1$d config entries changed. 40 | 41 | -------------------------------------------------------------------------------- /app/src/main/res/values/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 |