├── .gitignore ├── LICENSE ├── README.md ├── app ├── .gitignore ├── appcenter-pre-build.sh ├── build.gradle ├── build.gradle.ci ├── libs │ └── android-30.jar ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── cn │ │ └── geektang │ │ └── privacyspace │ │ └── ExampleInstrumentedTest.kt │ ├── main │ ├── AndroidManifest.xml │ ├── assets │ │ └── xposed_init │ ├── java │ │ └── cn │ │ │ └── geektang │ │ │ └── privacyspace │ │ │ ├── App.kt │ │ │ ├── bean │ │ │ ├── AppInfo.kt │ │ │ ├── ConfigData.kt │ │ │ └── SystemUserInfo.kt │ │ │ ├── constant │ │ │ ├── ConfigConstant.kt │ │ │ ├── RouteConstant.kt │ │ │ └── UiSettings.kt │ │ │ ├── hook │ │ │ ├── HookMain.kt │ │ │ ├── Hooker.kt │ │ │ └── impl │ │ │ │ ├── FrameworkHookerApi26Impl.kt │ │ │ │ ├── FrameworkHookerApi28Impl.kt │ │ │ │ ├── FrameworkHookerApi30Impl.kt │ │ │ │ ├── HookChecker.kt │ │ │ │ ├── SettingsAppHookImpl.kt │ │ │ │ └── SpecialAppsHookerImpl.kt │ │ │ ├── ui │ │ │ ├── main │ │ │ │ └── MainActivity.kt │ │ │ ├── screen │ │ │ │ ├── about │ │ │ │ │ └── AboutScreen.kt │ │ │ │ ├── blind │ │ │ │ │ ├── AddBlindAppsScreen.kt │ │ │ │ │ └── AddBlindAppsViewModel.kt │ │ │ │ ├── launcher │ │ │ │ │ ├── LauncherScreen.kt │ │ │ │ │ └── LauncherViewModel.kt │ │ │ │ ├── managehiddenapps │ │ │ │ │ ├── AddHiddenAppsScreen.kt │ │ │ │ │ └── AddHiddenAppsViewModel.kt │ │ │ │ ├── setconnectedapps │ │ │ │ │ ├── SetConnectedAppsScreen.kt │ │ │ │ │ └── SetConnectedAppsViewModel.kt │ │ │ │ └── setwhitelist │ │ │ │ │ ├── SetWhitelistScreen.kt │ │ │ │ │ └── SetWhitelistViewModel.kt │ │ │ ├── theme │ │ │ │ ├── Color.kt │ │ │ │ ├── Shape.kt │ │ │ │ ├── Theme.kt │ │ │ │ └── Type.kt │ │ │ └── widget │ │ │ │ ├── AppInfoColumnItem.kt │ │ │ │ ├── Chip.kt │ │ │ │ ├── Dialog.kt │ │ │ │ ├── LoadingBox.kt │ │ │ │ ├── Popup.kt │ │ │ │ └── TopBar.kt │ │ │ └── util │ │ │ ├── AppHelper.kt │ │ │ ├── ClassLoader.kt │ │ │ ├── ConfigClient.kt │ │ │ ├── ConfigHelper.kt │ │ │ ├── ConfigServer.kt │ │ │ ├── Context.kt │ │ │ ├── EqualsHelper.kt │ │ │ ├── HookUtil.kt │ │ │ ├── JsonHelper.kt │ │ │ ├── NavHostControllerWrapper.kt │ │ │ ├── OnLifecycleEvent.kt │ │ │ ├── Su.kt │ │ │ └── XLog.kt │ └── res │ │ ├── drawable-xxhdpi │ │ ├── ic_avatar.webp │ │ ├── ic_coolapk.webp │ │ └── ic_telegram.webp │ │ ├── drawable │ │ └── ic_github.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── values-zh-rCN │ │ └── strings.xml │ │ ├── values-zh-rTW │ │ └── strings.xml │ │ └── values │ │ ├── arrays.xml │ │ ├── colors.xml │ │ ├── strings.xml │ │ └── themes.xml │ └── test │ └── java │ └── cn │ └── geektang │ └── privacyspace │ └── ExampleUnitTest.kt ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/* 5 | .DS_Store 6 | /build 7 | /captures 8 | .externalNativeBuild 9 | .cxx 10 | local.properties 11 | *.jks 12 | google-services.json 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013-present, Rui Tang 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Privacy Space 2 | 3 | [![Stars](https://img.shields.io/github/stars/GeekTR/PrivacySpace?label=Stars)](https://github.com/GeekTR/PrivacySpace) 4 | [![Release](https://img.shields.io/github/v/release/Xposed-Modules-Repo/cn.geektang.privacyspace?label=Release)](https://github.com/Xposed-Modules-Repo/cn.geektang.privacyspace/releases/latest) 5 | [![Download](https://img.shields.io/github/downloads/Xposed-Modules-Repo/cn.geektang.privacyspace/total)](https://github.com/Xposed-Modules-Repo/cn.geektang.privacyspace/releases/latest) 6 | [![Channel](https://img.shields.io/badge/Follow-Telegram-blue.svg?logo=telegram)](https://t.me/PrivacySpaceAlpha) 7 | [![GitHub license](https://img.shields.io/github/license/Xposed-Modules-Repo/cn.geektang.privacyspace)](https://github.com/Xposed-Modules-Repo/cn.geektang.privacyspace/blob/main/LICENSE) 8 | 9 | [中文文档](https://github.com/Xposed-Modules-Repo/cn.geektang.privacyspace/blob/main/README_CN.md) 10 | 11 | This is an **Xposed** module. The function of this module is to "hide" the apps, which can achieve the "Second space" function of MIUI. 12 | 13 | ## What cool things can it do? 14 | 15 | 1. In addition to detecting Root, some banking apps will also detect Xposed modules. This module can hide our Xposed modules and pass the detection of banking apps; 16 | 17 | 2. A certain version of an app is particularly useful, and we don't want it to be automatically updated by the app store; 18 | 19 | 3. When we are watching an advertisement, some apps detect the existence of another app and will open it directly, but we do not want to open that app; 20 | 21 | 4. Why should we tell software vendors something as personal as which apps are installed? 22 | 23 | 5. More cool things are waiting for you to discover... 24 | 25 | ## Notice 26 | 27 | 1. If random package names are enabled in your Magisk app, whitelist it. Otherwise, the apps moved to the "Privacy Space" cannot obtain the Root permission correctly. 28 | 29 | 2. Apps that move to the "Privacy Space" can be launched by clicking the APP icon on the "Privacy Space" home page. 30 | 31 | 3. If you don't want to hide an app (such as the desktop app), you can add it to the whitelist. 32 | 33 | 4. If you use this module to hide some system apps, the system may fail to boot after the restart. Therefore, exercise caution when hiding system apps. 34 | 35 | 5. If this module causes your system to fail to boot, you can restart the system again after connecting to the computer and executing "adb uninstall cn.geektang.privacyspace" on the system load page (with USB debugging enabled). 36 | 37 | ## Todo 38 | 39 | They will coming soon in future. 40 | 41 | - [x] Add the search function on the app list page. 42 | 43 | - [x] Adapt to Android version 8-10. 44 | 45 | - [x] Fixed the bug that some mobile phones crashed directly without root permission. 46 | 47 | - [x] Remove the dependency on root permissions. 48 | 49 | - [ ] Install Xposed module to automatically hide (user optional). 50 | 51 | - [ ] When the Xposed module is hidden, the user can choose whether to add its recommended app as its "Connected App". 52 | 53 | ## Scope 54 | 55 | 1. System Framework (**required**) 56 | 57 | 2. Targeted hooks can be made when the non-Android system scope is checked (used when the hidden function cannot take effect after System Framework is checked) -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /app/appcenter-pre-build.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | /bin/cp build.gradle.ci build.gradle -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'com.android.application' 3 | id 'org.jetbrains.kotlin.android' 4 | id 'com.google.devtools.ksp' 5 | id 'kotlin-parcelize' 6 | } 7 | 8 | android { 9 | compileSdk 32 10 | 11 | defaultConfig { 12 | applicationId "cn.geektang.privacyspace" 13 | minSdk 26 14 | targetSdk 32 15 | versionCode 21 16 | versionName "1.3.7" 17 | 18 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 19 | vectorDrawables { 20 | useSupportLibrary true 21 | } 22 | } 23 | 24 | signingConfigs { 25 | release { 26 | keyAlias System.env.Space_keyAlias 27 | keyPassword System.env.Space_keyPassword 28 | storeFile file(System.env.Space_storeFile) 29 | storePassword System.env.Space_storePassword 30 | } 31 | } 32 | 33 | buildTypes { 34 | debug { 35 | buildConfigField('String', 'APP_CENTER_SECRET', "\"${System.env.Space_AppCenterSecret}\"") 36 | signingConfig signingConfigs.release 37 | } 38 | release { 39 | buildConfigField('String', 'APP_CENTER_SECRET', "\"${System.env.Space_AppCenterSecret}\"") 40 | minifyEnabled false 41 | signingConfig signingConfigs.release 42 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 43 | } 44 | } 45 | compileOptions { 46 | sourceCompatibility JavaVersion.VERSION_1_8 47 | targetCompatibility JavaVersion.VERSION_1_8 48 | } 49 | kotlinOptions { 50 | jvmTarget = '1.8' 51 | } 52 | buildFeatures { 53 | compose true 54 | } 55 | composeOptions { 56 | kotlinCompilerExtensionVersion compose_version 57 | } 58 | packagingOptions { 59 | resources { 60 | excludes += '/META-INF/**' 61 | excludes += 'okhttp3/**' 62 | excludes += 'kotlin/**' 63 | excludes += '**.bin' 64 | excludes += '**.properties' 65 | } 66 | } 67 | } 68 | 69 | dependencies { 70 | 71 | implementation 'androidx.core:core-ktx:1.7.0' 72 | implementation "androidx.compose.ui:ui:$compose_version" 73 | implementation "androidx.compose.material:material:$compose_version" 74 | implementation "androidx.compose.ui:ui-tooling-preview:$compose_version" 75 | implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.4.1' 76 | implementation 'androidx.activity:activity-compose:1.4.0' 77 | implementation 'io.coil-kt:coil-compose:1.4.0' 78 | testImplementation 'junit:junit:4.13.2' 79 | androidTestImplementation 'androidx.test.ext:junit:1.1.3' 80 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' 81 | androidTestImplementation "androidx.compose.ui:ui-test-junit4:$compose_version" 82 | debugImplementation "androidx.compose.ui:ui-tooling:$compose_version" 83 | compileOnly 'de.robv.android.xposed:api:82' 84 | compileOnly 'de.robv.android.xposed:api:82:sources' 85 | compileOnly files('libs/android-30.jar') 86 | implementation 'androidx.navigation:navigation-compose:2.4.2' 87 | implementation 'com.google.accompanist:accompanist-insets:0.23.1' 88 | implementation 'com.google.accompanist:accompanist-insets-ui:0.23.1' 89 | implementation "com.google.accompanist:accompanist-flowlayout:0.23.1" 90 | implementation 'com.squareup.moshi:moshi:1.13.0' 91 | ksp("com.squareup.moshi:moshi-kotlin-codegen:1.13.0") 92 | def appCenterSdkVersion = '4.4.2' 93 | implementation "com.microsoft.appcenter:appcenter-analytics:${appCenterSdkVersion}" 94 | implementation "com.microsoft.appcenter:appcenter-crashes:${appCenterSdkVersion}" 95 | compileOnly 'androidx.recyclerview:recyclerview:1.2.1' 96 | } -------------------------------------------------------------------------------- /app/build.gradle.ci: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'com.android.application' 3 | id 'org.jetbrains.kotlin.android' 4 | id 'com.google.devtools.ksp' 5 | } 6 | 7 | android { 8 | compileSdk 32 9 | 10 | defaultConfig { 11 | applicationId "cn.geektang.privacyspace" 12 | minSdk 26 13 | targetSdk 32 14 | versionCode 14 15 | versionName "1.3.1" 16 | 17 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 18 | vectorDrawables { 19 | useSupportLibrary true 20 | } 21 | } 22 | 23 | buildTypes { 24 | debug { 25 | buildConfigField('String', 'APP_CENTER_SECRET', "\"${System.env.Space_AppCenterSecret}\"") 26 | } 27 | release { 28 | buildConfigField('String', 'APP_CENTER_SECRET', "\"${System.env.Space_AppCenterSecret}\"") 29 | minifyEnabled false 30 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 31 | } 32 | } 33 | compileOptions { 34 | sourceCompatibility JavaVersion.VERSION_1_8 35 | targetCompatibility JavaVersion.VERSION_1_8 36 | } 37 | kotlinOptions { 38 | jvmTarget = '1.8' 39 | } 40 | buildFeatures { 41 | compose true 42 | } 43 | composeOptions { 44 | kotlinCompilerExtensionVersion compose_version 45 | } 46 | packagingOptions { 47 | resources { 48 | excludes += '/META-INF/**' 49 | excludes += 'okhttp3/**' 50 | excludes += 'kotlin/**' 51 | excludes += '**.bin' 52 | excludes += '**.properties' 53 | } 54 | } 55 | } 56 | 57 | dependencies { 58 | 59 | implementation 'androidx.core:core-ktx:1.7.0' 60 | implementation "androidx.compose.ui:ui:$compose_version" 61 | implementation "androidx.compose.material:material:$compose_version" 62 | implementation "androidx.compose.ui:ui-tooling-preview:$compose_version" 63 | implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.4.1' 64 | implementation 'androidx.activity:activity-compose:1.4.0' 65 | implementation 'io.coil-kt:coil-compose:1.4.0' 66 | testImplementation 'junit:junit:4.13.2' 67 | androidTestImplementation 'androidx.test.ext:junit:1.1.3' 68 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' 69 | androidTestImplementation "androidx.compose.ui:ui-test-junit4:$compose_version" 70 | debugImplementation "androidx.compose.ui:ui-tooling:$compose_version" 71 | compileOnly 'de.robv.android.xposed:api:82' 72 | compileOnly 'de.robv.android.xposed:api:82:sources' 73 | compileOnly files('libs/android-30.jar') 74 | implementation 'androidx.navigation:navigation-compose:2.4.1' 75 | implementation 'com.google.accompanist:accompanist-insets:0.23.0' 76 | implementation 'com.google.accompanist:accompanist-insets-ui:0.23.0' 77 | implementation 'com.squareup.moshi:moshi:1.13.0' 78 | ksp("com.squareup.moshi:moshi-kotlin-codegen:1.13.0") 79 | def appCenterSdkVersion = '4.4.2' 80 | implementation "com.microsoft.appcenter:appcenter-analytics:${appCenterSdkVersion}" 81 | implementation "com.microsoft.appcenter:appcenter-crashes:${appCenterSdkVersion}" 82 | } -------------------------------------------------------------------------------- /app/libs/android-30.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GeekTR/PrivacySpace/46eb0c846abb57cff0ca3a92b4a2e403765b9c31/app/libs/android-30.jar -------------------------------------------------------------------------------- /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 -------------------------------------------------------------------------------- /app/src/androidTest/java/cn/geektang/privacyspace/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package cn.geektang.privacyspace 2 | 3 | import androidx.test.platform.app.InstrumentationRegistry 4 | import androidx.test.ext.junit.runners.AndroidJUnit4 5 | 6 | import org.junit.Test 7 | import org.junit.runner.RunWith 8 | 9 | import org.junit.Assert.* 10 | 11 | /** 12 | * Instrumented test, which will execute on an Android device. 13 | * 14 | * See [testing documentation](http://d.android.com/tools/testing). 15 | */ 16 | @RunWith(AndroidJUnit4::class) 17 | class ExampleInstrumentedTest { 18 | @Test 19 | fun useAppContext() { 20 | // Context of the app under test. 21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext 22 | assertEquals("cn.geektang.privacyspace", appContext.packageName) 23 | } 24 | } -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 9 | 10 | 18 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 32 | 35 | 38 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /app/src/main/assets/xposed_init: -------------------------------------------------------------------------------- 1 | cn.geektang.privacyspace.hook.HookMain -------------------------------------------------------------------------------- /app/src/main/java/cn/geektang/privacyspace/App.kt: -------------------------------------------------------------------------------- 1 | package cn.geektang.privacyspace 2 | 3 | import android.app.Application 4 | import com.microsoft.appcenter.AppCenter 5 | import com.microsoft.appcenter.analytics.Analytics 6 | import com.microsoft.appcenter.crashes.Crashes 7 | 8 | class App : Application() { 9 | override fun onCreate() { 10 | super.onCreate() 11 | AppCenter.start( 12 | this, BuildConfig.APP_CENTER_SECRET, 13 | Analytics::class.java, Crashes::class.java 14 | ) 15 | } 16 | } -------------------------------------------------------------------------------- /app/src/main/java/cn/geektang/privacyspace/bean/AppInfo.kt: -------------------------------------------------------------------------------- 1 | package cn.geektang.privacyspace.bean 2 | 3 | import android.content.pm.ApplicationInfo 4 | import android.graphics.drawable.Drawable 5 | 6 | class AppInfo( 7 | val applicationInfo: ApplicationInfo, 8 | val packageName: String, 9 | val appIcon: Drawable, 10 | val appName: String, 11 | val sharedUserId: String?, 12 | val isSystemApp: Boolean, 13 | val isXposedModule: Boolean 14 | ) -------------------------------------------------------------------------------- /app/src/main/java/cn/geektang/privacyspace/bean/ConfigData.kt: -------------------------------------------------------------------------------- 1 | package cn.geektang.privacyspace.bean 2 | 3 | import cn.geektang.privacyspace.BuildConfig 4 | import com.squareup.moshi.Json 5 | import com.squareup.moshi.JsonClass 6 | 7 | @JsonClass(generateAdapter = true) 8 | data class ConfigData( 9 | @Json(name = "enableLog") 10 | val enableDetailLog: Boolean, 11 | val hiddenAppList: Set, 12 | val whitelist: Set, 13 | val connectedApps: Map>, 14 | val sharedUserIdMap: Map?, 15 | val multiUserConfig: Map>?, 16 | val blind: Set?, 17 | ) { 18 | companion object { 19 | val EMPTY = ConfigData( 20 | BuildConfig.DEBUG, 21 | hiddenAppList = emptySet(), 22 | whitelist = emptySet(), 23 | connectedApps = emptyMap(), 24 | sharedUserIdMap = emptyMap(), 25 | multiUserConfig = emptyMap(), 26 | blind = emptySet() 27 | ) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /app/src/main/java/cn/geektang/privacyspace/bean/SystemUserInfo.kt: -------------------------------------------------------------------------------- 1 | package cn.geektang.privacyspace.bean 2 | 3 | import com.squareup.moshi.JsonClass 4 | 5 | @JsonClass(generateAdapter = true) 6 | data class SystemUserInfo( 7 | val id: Int, 8 | val name: String 9 | ) -------------------------------------------------------------------------------- /app/src/main/java/cn/geektang/privacyspace/constant/ConfigConstant.kt: -------------------------------------------------------------------------------- 1 | package cn.geektang.privacyspace.constant 2 | 3 | import cn.geektang.privacyspace.BuildConfig 4 | 5 | object ConfigConstant { 6 | const val ANDROID_FRAMEWORK = "android" 7 | const val CONFIG_FILE_FOLDER_ORIGINAL = "/data/system/privacy_space/" 8 | const val CONFIG_FILE_FOLDER = "/data/system/${BuildConfig.APPLICATION_ID}/" 9 | const val CONFIG_FILE_JSON = "config.json" 10 | 11 | val defaultWhitelist = setOf( 12 | "com.android.systemui", 13 | "android.uid.system", 14 | "com.android.providers.media.module", 15 | "com.android.providers.telephony", 16 | "com.android.providers.calendar", 17 | "com.android.providers.media", 18 | "com.android.providers.downloads", 19 | "com.android.providers.downloads.ui", 20 | "com.android.providers.settings", 21 | "com.android.providers.partnerbookmarks", 22 | "com.android.providers.settings.auto_generated_rro_product__", 23 | "com.android.providers.contacts.auto_generated_rro_product__", 24 | "com.android.providers.telephony.auto_generated_rro_product__", 25 | "com.android.bookmarkprovider", 26 | "com.android.providers.blockednumber", 27 | "com.android.providers.userdictionary", 28 | "com.android.providers.media.module", 29 | "com.android.providers.contacts", 30 | "com.android.permissioncontroller", 31 | "com.lbe.security.miui", 32 | "com.google.android.documentsui", 33 | "android.uid.phone", 34 | "com.topjohnwu.magisk", 35 | "android.uid.nfc", 36 | "android.uid.bluetooth", 37 | "android.uid.systemui", 38 | "android.uid.networkstack", 39 | "com.google.uid.shared", 40 | "com.miui.packageinstaller", 41 | "com.android.packageinstaller", 42 | "com.google.android.packageinstaller", 43 | "com.google.android.providers.media.module", 44 | "com.google.android.permissioncontroller", 45 | "com.google.android.webview", 46 | BuildConfig.APPLICATION_ID 47 | ) 48 | 49 | val defaultBlindWhitelist = setOf( 50 | "android", 51 | "android.uid.system", 52 | "android.uid.phone", 53 | "android.uid.bluetooth", 54 | "android.uid.nfc", 55 | "android.uid.se", 56 | "android.uid.networkstack", 57 | "android.uid.shell", 58 | "android.uid.shared", 59 | "android.uid.qtiphone", 60 | "android.uid.systemui", 61 | "android.media", 62 | "android.uid.calendar", 63 | "com.android.emergency.uid", 64 | "com.google.uid.shared", 65 | "com.google.android.webview", 66 | "com.google.android.providers.media.module", 67 | "com.android.providers.media.module", 68 | "com.android.providers.telephony", 69 | "com.android.providers.calendar", 70 | "com.android.providers.media", 71 | "com.android.providers.downloads", 72 | "com.android.providers.downloads.ui", 73 | "com.android.providers.settings", 74 | "com.android.providers.partnerbookmarks", 75 | "com.android.providers.settings.auto_generated_rro_product__", 76 | "com.android.providers.contacts.auto_generated_rro_product__", 77 | "com.android.providers.telephony.auto_generated_rro_product__", 78 | "com.android.bookmarkprovider", 79 | "com.android.providers.blockednumber", 80 | "com.android.providers.userdictionary", 81 | "com.android.providers.media.module", 82 | "com.android.providers.contacts", 83 | "com.android.permissioncontroller" 84 | ) 85 | } -------------------------------------------------------------------------------- /app/src/main/java/cn/geektang/privacyspace/constant/RouteConstant.kt: -------------------------------------------------------------------------------- 1 | package cn.geektang.privacyspace.constant 2 | 3 | object RouteConstant { 4 | const val LAUNCHER = "launcher" 5 | 6 | const val ADD_HIDDEN_APPS = "addHiddenApps" 7 | 8 | const val WHITELIST = "Whitelist" 9 | 10 | const val SET_CONNECTED_APPS = "setConnectedApps" 11 | 12 | const val BLACKLIST = "blacklist" 13 | 14 | const val ABOUT = "about" 15 | } -------------------------------------------------------------------------------- /app/src/main/java/cn/geektang/privacyspace/constant/UiSettings.kt: -------------------------------------------------------------------------------- 1 | package cn.geektang.privacyspace.constant 2 | 3 | import android.content.Context 4 | import android.content.res.Configuration 5 | import android.graphics.drawable.GradientDrawable 6 | import androidx.compose.runtime.Composable 7 | import androidx.compose.runtime.LaunchedEffect 8 | import androidx.compose.ui.platform.LocalConfiguration 9 | import cn.geektang.privacyspace.util.* 10 | 11 | object UiSettings { 12 | @Composable 13 | fun Context.obtainCellCount(): Int { 14 | return when (LocalConfiguration.current.orientation) { 15 | Configuration.ORIENTATION_LANDSCAPE -> { 16 | sp.iconCellCountLandscape 17 | } 18 | else -> { 19 | sp.iconCellCountPortrait 20 | } 21 | } 22 | } 23 | 24 | @Composable 25 | fun Context.obtainCellContentPaddingRatio(): Float { 26 | return when (LocalConfiguration.current.orientation) { 27 | Configuration.ORIENTATION_LANDSCAPE -> { 28 | sp.iconPaddingLandscape 29 | } 30 | else -> { 31 | sp.iconPaddingPortrait 32 | } 33 | } 34 | } 35 | 36 | @Composable 37 | fun WatchingAndSaveConfig( 38 | cellCount: Int, 39 | cellContentPaddingRatio: Float, 40 | context: Context 41 | ) { 42 | val orientation = LocalConfiguration.current.orientation 43 | LaunchedEffect(key1 = cellCount, key2 = cellContentPaddingRatio, block = { 44 | when (orientation) { 45 | Configuration.ORIENTATION_LANDSCAPE -> { 46 | context.sp.iconCellCountLandscape = cellCount 47 | context.sp.iconPaddingLandscape = cellContentPaddingRatio 48 | } 49 | else -> { 50 | context.sp.iconCellCountPortrait = cellCount 51 | context.sp.iconPaddingPortrait = cellContentPaddingRatio 52 | } 53 | } 54 | }) 55 | } 56 | 57 | fun getCellCountChoices(orientation: Int): Array { 58 | return if (orientation == Configuration.ORIENTATION_LANDSCAPE) { 59 | arrayOf(6, 8) 60 | } else { 61 | arrayOf(4, 5) 62 | } 63 | } 64 | } -------------------------------------------------------------------------------- /app/src/main/java/cn/geektang/privacyspace/hook/HookMain.kt: -------------------------------------------------------------------------------- 1 | package cn.geektang.privacyspace.hook 2 | 3 | import android.app.Application 4 | import android.content.Context 5 | import android.os.Build 6 | import android.os.FileObserver 7 | import cn.geektang.privacyspace.bean.ConfigData 8 | import cn.geektang.privacyspace.constant.ConfigConstant 9 | import cn.geektang.privacyspace.hook.impl.* 10 | import cn.geektang.privacyspace.util.* 11 | import de.robv.android.xposed.IXposedHookLoadPackage 12 | import de.robv.android.xposed.XC_MethodHook 13 | import de.robv.android.xposed.XposedHelpers 14 | import de.robv.android.xposed.callbacks.XC_LoadPackage 15 | import kotlinx.coroutines.MainScope 16 | import kotlinx.coroutines.launch 17 | import java.io.File 18 | 19 | class HookMain : IXposedHookLoadPackage { 20 | 21 | companion object { 22 | private lateinit var classLoader: ClassLoader 23 | private var packageName: String? = null 24 | private var fileObserver: FileObserver? = null 25 | private val configServer = ConfigServer() 26 | 27 | @Volatile 28 | var configData: ConfigData = ConfigData.EMPTY 29 | private set 30 | 31 | fun updateConfigData(data: ConfigData) { 32 | val sharedUserIdMap = data.sharedUserIdMap 33 | val whitelistTmp = data.whitelist.toMutableSet() 34 | val connectedAppsTpm = data.connectedApps.toMutableMap() 35 | if (!sharedUserIdMap.isNullOrEmpty()) { 36 | sharedUserIdMap.forEach { 37 | if (whitelistTmp.contains(it.key)) { 38 | whitelistTmp.add(it.value) 39 | } 40 | } 41 | 42 | val connectedAppsNew = mutableMapOf>() 43 | connectedAppsTpm.forEach { 44 | val newSet = it.value.toMutableSet() 45 | it.value.forEach { packageName -> 46 | val sharedUserId = sharedUserIdMap[packageName] 47 | if (!sharedUserId.isNullOrEmpty()) { 48 | newSet.add(sharedUserId) 49 | } 50 | } 51 | connectedAppsNew[it.key] = newSet 52 | 53 | val keySharedUserId = sharedUserIdMap[it.key] 54 | if (!keySharedUserId.isNullOrEmpty()) { 55 | connectedAppsNew[keySharedUserId] = newSet 56 | } 57 | } 58 | connectedAppsTpm.putAll(connectedAppsNew) 59 | } 60 | 61 | this.configData = configData.copy( 62 | whitelist = whitelistTmp, 63 | connectedApps = connectedAppsTpm 64 | ) 65 | XLog.enableLog = configData.enableDetailLog 66 | } 67 | } 68 | 69 | override fun handleLoadPackage(lpparam: XC_LoadPackage.LoadPackageParam) { 70 | packageName = lpparam.packageName 71 | classLoader = lpparam.classLoader 72 | if (lpparam.packageName == ConfigConstant.ANDROID_FRAMEWORK) { 73 | loadConfigDataAndParse() 74 | configServer.start(classLoader = classLoader) 75 | when { 76 | Build.VERSION.SDK_INT >= 30 -> { 77 | FrameworkHookerApi30Impl.start(classLoader) 78 | } 79 | Build.VERSION.SDK_INT >= 28 -> { 80 | FrameworkHookerApi28Impl.start(classLoader) 81 | } 82 | else -> { 83 | FrameworkHookerApi26Impl.start(classLoader) 84 | } 85 | } 86 | } else if ("com.android.settings" == lpparam.packageName) { 87 | SettingsAppHookImpl.start(classLoader) 88 | XposedHelpers.findAndHookMethod( 89 | Application::class.java, 90 | "onCreate", 91 | object : XC_MethodHook() { 92 | override fun beforeHookedMethod(param: MethodHookParam) { 93 | val configClient = ConfigClient(param.thisObject as Context) 94 | MainScope().launch { 95 | val config = configClient.queryConfig() 96 | XLog.i("config = $config") 97 | if (null != config) { 98 | updateConfigData(config) 99 | } 100 | } 101 | } 102 | }) 103 | } else if (AppHelper.isSystemApp(lpparam.appInfo)) { 104 | XLog.i("Hook class fdfasdfs start.") 105 | loadConfigDataAndParse() 106 | startWatchingConfigFiles() 107 | SpecialAppsHookerImpl.start(classLoader) 108 | } 109 | } 110 | 111 | private fun startWatchingConfigFiles() { 112 | if (null == fileObserver) { 113 | val file = File(ConfigConstant.CONFIG_FILE_FOLDER) 114 | file.mkdirs() 115 | fileObserver = 116 | object : FileObserver(ConfigConstant.CONFIG_FILE_FOLDER) { 117 | override fun onEvent(event: Int, path: String?) { 118 | if (event == CLOSE_WRITE && path == ConfigConstant.CONFIG_FILE_JSON) { 119 | loadConfigDataAndParse() 120 | XLog.i("$packageName reload config.json") 121 | } 122 | } 123 | } 124 | fileObserver?.startWatching() 125 | } 126 | } 127 | 128 | private fun loadConfigDataAndParse() { 129 | val configDataNew = 130 | ConfigHelper.loadConfigWithSystemApp(packageName ?: "") ?: ConfigData.EMPTY 131 | if (configDataNew != configData) { 132 | configData = configDataNew 133 | updateConfigData(configDataNew) 134 | } 135 | } 136 | } -------------------------------------------------------------------------------- /app/src/main/java/cn/geektang/privacyspace/hook/Hooker.kt: -------------------------------------------------------------------------------- 1 | package cn.geektang.privacyspace.hook 2 | 3 | interface Hooker { 4 | fun start(classLoader: ClassLoader) 5 | 6 | val Any.packageName: String 7 | get() = toString().substringAfterLast(" ").substringBefore("/") 8 | } -------------------------------------------------------------------------------- /app/src/main/java/cn/geektang/privacyspace/hook/impl/FrameworkHookerApi26Impl.kt: -------------------------------------------------------------------------------- 1 | package cn.geektang.privacyspace.hook.impl 2 | 3 | import android.content.pm.PackageManager 4 | import android.content.pm.ResolveInfo 5 | import android.os.Binder 6 | import cn.geektang.privacyspace.hook.Hooker 7 | import cn.geektang.privacyspace.util.ConfigHelper.getPackageName 8 | import cn.geektang.privacyspace.util.HookUtil 9 | import cn.geektang.privacyspace.util.XLog 10 | import cn.geektang.privacyspace.util.tryLoadClass 11 | import de.robv.android.xposed.XC_MethodHook 12 | import de.robv.android.xposed.XposedBridge 13 | import java.lang.reflect.Method 14 | 15 | object FrameworkHookerApi26Impl : XC_MethodHook(), Hooker { 16 | private lateinit var pmsClass: Class<*> 17 | private lateinit var packageSettingClass: Class<*> 18 | private lateinit var getPackageNameForUidMethod: Method 19 | private lateinit var classLoader: ClassLoader 20 | 21 | override fun start(classLoader: ClassLoader) { 22 | this.classLoader = classLoader 23 | try { 24 | pmsClass = HookUtil.loadPms(classLoader) ?: throw PackageManager.NameNotFoundException() 25 | packageSettingClass = 26 | classLoader.tryLoadClass("com.android.server.pm.PackageSetting") 27 | getPackageNameForUidMethod = pmsClass.getDeclaredMethod( 28 | "getNameForUid", 29 | Int::class.javaPrimitiveType 30 | ) 31 | getPackageNameForUidMethod.isAccessible = true 32 | } catch (e: Throwable) { 33 | XLog.e(e, "pmsClass load failed.") 34 | return 35 | } 36 | 37 | pmsClass.declaredMethods.forEach { method -> 38 | when (method.name) { 39 | "filterSharedLibPackageLPr" -> { 40 | if (method.parameterCount == 4) { 41 | XposedBridge.hookMethod(method, this) 42 | } 43 | } 44 | "applyPostResolutionFilter" -> { 45 | XposedBridge.hookMethod(method, this) 46 | } 47 | else -> {} 48 | } 49 | } 50 | } 51 | 52 | override fun afterHookedMethod(param: MethodHookParam) { 53 | when (param.method.name) { 54 | "filterSharedLibPackageLPr" -> { 55 | hookFilterSharedLibPackageLPr(param) 56 | } 57 | "applyPostResolutionFilter" -> { 58 | hookApplyPostResolutionFilter(param) 59 | } 60 | } 61 | } 62 | 63 | private fun hookApplyPostResolutionFilter(param: MethodHookParam) { 64 | val resultList = param.result as? MutableList<*> ?: return 65 | val uid = Binder.getCallingUid() 66 | val userId = uid / 100000 67 | val callingPackageName = 68 | getPackageNameForUidMethod.invoke(param.thisObject, uid) 69 | ?.toString()?.split(":")?.first() ?: return 70 | val waitRemoveList = mutableListOf() 71 | for (resolveInfo in resultList) { 72 | val targetPackageName = (resolveInfo as? ResolveInfo)?.getPackageName() ?: continue 73 | val shouldIntercept = HookChecker.shouldIntercept( 74 | classLoader, 75 | userId, 76 | targetPackageName, 77 | callingPackageName 78 | ) 79 | if (shouldIntercept) { 80 | waitRemoveList.add(resolveInfo) 81 | } 82 | } 83 | for (resolveInfo in waitRemoveList) { 84 | resultList.remove(resolveInfo) 85 | } 86 | if (waitRemoveList.isNotEmpty()) { 87 | param.result = resultList 88 | } 89 | } 90 | 91 | private fun hookFilterSharedLibPackageLPr(param: MethodHookParam) { 92 | if (param.result == true) { 93 | return 94 | } 95 | val packageSetting = param.args.first() 96 | val targetPackageName = packageSetting?.packageName ?: return 97 | val userId = param.args[2] as? Int ?: return 98 | val callingPackageName = 99 | getPackageNameForUidMethod.invoke(param.thisObject, param.args[1]) 100 | ?.toString()?.split(":")?.first() ?: return 101 | 102 | val shouldIntercept = HookChecker.shouldIntercept( 103 | classLoader, 104 | userId, 105 | targetPackageName, 106 | callingPackageName 107 | ) 108 | if (shouldIntercept) { 109 | param.result = true 110 | } 111 | } 112 | } -------------------------------------------------------------------------------- /app/src/main/java/cn/geektang/privacyspace/hook/impl/FrameworkHookerApi28Impl.kt: -------------------------------------------------------------------------------- 1 | package cn.geektang.privacyspace.hook.impl 2 | 3 | import android.content.pm.PackageManager 4 | import android.content.pm.ResolveInfo 5 | import android.os.UserHandle 6 | import cn.geektang.privacyspace.hook.Hooker 7 | import cn.geektang.privacyspace.util.ConfigHelper.getPackageName 8 | import cn.geektang.privacyspace.util.HookUtil 9 | import cn.geektang.privacyspace.util.XLog 10 | import cn.geektang.privacyspace.util.tryLoadClass 11 | import de.robv.android.xposed.XC_MethodHook 12 | import de.robv.android.xposed.XposedBridge 13 | import java.lang.reflect.Field 14 | import java.lang.reflect.Method 15 | 16 | object FrameworkHookerApi28Impl : XC_MethodHook(), Hooker { 17 | private lateinit var pmsClass: Class<*> 18 | private lateinit var settingsClass: Class<*> 19 | private lateinit var mSettingsField: Field 20 | private lateinit var getAppIdMethod: Method 21 | private lateinit var getSettingLPrMethod: Method 22 | private lateinit var classLoader: ClassLoader 23 | 24 | override fun start(classLoader: ClassLoader) { 25 | this.classLoader = classLoader 26 | try { 27 | pmsClass = HookUtil.loadPms(classLoader) ?: throw PackageManager.NameNotFoundException() 28 | mSettingsField = pmsClass.getDeclaredField("mSettings") 29 | mSettingsField.isAccessible = true 30 | 31 | settingsClass = classLoader.tryLoadClass("com.android.server.pm.Settings") 32 | getAppIdMethod = 33 | UserHandle::class.java.getDeclaredMethod("getAppId", Int::class.javaPrimitiveType) 34 | getAppIdMethod.isAccessible = true 35 | 36 | for (method in settingsClass.declaredMethods) { 37 | if ((method.name == "getSettingLPr" || method.name == "getUserIdLPr") 38 | && method.parameterCount == 1 39 | && method.parameterTypes.first() == Int::class.javaPrimitiveType 40 | ) { 41 | getSettingLPrMethod = method 42 | method.isAccessible = true 43 | break 44 | } 45 | } 46 | } catch (e: Throwable) { 47 | XLog.e(e, "pms load failed.") 48 | return 49 | } 50 | pmsClass.declaredMethods.forEach { method -> 51 | when (method.name) { 52 | "filterAppAccessLPr" -> { 53 | if (method.parameterCount == 5) { 54 | XposedBridge.hookMethod(method, this) 55 | } 56 | } 57 | "applyPostResolutionFilter" -> { 58 | XposedBridge.hookMethod(method, this) 59 | } 60 | else -> {} 61 | } 62 | } 63 | } 64 | 65 | override fun afterHookedMethod(param: MethodHookParam) { 66 | when (param.method.name) { 67 | "filterAppAccessLPr" -> { 68 | hookFilterAppAccess(param) 69 | } 70 | "applyPostResolutionFilter" -> { 71 | hookApplyPostResolutionFilter(param) 72 | } 73 | else -> {} 74 | } 75 | 76 | } 77 | 78 | private fun hookApplyPostResolutionFilter(param: MethodHookParam) { 79 | val resultList = param.result as? MutableList<*> ?: return 80 | val callingUid = param.args[3] as? Int ?: return 81 | val userId = param.args[5] as? Int ?: return 82 | val callingPackageName = getPackageName(param.thisObject, callingUid) ?: return 83 | val waitRemoveList = mutableListOf() 84 | for (resolveInfo in resultList) { 85 | val targetPackageName = (resolveInfo as? ResolveInfo)?.getPackageName() ?: continue 86 | val shouldIntercept = HookChecker.shouldIntercept( 87 | classLoader, 88 | userId, 89 | targetPackageName, 90 | callingPackageName 91 | ) 92 | if (shouldIntercept) { 93 | waitRemoveList.add(resolveInfo) 94 | } 95 | } 96 | 97 | for (resolveInfo in waitRemoveList) { 98 | resultList.remove(resolveInfo) 99 | } 100 | if (waitRemoveList.isNotEmpty()) { 101 | param.result = resultList 102 | } 103 | } 104 | 105 | private fun hookFilterAppAccess(param: MethodHookParam) { 106 | if (param.result == true) { 107 | return 108 | } 109 | val packageSetting = param.args.first() 110 | val targetPackageName = packageSetting?.packageName ?: return 111 | val callingUid = param.args[1] as Int 112 | val userId = param.args[4] as Int 113 | val callingPackageName = getPackageName(param.thisObject, callingUid) ?: return 114 | 115 | val shouldIntercept = HookChecker.shouldIntercept( 116 | classLoader, 117 | userId, 118 | targetPackageName, 119 | callingPackageName 120 | ) 121 | if (shouldIntercept) { 122 | param.result = true 123 | } 124 | } 125 | 126 | private fun getPackageName(pms: Any, uid: Int): String? { 127 | val callingAppId = getAppIdMethod.invoke(null, uid) 128 | val mSettings = mSettingsField.get(pms) 129 | return getSettingLPrMethod.invoke(mSettings, callingAppId)?.packageName 130 | } 131 | } -------------------------------------------------------------------------------- /app/src/main/java/cn/geektang/privacyspace/hook/impl/FrameworkHookerApi30Impl.kt: -------------------------------------------------------------------------------- 1 | package cn.geektang.privacyspace.hook.impl 2 | 3 | import cn.geektang.privacyspace.hook.Hooker 4 | import cn.geektang.privacyspace.util.XLog 5 | import cn.geektang.privacyspace.util.tryLoadClass 6 | import de.robv.android.xposed.XC_MethodHook 7 | import de.robv.android.xposed.XposedHelpers 8 | 9 | object FrameworkHookerApi30Impl : XC_MethodHook(), Hooker { 10 | private lateinit var classLoader: ClassLoader 11 | 12 | override fun start(classLoader: ClassLoader) { 13 | this.classLoader = classLoader 14 | val appsFilerClass: Class<*> 15 | val settingBaseClass: Class<*> 16 | val packageSettingClass: Class<*> 17 | try { 18 | appsFilerClass = classLoader.tryLoadClass("com.android.server.pm.AppsFilter") 19 | settingBaseClass = classLoader.tryLoadClass("com.android.server.pm.SettingBase") 20 | packageSettingClass = 21 | classLoader.tryLoadClass("com.android.server.pm.PackageSetting") 22 | } catch (e: ClassNotFoundException) { 23 | XLog.e(e, "FrameworkHookerApi30Impl start failed.") 24 | return 25 | } 26 | 27 | XposedHelpers.findAndHookMethod( 28 | appsFilerClass, 29 | "shouldFilterApplication", 30 | Int::class.javaPrimitiveType, 31 | settingBaseClass, 32 | packageSettingClass, 33 | Int::class.javaPrimitiveType, 34 | this 35 | ) 36 | } 37 | 38 | override fun afterHookedMethod(param: MethodHookParam) { 39 | if (param.result == true) { 40 | return 41 | } 42 | val targetPackageName = param.args[2]?.packageName ?: return 43 | val callingPackageName = param.args[1]?.packageName ?: return 44 | val userId = param.args[3] as Int 45 | val shouldIntercept = HookChecker.shouldIntercept( 46 | classLoader = classLoader, 47 | targetPackageName = targetPackageName, 48 | callingPackageName = callingPackageName, 49 | userId = userId 50 | ) 51 | if (shouldIntercept) { 52 | param.result = true 53 | } 54 | } 55 | } -------------------------------------------------------------------------------- /app/src/main/java/cn/geektang/privacyspace/hook/impl/HookChecker.kt: -------------------------------------------------------------------------------- 1 | package cn.geektang.privacyspace.hook.impl 2 | 3 | import android.content.pm.PackageInfo 4 | import android.content.pm.PackageManager 5 | import android.os.Build 6 | import android.os.ServiceManager 7 | import android.util.ArrayMap 8 | import android.util.SparseArray 9 | import androidx.core.util.forEach 10 | import cn.geektang.privacyspace.constant.ConfigConstant 11 | import cn.geektang.privacyspace.hook.HookMain 12 | import cn.geektang.privacyspace.util.HookUtil 13 | import cn.geektang.privacyspace.util.XLog 14 | 15 | object HookChecker { 16 | @Volatile 17 | private var greenChannel = false 18 | private var defaultBlindWhitelist: Set = emptySet() 19 | 20 | @JvmStatic 21 | fun shouldIntercept( 22 | classLoader: ClassLoader, 23 | userId: Int, 24 | targetPackageName: String, 25 | callingPackageName: String 26 | ): Boolean { 27 | if (greenChannel) { 28 | return false 29 | } 30 | 31 | if (defaultBlindWhitelist.isEmpty()) { 32 | greenChannel = true 33 | 34 | val sharedUserIdMap = getSharedUserIdMap(classLoader) 35 | if (null != sharedUserIdMap) { 36 | val defaultBlindWhitelist = ConfigConstant.defaultBlindWhitelist.toMutableSet() 37 | for (white in ConfigConstant.defaultBlindWhitelist) { 38 | val value = sharedUserIdMap[white] ?: emptyList() 39 | defaultBlindWhitelist.addAll(value) 40 | } 41 | this@HookChecker.defaultBlindWhitelist = defaultBlindWhitelist 42 | } 43 | } 44 | greenChannel = false 45 | 46 | if (callingPackageName == targetPackageName) { 47 | return false 48 | } 49 | 50 | var result = false 51 | val configData = HookMain.configData 52 | val shouldFilterAppList = configData.hiddenAppList 53 | val userWhitelist = configData.whitelist 54 | val connectedAppsInfoMap = configData.connectedApps 55 | val multiUserConfig = configData.multiUserConfig ?: emptyMap() 56 | val blindApps = configData.blind ?: emptySet() 57 | val defaultWhitelist = ConfigConstant.defaultWhitelist 58 | val defaultBlindWhitelist = defaultBlindWhitelist 59 | XLog.enableLog = configData.enableDetailLog 60 | 61 | if (defaultBlindWhitelist.isNotEmpty() 62 | && !defaultBlindWhitelist.contains(targetPackageName) 63 | && blindApps.contains(callingPackageName) 64 | && connectedAppsInfoMap[callingPackageName]?.contains(targetPackageName) != true 65 | && connectedAppsInfoMap[targetPackageName]?.contains(callingPackageName) != true 66 | ) { 67 | XLog.d("$callingPackageName was prevented from reading ${targetPackageName}.") 68 | return true 69 | } 70 | 71 | if (!defaultWhitelist.contains(callingPackageName) 72 | && shouldFilterAppList.contains(targetPackageName) 73 | ) { 74 | val appMultiUserConfig = multiUserConfig[targetPackageName] 75 | // User's custom whitelist and 'connected apps' 76 | if (!userWhitelist.contains(callingPackageName) 77 | && connectedAppsInfoMap[callingPackageName]?.contains(targetPackageName) != true 78 | && connectedAppsInfoMap[targetPackageName]?.contains(callingPackageName) != true 79 | && (appMultiUserConfig.isNullOrEmpty() || appMultiUserConfig.contains(userId)) 80 | ) { 81 | XLog.d("$callingPackageName was prevented from reading ${targetPackageName}.") 82 | result = true 83 | } else { 84 | XLog.d("$callingPackageName read ${targetPackageName}.") 85 | } 86 | } 87 | return result 88 | } 89 | 90 | private fun getSharedUserIdMap(classLoader: ClassLoader): Map>? { 91 | val pms = ServiceManager.getService("package") 92 | val pmsClass = HookUtil.loadPms(classLoader) 93 | if (pms?.javaClass == pmsClass) { 94 | return if (Build.VERSION.SDK_INT >= 29) { 95 | getSharedUidMapAfterQ(pms) 96 | } else { 97 | getSharedUidMapCompat(pms) 98 | } 99 | } 100 | return null 101 | } 102 | 103 | // more efficient 104 | private fun getSharedUidMapAfterQ(pms: Any): Map>? { 105 | val pmsClass = pms.javaClass 106 | return try { 107 | val getAppsWithSharedUserMethod = 108 | pmsClass.getDeclaredMethod("getAppsWithSharedUserIdsLocked") 109 | getAppsWithSharedUserMethod.isAccessible = true 110 | val getPackagesForUidMethod = pmsClass.getDeclaredMethod( 111 | "getPackagesForUid", 112 | Int::class.javaPrimitiveType 113 | ) 114 | getPackagesForUidMethod.isAccessible = true 115 | 116 | val sharedUserIdMap = ArrayMap>() 117 | val result = getAppsWithSharedUserMethod.invoke(pms) as SparseArray<*> 118 | result.forEach { key, value -> 119 | val packages = 120 | getPackagesForUidMethod.invoke( 121 | pms, 122 | key 123 | ) as Array<*> 124 | sharedUserIdMap[value.toString()] = (packages as Array).toList() 125 | } 126 | sharedUserIdMap 127 | } catch (e: Throwable) { 128 | getSharedUidMapCompat(pms) 129 | } 130 | } 131 | 132 | // better compatibility 133 | private fun getSharedUidMapCompat(pms: Any): Map>? { 134 | val pmsClass = pms.javaClass 135 | return try { 136 | val getInstalledPackagesMethod = pmsClass.getDeclaredMethod( 137 | "getInstalledPackages", 138 | Int::class.javaPrimitiveType, 139 | Int::class.javaPrimitiveType 140 | ) ?: return null 141 | getInstalledPackagesMethod.isAccessible = true 142 | val resultParceledListSlice = getInstalledPackagesMethod.invoke( 143 | pms, 144 | PackageManager.MATCH_UNINSTALLED_PACKAGES, 145 | 0 146 | ) 147 | val listMethod = resultParceledListSlice.javaClass.getDeclaredMethod("getList") 148 | listMethod.isAccessible = true 149 | val resultList = listMethod.invoke(resultParceledListSlice) as? List<*> ?: return null 150 | val sharedUserIdMap = ArrayMap>() 151 | for (packageInfo in resultList) { 152 | if (packageInfo !is PackageInfo) return null 153 | val sharedUserId = packageInfo.sharedUserId 154 | if (sharedUserId.isNullOrEmpty()) { 155 | continue 156 | } 157 | val sharedUserIdPackages = 158 | sharedUserIdMap.getOrDefault(sharedUserId, mutableListOf()) 159 | sharedUserIdPackages.add(packageInfo.packageName) 160 | sharedUserIdMap[sharedUserId] = sharedUserIdPackages 161 | } 162 | sharedUserIdMap 163 | } catch (e: Throwable) { 164 | null 165 | } 166 | } 167 | } -------------------------------------------------------------------------------- /app/src/main/java/cn/geektang/privacyspace/hook/impl/SettingsAppHookImpl.kt: -------------------------------------------------------------------------------- 1 | package cn.geektang.privacyspace.hook.impl 2 | 3 | import android.app.ActivityManager 4 | import android.app.usage.UsageStats 5 | import android.app.usage.UsageStatsManager 6 | import android.content.pm.* 7 | import android.os.Build 8 | import cn.geektang.privacyspace.hook.HookMain 9 | import cn.geektang.privacyspace.hook.Hooker 10 | import cn.geektang.privacyspace.util.ConfigHelper.getPackageName 11 | import cn.geektang.privacyspace.util.XLog 12 | import cn.geektang.privacyspace.util.tryLoadClass 13 | import de.robv.android.xposed.XC_MethodHook 14 | import de.robv.android.xposed.XposedBridge 15 | import de.robv.android.xposed.XposedHelpers 16 | 17 | object SettingsAppHookImpl : Hooker, XC_MethodHook() { 18 | 19 | override fun start(classLoader: ClassLoader) { 20 | val packageManagerClass = try { 21 | classLoader.tryLoadClass("android.content.pm.IPackageManager\$Stub\$Proxy") 22 | } catch (e: ClassNotFoundException) { 23 | XLog.e(e, "SettingsAppHookImpl start failed.") 24 | return 25 | } 26 | XLog.i("Hook class packageManagerClass.") 27 | for (method in packageManagerClass.declaredMethods) { 28 | when (method.name) { 29 | "getInstalledPackages", "getInstalledApplications", "getInstalledModules", "queryIntentActivities" -> { 30 | XLog.d("Hook method ${method.name}") 31 | XposedBridge.hookMethod(method, this) 32 | } 33 | else -> {} 34 | } 35 | } 36 | 37 | XposedHelpers.findAndHookMethod( 38 | UsageStatsManager::class.java, 39 | "queryUsageStats", 40 | Int::class.javaPrimitiveType, 41 | Long::class.javaPrimitiveType, 42 | Long::class.javaPrimitiveType, 43 | this 44 | ) 45 | 46 | XposedHelpers.findAndHookMethod( 47 | ActivityManager::class.java, 48 | "getRunningAppProcesses", 49 | this 50 | ) 51 | } 52 | 53 | override fun afterHookedMethod(param: MethodHookParam) { 54 | val hiddenAppList = HookMain.configData.hiddenAppList 55 | when (param.method.name) { 56 | "getInstalledPackages" -> { 57 | val result = param.result as ParceledListSlice 58 | val iterator = result.list.iterator() 59 | while (iterator.hasNext()) { 60 | val packageInfo = iterator.next() 61 | if (hiddenAppList.contains(packageInfo.packageName)) { 62 | iterator.remove() 63 | XLog.i("com.android.settings was prevented from reading ${packageInfo.packageName}.") 64 | } 65 | } 66 | } 67 | "getInstalledApplications" -> { 68 | val result = param.result as ParceledListSlice 69 | val iterator = result.list.iterator() 70 | while (iterator.hasNext()) { 71 | val applicationInfo = iterator.next() 72 | if (hiddenAppList.contains(applicationInfo.packageName)) { 73 | iterator.remove() 74 | XLog.i("com.android.settings was prevented from reading ${applicationInfo.packageName}.") 75 | } 76 | } 77 | } 78 | "getInstalledModules" -> { 79 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { 80 | val result = param.result as MutableList 81 | val iterator = result.iterator() 82 | while (iterator.hasNext()) { 83 | val applicationInfo = iterator.next() 84 | if (hiddenAppList.contains(applicationInfo.packageName)) { 85 | iterator.remove() 86 | XLog.i("com.android.settings was prevented from reading ${applicationInfo.packageName}.") 87 | } 88 | } 89 | } 90 | } 91 | "queryIntentActivities" -> { 92 | val result = param.result as ParceledListSlice 93 | val iterator = result.list.iterator() 94 | while (iterator.hasNext()) { 95 | val resolveInfo = iterator.next() 96 | if (hiddenAppList.contains(resolveInfo.getPackageName())) { 97 | iterator.remove() 98 | XLog.i("com.android.settings was prevented from reading ${resolveInfo.getPackageName()}.") 99 | } 100 | } 101 | } 102 | "queryUsageStats" -> { 103 | val result = param.result as MutableList 104 | val iterator = result.iterator() 105 | while (iterator.hasNext()) { 106 | val usageStats = iterator.next() 107 | if (hiddenAppList.contains(usageStats.packageName)) { 108 | iterator.remove() 109 | XLog.i("com.android.settings was prevented from reading ${usageStats.packageName}.") 110 | } 111 | } 112 | } 113 | "getRunningAppProcesses" -> { 114 | val result = param.result as? List<*> ?: return 115 | param.result = result.filter { 116 | it as ActivityManager.RunningAppProcessInfo 117 | var shouldFilter = false 118 | it.pkgList.forEach { 119 | if (hiddenAppList.contains(it)) { 120 | shouldFilter = true 121 | } 122 | } 123 | !shouldFilter 124 | } 125 | } 126 | else -> {} 127 | } 128 | } 129 | } -------------------------------------------------------------------------------- /app/src/main/java/cn/geektang/privacyspace/ui/main/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package cn.geektang.privacyspace.ui.main 2 | 3 | import android.os.Bundle 4 | import androidx.activity.ComponentActivity 5 | import androidx.activity.compose.setContent 6 | import androidx.compose.foundation.layout.fillMaxSize 7 | import androidx.compose.material.MaterialTheme 8 | import androidx.compose.material.Surface 9 | import androidx.compose.runtime.Composable 10 | import androidx.compose.runtime.CompositionLocalProvider 11 | import androidx.compose.runtime.remember 12 | import androidx.compose.ui.Modifier 13 | import androidx.core.view.WindowCompat 14 | import androidx.lifecycle.lifecycleScope 15 | import androidx.navigation.NavType 16 | import androidx.navigation.compose.NavHost 17 | import androidx.navigation.compose.composable 18 | import androidx.navigation.compose.rememberNavController 19 | import cn.geektang.privacyspace.constant.RouteConstant 20 | import cn.geektang.privacyspace.ui.screen.about.AboutScreen 21 | import cn.geektang.privacyspace.ui.screen.blind.AddBlindAppsScreen 22 | import cn.geektang.privacyspace.ui.screen.launcher.LauncherScreen 23 | import cn.geektang.privacyspace.ui.screen.managehiddenapps.AddHiddenAppsScreen 24 | import cn.geektang.privacyspace.ui.screen.setconnectedapps.SetConnectedAppsScreen 25 | import cn.geektang.privacyspace.ui.screen.setwhitelist.SetWhitelistScreen 26 | import cn.geektang.privacyspace.ui.theme.PrivacySpaceTheme 27 | import cn.geektang.privacyspace.util.AppHelper 28 | import cn.geektang.privacyspace.util.ConfigHelper 29 | import cn.geektang.privacyspace.util.LocalNavHostController 30 | import cn.geektang.privacyspace.util.NavHostControllerWrapper 31 | import com.google.accompanist.insets.ProvideWindowInsets 32 | import kotlinx.coroutines.launch 33 | 34 | class MainActivity : ComponentActivity() { 35 | override fun onCreate(savedInstanceState: Bundle?) { 36 | super.onCreate(savedInstanceState) 37 | WindowCompat.setDecorFitsSystemWindows(window, false) 38 | setContent { 39 | PrivacySpaceTheme { 40 | ProvideWindowInsets { 41 | Surface( 42 | modifier = Modifier.fillMaxSize(), 43 | color = MaterialTheme.colors.background 44 | ) { 45 | Content() 46 | } 47 | } 48 | } 49 | } 50 | 51 | lifecycleScope.launch { 52 | AppHelper.initialize(applicationContext) 53 | } 54 | } 55 | 56 | override fun onResume() { 57 | super.onResume() 58 | if (!isFinishing 59 | && ConfigHelper.loadingStatusFlow.value == ConfigHelper.LOADING_STATUS_INIT 60 | ) { 61 | ConfigHelper.initConfig(applicationContext) 62 | } 63 | } 64 | } 65 | 66 | @Composable 67 | private fun Content() { 68 | val navHostController = rememberNavController() 69 | 70 | val navHostControllerWrapper = remember { 71 | NavHostControllerWrapper(navHostController) 72 | } 73 | CompositionLocalProvider(LocalNavHostController provides navHostControllerWrapper) { 74 | NavHost( 75 | navController = navHostController, 76 | startDestination = RouteConstant.LAUNCHER 77 | ) { 78 | composable(RouteConstant.LAUNCHER) { 79 | LauncherScreen() 80 | } 81 | composable(RouteConstant.ADD_HIDDEN_APPS) { 82 | AddHiddenAppsScreen() 83 | } 84 | composable(RouteConstant.WHITELIST) { 85 | SetWhitelistScreen() 86 | } 87 | composable("${RouteConstant.SET_CONNECTED_APPS}?targetPackageName={targetPackageName}") { 88 | argument("targetPackageName") { 89 | type = NavType.StringType 90 | } 91 | SetConnectedAppsScreen() 92 | } 93 | composable(RouteConstant.BLACKLIST) { 94 | AddBlindAppsScreen() 95 | } 96 | 97 | composable(RouteConstant.ABOUT) { 98 | AboutScreen() 99 | } 100 | } 101 | } 102 | } -------------------------------------------------------------------------------- /app/src/main/java/cn/geektang/privacyspace/ui/screen/about/AboutScreen.kt: -------------------------------------------------------------------------------- 1 | package cn.geektang.privacyspace.ui.screen.about 2 | 3 | import androidx.annotation.DrawableRes 4 | import androidx.compose.foundation.Image 5 | import androidx.compose.foundation.clickable 6 | import androidx.compose.foundation.layout.* 7 | import androidx.compose.material.MaterialTheme 8 | import androidx.compose.material.Surface 9 | import androidx.compose.material.Text 10 | import androidx.compose.material.primarySurface 11 | import androidx.compose.runtime.Composable 12 | import androidx.compose.runtime.remember 13 | import androidx.compose.ui.Alignment 14 | import androidx.compose.ui.Modifier 15 | import androidx.compose.ui.draw.clip 16 | import androidx.compose.ui.graphics.Color 17 | import androidx.compose.ui.layout.ContentScale 18 | import androidx.compose.ui.platform.LocalContext 19 | import androidx.compose.ui.res.painterResource 20 | import androidx.compose.ui.res.stringResource 21 | import androidx.compose.ui.tooling.preview.Preview 22 | import androidx.compose.ui.unit.dp 23 | import androidx.compose.ui.unit.sp 24 | import cn.geektang.privacyspace.BuildConfig 25 | import cn.geektang.privacyspace.R 26 | import cn.geektang.privacyspace.ui.widget.TopBar 27 | import cn.geektang.privacyspace.util.LocalNavHostController 28 | import cn.geektang.privacyspace.util.openUrl 29 | import coil.compose.rememberImagePainter 30 | 31 | @Preview(showSystemUi = true) 32 | @Composable 33 | fun AboutScreen() { 34 | val navController = LocalNavHostController.current 35 | Column(modifier = Modifier.fillMaxSize()) { 36 | TopBar(title = stringResource(R.string.about), onNavigationIconClick = { 37 | navController.popBackStack() 38 | }) 39 | 40 | Column( 41 | modifier = Modifier 42 | .fillMaxWidth() 43 | .padding(vertical = 20.dp), 44 | horizontalAlignment = Alignment.CenterHorizontally 45 | ) { 46 | val appName = stringResource(id = R.string.app_name) 47 | Image( 48 | painter = painterResource(id = R.mipmap.ic_launcher_round), 49 | contentDescription = appName 50 | ) 51 | Text( 52 | modifier = Modifier.padding(top = 5.dp), 53 | text = appName, 54 | style = MaterialTheme.typography.h6 55 | ) 56 | Text( 57 | text = "Version: ${BuildConfig.VERSION_NAME}", 58 | style = MaterialTheme.typography.subtitle1 59 | ) 60 | } 61 | 62 | GroupTitle(text = "What's this") 63 | Text( 64 | modifier = Modifier 65 | .fillMaxWidth() 66 | .padding(horizontal = 15.dp, vertical = 10.dp), 67 | text = stringResource(R.string.about_page_app_intro), 68 | fontSize = 14.sp 69 | ) 70 | 71 | GroupTitle(text = "Developers") 72 | val developers = remember { 73 | listOf( 74 | CardItem( 75 | R.drawable.ic_avatar, 76 | "GeekTR", 77 | "Developer & Designer", 78 | "https://github.com/GeekTR" 79 | ), 80 | CardItem( 81 | R.drawable.ic_github, 82 | "Source Code", 83 | "https://github.com/GeekTR/PrivacySpace", 84 | "https://github.com/GeekTR/PrivacySpace" 85 | ), 86 | ) 87 | } 88 | developers.forEach { 89 | Card(cardItem = it) 90 | } 91 | 92 | val context = LocalContext.current 93 | val telegramAndCoolapk = remember { 94 | listOf( 95 | CardItem( 96 | R.drawable.ic_telegram, 97 | "Telegram", 98 | context.getString(R.string.about_page_telegram_description), 99 | "https://t.me/PrivacySpaceAlpha" 100 | ), 101 | CardItem( 102 | R.drawable.ic_coolapk, 103 | context.getString(R.string.coolapk), 104 | context.getString(R.string.about_page_coolapk_description), 105 | "coolmarket://u/18765870" 106 | ), 107 | ) 108 | } 109 | GroupTitle(text = "Telegram & Coolapk") 110 | telegramAndCoolapk.forEach { 111 | Card(cardItem = it) 112 | } 113 | } 114 | } 115 | 116 | @Composable 117 | private fun GroupTitle(text: String) { 118 | val isLight = MaterialTheme.colors.isLight 119 | val primarySurfaceColor = MaterialTheme.colors.primarySurface 120 | val surfaceBackgroundColor = remember(isLight) { 121 | if (isLight) { 122 | Color(0xfff7f7f7) 123 | } else { 124 | primarySurfaceColor 125 | } 126 | } 127 | Surface(modifier = Modifier.fillMaxWidth(), color = surfaceBackgroundColor, elevation = 2.dp) { 128 | Text( 129 | modifier = Modifier 130 | .padding(horizontal = 15.dp, vertical = 10.dp), 131 | text = text 132 | ) 133 | } 134 | } 135 | 136 | @Composable 137 | private fun Card(cardItem: CardItem) { 138 | val isLight = MaterialTheme.colors.isLight 139 | val textColor = remember(isLight) { 140 | if (isLight) { 141 | Color(0xde000000) 142 | } else { 143 | Color(0xdeffffff) 144 | } 145 | } 146 | val hintColor = remember(isLight) { 147 | if (isLight) { 148 | Color(0xff757575) 149 | } else { 150 | Color(0x80ffffff) 151 | } 152 | } 153 | 154 | val context = LocalContext.current 155 | Row( 156 | modifier = Modifier 157 | .clickable { 158 | try { 159 | context.openUrl(cardItem.homePage) 160 | } catch (e: Exception) { 161 | } 162 | } 163 | .fillMaxWidth() 164 | .padding(horizontal = 15.dp, vertical = 10.dp), 165 | verticalAlignment = Alignment.CenterVertically 166 | ) { 167 | Image( 168 | modifier = Modifier 169 | .clip(MaterialTheme.shapes.small) 170 | .size(38.dp), 171 | painter = rememberImagePainter(data = cardItem.avatar), 172 | contentDescription = "avatar", 173 | contentScale = ContentScale.Crop 174 | ) 175 | 176 | Column(modifier = Modifier.padding(horizontal = 10.dp)) { 177 | Text(text = cardItem.name, fontSize = 14.sp, color = textColor) 178 | Text(text = cardItem.description, fontSize = 12.sp, color = hintColor) 179 | } 180 | } 181 | } 182 | 183 | class CardItem( 184 | @DrawableRes val avatar: Int, 185 | val name: String, 186 | val description: String, 187 | val homePage: String 188 | ) -------------------------------------------------------------------------------- /app/src/main/java/cn/geektang/privacyspace/ui/screen/blind/AddBlindAppsScreen.kt: -------------------------------------------------------------------------------- 1 | package cn.geektang.privacyspace.ui.screen.blind 2 | 3 | import androidx.compose.foundation.clickable 4 | import androidx.compose.foundation.layout.* 5 | import androidx.compose.foundation.lazy.LazyColumn 6 | import androidx.compose.foundation.lazy.items 7 | import androidx.compose.foundation.shape.RoundedCornerShape 8 | import androidx.compose.material.MaterialTheme 9 | import androidx.compose.material.Text 10 | import androidx.compose.runtime.* 11 | import androidx.compose.ui.Modifier 12 | import androidx.compose.ui.draw.clip 13 | import androidx.compose.ui.res.stringResource 14 | import androidx.compose.ui.unit.dp 15 | import androidx.lifecycle.Lifecycle 16 | import androidx.lifecycle.viewmodel.compose.viewModel 17 | import cn.geektang.privacyspace.R 18 | import cn.geektang.privacyspace.bean.AppInfo 19 | import cn.geektang.privacyspace.constant.RouteConstant 20 | import cn.geektang.privacyspace.ui.widget.* 21 | import cn.geektang.privacyspace.util.LocalNavHostController 22 | import cn.geektang.privacyspace.util.OnLifecycleEvent 23 | import com.google.accompanist.insets.navigationBarsPadding 24 | 25 | @Composable 26 | fun AddBlindAppsScreen(viewModel: AddBlindAppsViewModel = viewModel()) { 27 | val isPopupMenuShow = remember { 28 | mutableStateOf(false) 29 | } 30 | val searchText = viewModel.searchTextFlow.collectAsState().value 31 | val isLoading = viewModel.allAppInfoListFlow.collectAsState().value.isEmpty() 32 | val appInfoList = viewModel.appInfoListFlow.collectAsState().value 33 | val blindApps = viewModel.blindAppsListFlow.collectAsState().value 34 | val whitelistApps = viewModel.whitelistFlow.collectAsState().value 35 | val showSystemApps = viewModel.isShowSystemAppsFlow.collectAsState().value 36 | AddBlindAppsScreen( 37 | isPopupMenuShow, 38 | showSystemApps, 39 | onSystemAppsVisibleChange = { showSystemApps -> 40 | isPopupMenuShow.value = false 41 | viewModel.setSystemAppsVisible(showSystemApps) 42 | }) 43 | Column { 44 | val navController = LocalNavHostController.current 45 | SearchTopBar( 46 | title = stringResource(R.string.blind), 47 | searchText = searchText, 48 | onSearchTextChange = { 49 | viewModel.updateSearchText(it) 50 | }, showMorePopupState = isPopupMenuShow, 51 | onNavigationIconClick = { 52 | navController.popBackStack() 53 | }) 54 | LoadingBox( 55 | modifier = Modifier.fillMaxSize(), 56 | showLoading = isLoading 57 | ) { 58 | LazyColumn(content = { 59 | items(appInfoList) { appInfo -> 60 | AppItem(blindApps, appInfo, whitelistApps, viewModel) 61 | } 62 | item { 63 | Box(modifier = Modifier.navigationBarsPadding()) 64 | } 65 | }) 66 | } 67 | } 68 | OnLifecycleEvent { event -> 69 | if (event == Lifecycle.Event.ON_PAUSE 70 | || event == Lifecycle.Event.ON_STOP 71 | || event == Lifecycle.Event.ON_DESTROY 72 | ) { 73 | viewModel.tryUpdateConfig() 74 | } 75 | } 76 | } 77 | 78 | @Composable 79 | private fun AppItem( 80 | blindApps: Set, 81 | appInfo: AppInfo, 82 | whitelistApps: Set, 83 | actions: AddBlindAppsActions 84 | ) { 85 | var showConfirmDialog by remember { 86 | mutableStateOf(false) 87 | } 88 | val isChecked = blindApps.contains(appInfo.packageName) 89 | Column { 90 | val customButtons = remember(isChecked) { 91 | if (isChecked) { 92 | val setConnectedAppsButton: (@Composable () -> Unit) = { 93 | val navController = LocalNavHostController.current 94 | Chip( 95 | modifier = Modifier 96 | .padding(all = 2.5.dp) 97 | .clip(RoundedCornerShape(percent = 50)) 98 | .clickable { 99 | navController.navigate("${RouteConstant.SET_CONNECTED_APPS}?targetPackageName=${appInfo.packageName}") 100 | }, 101 | color = MaterialTheme.colors.primary, 102 | text = stringResource(R.string.click_to_set_connected_apps) 103 | ) 104 | } 105 | listOf(setConnectedAppsButton) 106 | } else { 107 | emptyList() 108 | } 109 | } 110 | AppInfoColumnItem(appInfo, isChecked, customButtons, onClick = { 111 | if (!blindApps.contains(appInfo.packageName)) { 112 | if (whitelistApps.contains(appInfo.packageName)) { 113 | showConfirmDialog = true 114 | } else { 115 | actions.addApp2BlindList(appInfo) 116 | } 117 | } else { 118 | actions.removeApp2BlindList(appInfo) 119 | } 120 | }) 121 | } 122 | if (showConfirmDialog) { 123 | MessageDialog( 124 | text = { 125 | Text(text = stringResource(id = R.string.tips_cancel_whitelist, appInfo.appName)) 126 | }, onPositiveButtonClick = { 127 | actions.addApp2BlindList(appInfo) 128 | showConfirmDialog = false 129 | }, onDismissRequest = { showConfirmDialog = false } 130 | ) 131 | } 132 | } 133 | 134 | @Composable 135 | private fun AddBlindAppsScreen( 136 | popupMenuShow: MutableState, 137 | showSystemApps: Boolean, 138 | onSystemAppsVisibleChange: (Boolean) -> Unit 139 | ) { 140 | PopupMenu(isShow = popupMenuShow) { 141 | Column( 142 | modifier = Modifier 143 | .padding(vertical = 5.dp) 144 | .width(IntrinsicSize.Max) 145 | ) { 146 | PopupCheckboxItem( 147 | text = stringResource(R.string.display_system_apps), 148 | checked = showSystemApps, 149 | onCheckedChange = onSystemAppsVisibleChange 150 | ) 151 | } 152 | } 153 | } 154 | 155 | interface AddBlindAppsActions { 156 | fun addApp2BlindList(appInfo: AppInfo) { 157 | } 158 | 159 | fun removeApp2BlindList(appInfo: AppInfo) { 160 | } 161 | 162 | fun setSystemAppsVisible(showSystemApps: Boolean) { 163 | } 164 | 165 | fun updateSearchText(searchText: String) { 166 | 167 | } 168 | } -------------------------------------------------------------------------------- /app/src/main/java/cn/geektang/privacyspace/ui/screen/blind/AddBlindAppsViewModel.kt: -------------------------------------------------------------------------------- 1 | package cn.geektang.privacyspace.ui.screen.blind 2 | 3 | import android.app.Application 4 | import androidx.lifecycle.AndroidViewModel 5 | import androidx.lifecycle.viewModelScope 6 | import cn.geektang.privacyspace.BuildConfig 7 | import cn.geektang.privacyspace.bean.AppInfo 8 | import cn.geektang.privacyspace.util.AppHelper 9 | import cn.geektang.privacyspace.util.AppHelper.isMatch 10 | import cn.geektang.privacyspace.util.AppHelper.sortApps 11 | import cn.geektang.privacyspace.util.ConfigHelper 12 | import cn.geektang.privacyspace.util.setDifferentValue 13 | import kotlinx.coroutines.flow.MutableStateFlow 14 | import kotlinx.coroutines.flow.collect 15 | import kotlinx.coroutines.launch 16 | import java.util.* 17 | 18 | class AddBlindAppsViewModel(private val context: Application) : AndroidViewModel(context), 19 | AddBlindAppsActions { 20 | val appInfoListFlow = MutableStateFlow>(emptyList()) 21 | val allAppInfoListFlow = MutableStateFlow>(emptyList()) 22 | val whitelistFlow = MutableStateFlow>(emptySet()) 23 | val blindAppsListFlow = MutableStateFlow>(emptySet()) 24 | val isShowSystemAppsFlow = MutableStateFlow(false) 25 | val searchTextFlow = MutableStateFlow("") 26 | private var isModified = false 27 | private val connectedAppsCache = mutableMapOf>() 28 | private val hiddenAppList = mutableSetOf() 29 | 30 | init { 31 | viewModelScope.launch { 32 | launch { 33 | ConfigHelper.configDataFlow.collect { 34 | whitelistFlow.value = it.whitelist.toSet() 35 | blindAppsListFlow.value = it.blind?.toSet() ?: emptySet() 36 | 37 | connectedAppsCache.clear() 38 | connectedAppsCache.putAll(it.connectedApps) 39 | 40 | hiddenAppList.clear() 41 | hiddenAppList.addAll(it.hiddenAppList) 42 | } 43 | } 44 | launch { 45 | loadAllAppList(context) 46 | } 47 | } 48 | } 49 | 50 | private suspend fun loadAllAppList(context: Application) { 51 | AppHelper.allApps.collect { apps -> 52 | val appList = 53 | apps.sortApps(context = context, toTopCollections = blindAppsListFlow.value) 54 | allAppInfoListFlow.value = appList 55 | updateAppInfoListFlow() 56 | } 57 | } 58 | 59 | private fun updateAppInfoListFlow() { 60 | var appList = allAppInfoListFlow.value 61 | val searchText = searchTextFlow.value 62 | val searchTextLowercase = searchText.lowercase(Locale.getDefault()) 63 | if (searchText.isNotEmpty()) { 64 | appList = appList.filter { 65 | it.isMatch(searchTextLowercase) 66 | } 67 | } 68 | if (isShowSystemAppsFlow.value) { 69 | appInfoListFlow.setDifferentValue(appList) 70 | } else { 71 | val hiddenAppList = blindAppsListFlow.value 72 | appInfoListFlow.setDifferentValue(appList.filter { 73 | BuildConfig.APPLICATION_ID != it.packageName && 74 | (!it.isSystemApp || hiddenAppList.contains(it.packageName)) 75 | }) 76 | } 77 | } 78 | 79 | override fun addApp2BlindList(appInfo: AppInfo) { 80 | val newAppsList = blindAppsListFlow.value.toMutableSet() 81 | newAppsList.add(appInfo.packageName) 82 | blindAppsListFlow.value = newAppsList 83 | isModified = true 84 | } 85 | 86 | override fun removeApp2BlindList(appInfo: AppInfo) { 87 | val newAppsList = blindAppsListFlow.value.toMutableSet() 88 | newAppsList.remove(appInfo.packageName) 89 | blindAppsListFlow.value = newAppsList 90 | if (!hiddenAppList.contains(appInfo.packageName)) { 91 | connectedAppsCache.remove(appInfo.packageName) 92 | } 93 | isModified = true 94 | } 95 | 96 | override fun setSystemAppsVisible(showSystemApps: Boolean) { 97 | if (showSystemApps != isShowSystemAppsFlow.value) { 98 | isShowSystemAppsFlow.value = showSystemApps 99 | updateAppInfoListFlow() 100 | } 101 | } 102 | 103 | fun tryUpdateConfig() { 104 | if (isModified) { 105 | val blindAppsList = blindAppsListFlow.value 106 | val whitelist = whitelistFlow.value.toMutableSet() 107 | for (app in blindAppsList) { 108 | if (whitelist.contains(app)) { 109 | whitelist.remove(app) 110 | } 111 | } 112 | 113 | ConfigHelper.updateBlindApps( 114 | whitelistNew = whitelist, 115 | blindAppsListNew = blindAppsList, 116 | connectedAppsNew = connectedAppsCache 117 | ) 118 | isModified = false 119 | } 120 | } 121 | 122 | override fun updateSearchText(searchText: String) { 123 | searchTextFlow.value = searchText 124 | updateAppInfoListFlow() 125 | } 126 | } -------------------------------------------------------------------------------- /app/src/main/java/cn/geektang/privacyspace/ui/screen/launcher/LauncherViewModel.kt: -------------------------------------------------------------------------------- 1 | package cn.geektang.privacyspace.ui.screen.launcher 2 | 3 | import android.app.Application 4 | import android.content.pm.ApplicationInfo 5 | import android.content.pm.PackageInfo 6 | import android.content.pm.PackageManager 7 | import androidx.compose.runtime.mutableStateListOf 8 | import androidx.compose.runtime.mutableStateMapOf 9 | import androidx.compose.runtime.mutableStateOf 10 | import androidx.lifecycle.AndroidViewModel 11 | import androidx.lifecycle.viewModelScope 12 | import cn.geektang.privacyspace.R 13 | import cn.geektang.privacyspace.bean.AppInfo 14 | import cn.geektang.privacyspace.bean.ConfigData 15 | import cn.geektang.privacyspace.bean.SystemUserInfo 16 | import cn.geektang.privacyspace.util.AppHelper 17 | import cn.geektang.privacyspace.util.AppHelper.getPackageInfo 18 | import cn.geektang.privacyspace.util.AppHelper.isXposedModule 19 | import cn.geektang.privacyspace.util.ConfigHelper 20 | import cn.geektang.privacyspace.util.showToast 21 | import kotlinx.coroutines.flow.collect 22 | import kotlinx.coroutines.launch 23 | 24 | class LauncherViewModel(private val context: Application) : AndroidViewModel(context) { 25 | val hiddenAppList = mutableStateListOf() 26 | val configData = mutableStateOf(ConfigData.EMPTY) 27 | val systemUsers = mutableStateListOf() 28 | val multiUserConfig = mutableStateMapOf>() 29 | private val sharedUserIdMap = mutableMapOf() 30 | val connectedApps = mutableMapOf>() 31 | private var needSync = false 32 | private val blindApps = mutableSetOf() 33 | 34 | init { 35 | ConfigHelper.initConfig(context) 36 | viewModelScope.launch { 37 | systemUsers.clear() 38 | systemUsers.addAll(ConfigHelper.queryAllUsers() ?: emptyList()) 39 | } 40 | viewModelScope.launch { 41 | ConfigHelper.configDataFlow.collect { 42 | configData.value = it 43 | hiddenAppList.clear() 44 | hiddenAppList.addAll(it.hiddenAppList.mapToAppInfoList()) 45 | 46 | sharedUserIdMap.clear() 47 | sharedUserIdMap.putAll(it.sharedUserIdMap ?: emptyMap()) 48 | 49 | connectedApps.clear() 50 | connectedApps.putAll(it.connectedApps) 51 | 52 | multiUserConfig.clear() 53 | multiUserConfig.putAll(it.multiUserConfig ?: emptyMap()) 54 | 55 | blindApps.clear() 56 | blindApps.addAll(it.blind ?: emptySet()) 57 | } 58 | } 59 | } 60 | 61 | private fun Set.mapToAppInfoList(): List { 62 | return this.mapNotNull { packageName -> 63 | try { 64 | getPackageInfo( 65 | context = context, 66 | packageName = packageName, 67 | PackageManager.MATCH_UNINSTALLED_PACKAGES or PackageManager.GET_META_DATA 68 | ) 69 | } catch (e: Throwable) { 70 | null 71 | } 72 | } 73 | .sortedWith(DisplayNameComparator(context.packageManager)) 74 | .map { packageInfo -> 75 | val applicationInfo = packageInfo.applicationInfo 76 | val appName = applicationInfo.loadLabel(context.packageManager).toString() 77 | val appIcon = applicationInfo.loadIcon(context.packageManager) 78 | val isXposedModule = applicationInfo.isXposedModule() 79 | AppInfo( 80 | applicationInfo = applicationInfo, 81 | packageName = applicationInfo.packageName, 82 | appName = appName, 83 | appIcon = appIcon, 84 | sharedUserId = packageInfo.sharedUserId, 85 | isSystemApp = AppHelper.isSystemApp(applicationInfo), 86 | isXposedModule = isXposedModule 87 | ) 88 | } 89 | } 90 | 91 | class DisplayNameComparator(packageManager: PackageManager) : Comparator { 92 | private val innerComparator = ApplicationInfo.DisplayNameComparator(packageManager) 93 | override fun compare(p0: PackageInfo, p1: PackageInfo): Int { 94 | return innerComparator.compare(p0.applicationInfo, p1.applicationInfo) 95 | } 96 | } 97 | 98 | fun cancelHide(appInfo: AppInfo) { 99 | val hasChange = this.hiddenAppList.removeIf { it.packageName == appInfo.packageName } 100 | if (hasChange) { 101 | multiUserConfig.remove(appInfo.packageName) 102 | if (!blindApps.contains(appInfo.packageName)) { 103 | connectedApps.remove(appInfo.packageName) 104 | } 105 | } 106 | needSync = needSync or hasChange 107 | } 108 | 109 | fun connectTo(sourceApp: AppInfo, targetApp: String) { 110 | val connectedAppsForSourceApp = 111 | connectedApps[sourceApp.packageName]?.toMutableSet() ?: mutableSetOf() 112 | connectedAppsForSourceApp.add(targetApp) 113 | connectedApps[sourceApp.packageName] = connectedAppsForSourceApp 114 | 115 | val sharedUserIdForSourceApp = sourceApp.sharedUserId 116 | if (!sharedUserIdForSourceApp.isNullOrEmpty()) { 117 | sharedUserIdMap[sourceApp.packageName] = sharedUserIdForSourceApp 118 | } 119 | 120 | val sharedUserIdForTargetApp = sourceApp.sharedUserId 121 | if (!sharedUserIdForTargetApp.isNullOrEmpty()) { 122 | sharedUserIdMap[targetApp] = sharedUserIdForTargetApp 123 | } 124 | 125 | needSync = true 126 | } 127 | 128 | fun disconnectTo(sourceApp: AppInfo, targetApp: String) { 129 | val connectedAppsForSourceApp = 130 | connectedApps[sourceApp.packageName]?.toMutableSet() ?: mutableSetOf() 131 | val hasChange = connectedAppsForSourceApp.remove(targetApp) 132 | connectedApps[sourceApp.packageName] = connectedAppsForSourceApp 133 | needSync = needSync or hasChange 134 | } 135 | 136 | suspend fun syncConfig() { 137 | if (needSync) { 138 | needSync = false 139 | syncConfigInner() 140 | } 141 | } 142 | 143 | private suspend fun syncConfigInner() { 144 | ConfigHelper.updateConfig( 145 | configData.value.copy( 146 | hiddenAppList = hiddenAppList.map { it.packageName }.toSet(), 147 | sharedUserIdMap = sharedUserIdMap.toMap(), 148 | connectedApps = connectedApps.toMap(), 149 | multiUserConfig = multiUserConfig.toMap() 150 | ) 151 | ) 152 | } 153 | 154 | fun changeMultiUserConfig(appInfo: AppInfo, checkedUsers: Set?) { 155 | if (multiUserConfig[appInfo.packageName] != checkedUsers) { 156 | if (null == checkedUsers) { 157 | multiUserConfig.remove(appInfo.packageName) 158 | } else { 159 | multiUserConfig[appInfo.packageName] = checkedUsers 160 | } 161 | needSync = true 162 | 163 | if (ConfigHelper.getServerVersion() < 17) { 164 | context.showToast(R.string.configuration_takes_effect_after_restarting_the_phone_system) 165 | } 166 | } 167 | } 168 | } -------------------------------------------------------------------------------- /app/src/main/java/cn/geektang/privacyspace/ui/screen/managehiddenapps/AddHiddenAppsScreen.kt: -------------------------------------------------------------------------------- 1 | package cn.geektang.privacyspace.ui.screen.managehiddenapps 2 | 3 | import android.content.Context 4 | import android.content.pm.ApplicationInfo 5 | import android.graphics.drawable.ColorDrawable 6 | import androidx.compose.foundation.layout.* 7 | import androidx.compose.foundation.lazy.LazyColumn 8 | import androidx.compose.foundation.lazy.items 9 | import androidx.compose.material.AlertDialog 10 | import androidx.compose.material.MaterialTheme 11 | import androidx.compose.material.Text 12 | import androidx.compose.material.TextButton 13 | import androidx.compose.runtime.* 14 | import androidx.compose.ui.Modifier 15 | import androidx.compose.ui.platform.LocalContext 16 | import androidx.compose.ui.res.stringResource 17 | import androidx.compose.ui.tooling.preview.Preview 18 | import androidx.compose.ui.unit.dp 19 | import androidx.compose.ui.window.DialogProperties 20 | import androidx.lifecycle.Lifecycle 21 | import androidx.lifecycle.viewmodel.compose.viewModel 22 | import cn.geektang.privacyspace.BuildConfig 23 | import cn.geektang.privacyspace.R 24 | import cn.geektang.privacyspace.bean.AppInfo 25 | import cn.geektang.privacyspace.ui.widget.* 26 | import cn.geektang.privacyspace.util.* 27 | import com.google.accompanist.insets.navigationBarsPadding 28 | import kotlin.system.exitProcess 29 | 30 | @Composable 31 | fun AddHiddenAppsScreen(viewModel: AddHiddenAppsViewModel = viewModel()) { 32 | val allAppInfoList by viewModel.allAppInfoListFlow.collectAsState() 33 | val isLoading = allAppInfoList.isEmpty() 34 | val appInfoList by viewModel.appInfoListFlow.collectAsState() 35 | val hiddenAppList by viewModel.hiddenAppListFlow.collectAsState() 36 | val showSystemApps by viewModel.isShowSystemAppsFlow.collectAsState() 37 | val searchText by viewModel.searchTextFlow.collectAsState() 38 | val actions = object : AddHiddenAppsActions { 39 | override fun addApp2HiddenList(appInfo: AppInfo) { 40 | viewModel.addApp2HiddenList(appInfo) 41 | } 42 | 43 | override fun removeApp2HiddenList(appInfo: AppInfo) { 44 | viewModel.removeApp2HiddenList(appInfo) 45 | } 46 | 47 | override fun setSystemAppsVisible(showSystemApps: Boolean) { 48 | viewModel.setShowSystemApps(showSystemApps) 49 | } 50 | 51 | override fun onSearchTextChange(searchText: String) { 52 | viewModel.updateSearchText(searchText) 53 | } 54 | } 55 | 56 | AddHiddenAppsContent( 57 | appInfoList = appInfoList, 58 | hiddenAppList = hiddenAppList, 59 | searchText = searchText, 60 | isLoading = isLoading, 61 | showSystemApps = showSystemApps, 62 | actions = actions 63 | ) 64 | 65 | val context = LocalContext.current 66 | NoticeDialogLocal(context) 67 | 68 | OnLifecycleEvent { event -> 69 | if (event == Lifecycle.Event.ON_PAUSE 70 | || event == Lifecycle.Event.ON_STOP 71 | || event == Lifecycle.Event.ON_DESTROY 72 | ) { 73 | viewModel.tryUpdateConfig() 74 | } 75 | } 76 | } 77 | 78 | @Composable 79 | private fun NoticeDialogLocal(context: Context) { 80 | var isShowAlterDialog by remember { 81 | mutableStateOf(!context.sp.hasReadNotice2) 82 | } 83 | if (isShowAlterDialog) { 84 | NoticeDialog( 85 | text = stringResource(R.string.tips_whitelist_magisk), 86 | onPositiveButtonClick = { 87 | isShowAlterDialog = false 88 | context.sp.hasReadNotice2 = true 89 | }, 90 | onDismissRequest = { 91 | isShowAlterDialog = false 92 | }) 93 | } 94 | } 95 | 96 | @Composable 97 | fun AddHiddenAppsContent( 98 | appInfoList: List, 99 | hiddenAppList: Set, 100 | searchText: String, 101 | isLoading: Boolean, 102 | showSystemApps: Boolean, 103 | actions: AddHiddenAppsActions 104 | ) { 105 | val isPopupMenuShow = remember { 106 | mutableStateOf(false) 107 | } 108 | AddHiddenPopupMenu( 109 | isPopupMenuShow, 110 | showSystemApps, 111 | onSystemAppsVisibleChange = { showSystemApps -> 112 | isPopupMenuShow.value = false 113 | actions.setSystemAppsVisible(showSystemApps) 114 | }) 115 | Column { 116 | val navController = LocalNavHostController.current 117 | SearchTopBar( 118 | title = stringResource(R.string.add_hidden_apps), 119 | searchText = searchText, 120 | onSearchTextChange = { 121 | actions.onSearchTextChange(it) 122 | }, showMorePopupState = isPopupMenuShow, 123 | onNavigationIconClick = { 124 | navController.popBackStack() 125 | }) 126 | LoadingBox( 127 | modifier = Modifier.fillMaxSize(), 128 | showLoading = isLoading 129 | ) { 130 | LazyColumn(content = { 131 | items(appInfoList) { appInfo -> 132 | val isChecked = hiddenAppList.contains(appInfo.packageName) 133 | AppInfoColumnItem(appInfo, isChecked, onClick = { 134 | if (!hiddenAppList.contains(appInfo.packageName)) { 135 | actions.addApp2HiddenList(appInfo) 136 | } else { 137 | actions.removeApp2HiddenList(appInfo) 138 | } 139 | }) 140 | } 141 | item { 142 | Box(modifier = Modifier.navigationBarsPadding()) 143 | } 144 | }) 145 | } 146 | } 147 | } 148 | 149 | @Composable 150 | private fun AddHiddenPopupMenu( 151 | popupMenuShow: MutableState, 152 | showSystemApps: Boolean, 153 | onSystemAppsVisibleChange: (Boolean) -> Unit 154 | ) { 155 | PopupMenu(isShow = popupMenuShow) { 156 | Column( 157 | modifier = Modifier 158 | .padding(vertical = 5.dp) 159 | .width(IntrinsicSize.Max) 160 | ) { 161 | PopupCheckboxItem( 162 | text = stringResource(R.string.display_system_apps), 163 | checked = showSystemApps, 164 | onCheckedChange = onSystemAppsVisibleChange 165 | ) 166 | } 167 | } 168 | } 169 | 170 | @Preview(showSystemUi = true) 171 | @Composable 172 | fun AddHiddenAppsScreenPreview() { 173 | val context = LocalContext.current 174 | val data = AppInfo( 175 | appIcon = ColorDrawable(), 176 | packageName = BuildConfig.APPLICATION_ID, 177 | appName = context.getString(R.string.app_name), 178 | sharedUserId = null, 179 | isXposedModule = true, 180 | isSystemApp = false, 181 | applicationInfo = ApplicationInfo() 182 | ) 183 | val actions = object : AddHiddenAppsActions { 184 | } 185 | AddHiddenAppsContent( 186 | listOf(data, data, data, data), 187 | emptySet(), 188 | searchText = "", 189 | showSystemApps = false, 190 | isLoading = false, 191 | actions = actions 192 | ) 193 | } 194 | 195 | interface AddHiddenAppsActions { 196 | fun addApp2HiddenList(appInfo: AppInfo) { 197 | } 198 | 199 | fun removeApp2HiddenList(appInfo: AppInfo) { 200 | } 201 | 202 | fun setSystemAppsVisible(showSystemApps: Boolean) { 203 | } 204 | 205 | fun onSearchTextChange(searchText: String) { 206 | 207 | } 208 | } -------------------------------------------------------------------------------- /app/src/main/java/cn/geektang/privacyspace/ui/screen/managehiddenapps/AddHiddenAppsViewModel.kt: -------------------------------------------------------------------------------- 1 | package cn.geektang.privacyspace.ui.screen.managehiddenapps 2 | 3 | import android.app.Application 4 | import androidx.lifecycle.AndroidViewModel 5 | import androidx.lifecycle.viewModelScope 6 | import cn.geektang.privacyspace.BuildConfig 7 | import cn.geektang.privacyspace.bean.AppInfo 8 | import cn.geektang.privacyspace.constant.ConfigConstant 9 | import cn.geektang.privacyspace.util.AppHelper 10 | import cn.geektang.privacyspace.util.AppHelper.isMatch 11 | import cn.geektang.privacyspace.util.AppHelper.sortApps 12 | import cn.geektang.privacyspace.util.ConfigHelper 13 | import cn.geektang.privacyspace.util.setDifferentValue 14 | import kotlinx.coroutines.flow.MutableStateFlow 15 | import kotlinx.coroutines.flow.collect 16 | import kotlinx.coroutines.launch 17 | import java.util.* 18 | 19 | class AddHiddenAppsViewModel(private val context: Application) : AndroidViewModel(context) { 20 | val appInfoListFlow = MutableStateFlow>(emptyList()) 21 | val allAppInfoListFlow = MutableStateFlow>(emptyList()) 22 | val hiddenAppListFlow = MutableStateFlow>(emptySet()) 23 | val isShowSystemAppsFlow = MutableStateFlow(false) 24 | val searchTextFlow = MutableStateFlow("") 25 | private val multiUserConfig = mutableMapOf>() 26 | private var isModified = false 27 | private val connectedAppsCache = mutableMapOf>() 28 | private val sharedUserIdMap = mutableMapOf() 29 | private val blindApps = mutableSetOf() 30 | 31 | init { 32 | viewModelScope.launch { 33 | launch { 34 | ConfigHelper.configDataFlow.collect { 35 | hiddenAppListFlow.value = it.hiddenAppList.toSet() 36 | 37 | connectedAppsCache.clear() 38 | connectedAppsCache.putAll(it.connectedApps) 39 | 40 | val sharedUserIdMapNew = it.sharedUserIdMap ?: emptyMap() 41 | sharedUserIdMap.clear() 42 | sharedUserIdMap.putAll(sharedUserIdMapNew) 43 | 44 | multiUserConfig.clear() 45 | multiUserConfig.putAll(it.multiUserConfig ?: emptyMap()) 46 | 47 | blindApps.clear() 48 | blindApps.addAll(it.blind ?: emptyList()) 49 | } 50 | } 51 | launch { 52 | loadAllAppList(context) 53 | } 54 | } 55 | } 56 | 57 | private suspend fun loadAllAppList(context: Application) { 58 | AppHelper.allApps.collect { apps -> 59 | val appList = 60 | apps.sortApps(context = context, toTopCollections = hiddenAppListFlow.value) 61 | allAppInfoListFlow.value = appList 62 | updateAppInfoListFlow() 63 | } 64 | } 65 | 66 | private fun updateAppInfoListFlow() { 67 | var appList = allAppInfoListFlow.value 68 | val searchText = searchTextFlow.value 69 | val searchTextLowercase = searchText.lowercase(Locale.getDefault()) 70 | if (searchText.isNotEmpty()) { 71 | appList = appList.filter { 72 | it.isMatch(searchTextLowercase) 73 | } 74 | } 75 | if (isShowSystemAppsFlow.value) { 76 | appInfoListFlow.setDifferentValue(appList) 77 | } else { 78 | val hiddenAppList = hiddenAppListFlow.value 79 | appInfoListFlow.setDifferentValue(appList.filter { 80 | !it.isSystemApp || hiddenAppList.contains(it.packageName) 81 | }) 82 | } 83 | } 84 | 85 | fun addApp2HiddenList(appInfo: AppInfo) { 86 | val targetSharedUserId = appInfo.sharedUserId 87 | if (!targetSharedUserId.isNullOrEmpty()) { 88 | sharedUserIdMap[appInfo.packageName] = targetSharedUserId 89 | } 90 | 91 | val hiddenAppList = hiddenAppListFlow.value.toMutableSet() 92 | hiddenAppList.add(appInfo.packageName) 93 | hiddenAppListFlow.value = hiddenAppList 94 | if (appInfo.isXposedModule && appInfo.packageName != BuildConfig.APPLICATION_ID) { 95 | val scopeList = 96 | AppHelper.getXposedModuleScopeList(context, appInfo.applicationInfo).filter { 97 | it != ConfigConstant.ANDROID_FRAMEWORK 98 | } 99 | 100 | if (scopeList.isNotEmpty()) { 101 | val connectedApps = 102 | connectedAppsCache.getOrDefault(appInfo.packageName, mutableSetOf()) 103 | .toMutableSet() 104 | connectedApps.addAll(scopeList) 105 | connectedAppsCache[appInfo.packageName] = connectedApps 106 | } 107 | } 108 | isModified = true 109 | } 110 | 111 | fun removeApp2HiddenList(appInfo: AppInfo) { 112 | val hiddenAppList = hiddenAppListFlow.value.toMutableSet() 113 | hiddenAppList.remove(appInfo.packageName) 114 | hiddenAppListFlow.value = hiddenAppList 115 | if (!blindApps.contains(appInfo.packageName)) { 116 | connectedAppsCache.remove(appInfo.packageName) 117 | } 118 | multiUserConfig.remove(appInfo.packageName) 119 | isModified = true 120 | } 121 | 122 | fun setShowSystemApps(showSystemApps: Boolean) { 123 | if (showSystemApps != isShowSystemAppsFlow.value) { 124 | isShowSystemAppsFlow.value = showSystemApps 125 | updateAppInfoListFlow() 126 | } 127 | } 128 | 129 | fun tryUpdateConfig() { 130 | if (isModified) { 131 | ConfigHelper.updateHiddenListAndConnectedApps( 132 | hiddenAppListFlow.value, 133 | connectedAppsCache, 134 | multiUserConfig = multiUserConfig, 135 | sharedUserIdMap 136 | ) 137 | isModified = false 138 | } 139 | } 140 | 141 | fun updateSearchText(searchText: String) { 142 | searchTextFlow.value = searchText 143 | updateAppInfoListFlow() 144 | } 145 | } -------------------------------------------------------------------------------- /app/src/main/java/cn/geektang/privacyspace/ui/screen/setconnectedapps/SetConnectedAppsScreen.kt: -------------------------------------------------------------------------------- 1 | package cn.geektang.privacyspace.ui.screen.setconnectedapps 2 | 3 | import androidx.compose.foundation.background 4 | import androidx.compose.foundation.layout.* 5 | import androidx.compose.foundation.lazy.LazyColumn 6 | import androidx.compose.foundation.lazy.items 7 | import androidx.compose.material.MaterialTheme 8 | import androidx.compose.material.Text 9 | import androidx.compose.runtime.* 10 | import androidx.compose.ui.Modifier 11 | import androidx.compose.ui.res.stringResource 12 | import androidx.compose.ui.unit.dp 13 | import androidx.lifecycle.Lifecycle 14 | import androidx.lifecycle.viewmodel.compose.viewModel 15 | import cn.geektang.privacyspace.R 16 | import cn.geektang.privacyspace.bean.AppInfo 17 | import cn.geektang.privacyspace.ui.widget.* 18 | import cn.geektang.privacyspace.util.LocalNavHostController 19 | import cn.geektang.privacyspace.util.OnLifecycleEvent 20 | import com.google.accompanist.insets.navigationBarsPadding 21 | 22 | @Composable 23 | fun SetConnectedAppsScreen(viewModel: SetConnectedAppsViewModel = viewModel()) { 24 | val allAppList by viewModel.allAppListFlow.collectAsState() 25 | val isLoading = allAppList.isEmpty() 26 | val appList by viewModel.appListFlow.collectAsState() 27 | val whitelist by viewModel.whitelistFlow.collectAsState() 28 | val appName by viewModel.appNameFlow.collectAsState() 29 | val showSystemApps by viewModel.showSystemAppsFlow.collectAsState() 30 | val searchText by viewModel.searchTextFlow.collectAsState() 31 | val showSelectAll by viewModel.showSelectAll.collectAsState() 32 | val actions = object : SetConnectedAppsActions { 33 | override fun addApp2Whitelist(appInfo: AppInfo) { 34 | viewModel.addApp2HiddenList(appInfo) 35 | } 36 | 37 | override fun removeApp2Whitelist(appInfo: AppInfo) { 38 | viewModel.removeApp2HiddenList(appInfo) 39 | } 40 | 41 | override fun setSystemAppsVisible(showSystemApps: Boolean) { 42 | viewModel.setShowSystemApps(showSystemApps) 43 | } 44 | 45 | override fun onSearchTextChange(searchText: String) { 46 | viewModel.updateSearchText(searchText) 47 | } 48 | 49 | override fun selectAllSystemApps(selectAll: Boolean) { 50 | viewModel.selectAllSystemApps(selectAll) 51 | } 52 | } 53 | SetConnectedAppsContent( 54 | appName = appName, 55 | isLoading = isLoading, 56 | showSystemApps = showSystemApps, 57 | showSelectAll = showSelectAll, 58 | searchText = searchText, 59 | appList = appList, 60 | whitelist = whitelist, 61 | actions = actions 62 | ) 63 | 64 | OnLifecycleEvent(onEvent = { event -> 65 | if (event == Lifecycle.Event.ON_PAUSE 66 | || event == Lifecycle.Event.ON_STOP 67 | || event == Lifecycle.Event.ON_DESTROY 68 | ) { 69 | viewModel.tryUpdateConfig() 70 | } 71 | }) 72 | } 73 | 74 | @Composable 75 | private fun SetConnectedAppsContent( 76 | appName: String, 77 | isLoading: Boolean, 78 | showSystemApps: Boolean, 79 | showSelectAll: Boolean, 80 | searchText: String, 81 | appList: List, 82 | whitelist: Set, 83 | actions: SetConnectedAppsActions 84 | ) { 85 | val isPopupMenuShow = remember { 86 | mutableStateOf(false) 87 | } 88 | SetConnectedPopupMenu( 89 | isPopupMenuShow, 90 | showSystemApps, 91 | showSelectAll, 92 | onSystemAppsVisibleChange = { showSystemApps -> 93 | isPopupMenuShow.value = false 94 | actions.setSystemAppsVisible(showSystemApps) 95 | }, 96 | selectAllCallback = { selectAll -> 97 | actions.selectAllSystemApps(selectAll) 98 | } 99 | ) 100 | 101 | val navHostController = LocalNavHostController.current 102 | Column { 103 | SearchTopBar( 104 | title = stringResource(R.string.set_connected_apps), 105 | searchText = searchText, 106 | onSearchTextChange = { 107 | actions.onSearchTextChange(it) 108 | }, showMorePopupState = isPopupMenuShow, 109 | onNavigationIconClick = { 110 | navHostController.popBackStack() 111 | }) 112 | if (appName.isNotBlank()) { 113 | Text( 114 | modifier = Modifier 115 | .background(color = MaterialTheme.colors.secondary) 116 | .fillMaxWidth() 117 | .padding(horizontal = 15.dp, vertical = 10.dp), 118 | text = String.format(stringResource(R.string.setting_up_for), appName, appName), 119 | style = MaterialTheme.typography.caption 120 | ) 121 | } 122 | 123 | LoadingBox(modifier = Modifier.fillMaxSize(), showLoading = isLoading) { 124 | LazyColumn(content = { 125 | items(appList) { appInfo -> 126 | val isChecked = whitelist.contains(appInfo.packageName) 127 | AppInfoColumnItem(appInfo = appInfo, isChecked = isChecked) { 128 | if (!whitelist.contains(appInfo.packageName)) { 129 | actions.addApp2Whitelist(appInfo) 130 | } else { 131 | actions.removeApp2Whitelist(appInfo) 132 | } 133 | } 134 | } 135 | item { 136 | Box(modifier = Modifier.navigationBarsPadding()) 137 | } 138 | }) 139 | } 140 | } 141 | } 142 | 143 | @Composable 144 | private fun SetConnectedPopupMenu( 145 | popupMenuShow: MutableState, 146 | showSystemApps: Boolean, 147 | showSelectAll: Boolean, 148 | onSystemAppsVisibleChange: (Boolean) -> Unit, 149 | selectAllCallback: (Boolean) -> Unit 150 | ) { 151 | PopupMenu(isShow = popupMenuShow) { 152 | Column( 153 | modifier = Modifier 154 | .padding(vertical = 5.dp) 155 | .width(IntrinsicSize.Max) 156 | ) { 157 | PopupCheckboxItem( 158 | text = stringResource(R.string.display_system_apps), 159 | checked = showSystemApps, 160 | onCheckedChange = onSystemAppsVisibleChange 161 | ) 162 | PopupCheckboxItem( 163 | text = stringResource(R.string.select_all_system_apps), 164 | checked = showSelectAll, 165 | onCheckedChange = selectAllCallback 166 | ) 167 | } 168 | } 169 | } 170 | 171 | interface SetConnectedAppsActions { 172 | fun addApp2Whitelist(appInfo: AppInfo) { 173 | } 174 | 175 | fun removeApp2Whitelist(appInfo: AppInfo) { 176 | } 177 | 178 | fun setSystemAppsVisible(showSystemApps: Boolean) { 179 | } 180 | 181 | fun onSearchTextChange(searchText: String) { 182 | 183 | } 184 | 185 | fun selectAllSystemApps(selectAll: Boolean){ 186 | } 187 | } -------------------------------------------------------------------------------- /app/src/main/java/cn/geektang/privacyspace/ui/screen/setconnectedapps/SetConnectedAppsViewModel.kt: -------------------------------------------------------------------------------- 1 | package cn.geektang.privacyspace.ui.screen.setconnectedapps 2 | 3 | import android.app.Application 4 | import android.content.pm.PackageManager 5 | import androidx.lifecycle.AndroidViewModel 6 | import androidx.lifecycle.SavedStateHandle 7 | import androidx.lifecycle.viewModelScope 8 | import cn.geektang.privacyspace.bean.AppInfo 9 | import cn.geektang.privacyspace.util.AppHelper 10 | import cn.geektang.privacyspace.util.AppHelper.isMatch 11 | import cn.geektang.privacyspace.util.AppHelper.sortApps 12 | import cn.geektang.privacyspace.util.ConfigHelper 13 | import cn.geektang.privacyspace.util.setDifferentValue 14 | import kotlinx.coroutines.Dispatchers 15 | import kotlinx.coroutines.flow.MutableStateFlow 16 | import kotlinx.coroutines.flow.collect 17 | import kotlinx.coroutines.launch 18 | import kotlinx.coroutines.withContext 19 | import java.util.* 20 | 21 | class SetConnectedAppsViewModel( 22 | private val context: Application, 23 | savedStateHandle: SavedStateHandle 24 | ) : AndroidViewModel(context) { 25 | private val targetPackageName = savedStateHandle.get("targetPackageName") 26 | val allAppListFlow = MutableStateFlow>(emptyList()) 27 | val appNameFlow = MutableStateFlow("") 28 | val appListFlow = MutableStateFlow>(emptyList()) 29 | val whitelistFlow = MutableStateFlow>(emptySet()) 30 | val showSystemAppsFlow = MutableStateFlow(true) 31 | val searchTextFlow = MutableStateFlow("") 32 | val showSelectAll = MutableStateFlow(false) 33 | private var isModified = false 34 | private val sharedUserIdMap = mutableMapOf() 35 | 36 | init { 37 | viewModelScope.launch { 38 | launch { 39 | withContext(Dispatchers.IO) { 40 | if (null != targetPackageName) { 41 | val applicationInfo = AppHelper.getPackageInfo( 42 | context, 43 | targetPackageName, 44 | PackageManager.MATCH_UNINSTALLED_PACKAGES 45 | )?.applicationInfo 46 | if (null != applicationInfo) { 47 | appNameFlow.emit( 48 | applicationInfo.loadLabel(context.packageManager).toString() 49 | ) 50 | } else { 51 | appNameFlow.emit(targetPackageName) 52 | } 53 | } 54 | } 55 | } 56 | 57 | launch { 58 | ConfigHelper.configDataFlow.collect { 59 | whitelistFlow.value = ConfigHelper.configDataFlow 60 | .value.connectedApps[targetPackageName] ?: emptySet() 61 | val sharedUserIdMapNew = 62 | ConfigHelper.configDataFlow.value.sharedUserIdMap ?: emptyMap() 63 | sharedUserIdMap.clear() 64 | sharedUserIdMap.putAll(sharedUserIdMapNew) 65 | } 66 | } 67 | 68 | launch { 69 | AppHelper.allApps.collect { apps -> 70 | allAppListFlow.value = 71 | apps.filter { 72 | targetPackageName != it.packageName 73 | } 74 | .sortApps(context = context, toTopCollections = whitelistFlow.value) 75 | updateAppInfoListFlow() 76 | } 77 | } 78 | } 79 | } 80 | 81 | private fun updateAppInfoListFlow() { 82 | var appList = allAppListFlow.value 83 | val searchText = searchTextFlow.value 84 | val searchTextLowercase = searchText.lowercase(Locale.getDefault()) 85 | if (searchText.isNotEmpty()) { 86 | appList = appList.filter { 87 | it.isMatch(searchTextLowercase) 88 | } 89 | } 90 | if (showSystemAppsFlow.value) { 91 | appListFlow.setDifferentValue(appList) 92 | } else { 93 | val whitelist = whitelistFlow.value 94 | appListFlow.setDifferentValue(appList.filter { 95 | !it.isSystemApp || whitelist.contains(it.packageName) 96 | }) 97 | } 98 | } 99 | 100 | fun addApp2HiddenList(appInfo: AppInfo) { 101 | val sharedUserId = appInfo.sharedUserId 102 | if (!sharedUserId.isNullOrEmpty()) { 103 | sharedUserIdMap[appInfo.packageName] = sharedUserId 104 | } 105 | isModified = true 106 | val whitelistNew = whitelistFlow.value.toMutableSet() 107 | whitelistNew.add(appInfo.packageName) 108 | whitelistFlow.value = whitelistNew 109 | } 110 | 111 | fun removeApp2HiddenList(appInfo: AppInfo) { 112 | isModified = true 113 | val whitelistNew = whitelistFlow.value.toMutableSet() 114 | whitelistNew.remove(appInfo.packageName) 115 | whitelistFlow.value = whitelistNew 116 | 117 | if (appInfo.isSystemApp) { 118 | showSelectAll.value = false 119 | } 120 | } 121 | 122 | fun tryUpdateConfig() { 123 | val targetPackageName = targetPackageName 124 | if (isModified && !targetPackageName.isNullOrEmpty()) { 125 | val connectedAppsNew = ConfigHelper.configDataFlow.value.connectedApps.toMutableMap() 126 | connectedAppsNew[targetPackageName] = whitelistFlow.value 127 | ConfigHelper.updateConnectedApps(connectedAppsNew, sharedUserIdMap) 128 | isModified = false 129 | } 130 | } 131 | 132 | fun setShowSystemApps(showSystemApps: Boolean) { 133 | if (showSystemApps != showSystemAppsFlow.value) { 134 | showSystemAppsFlow.value = showSystemApps 135 | updateAppInfoListFlow() 136 | } 137 | } 138 | 139 | fun updateSearchText(searchText: String) { 140 | searchTextFlow.value = searchText 141 | updateAppInfoListFlow() 142 | } 143 | 144 | fun selectAllSystemApps(selectAll: Boolean) { 145 | if (selectAll && !showSystemAppsFlow.value) { 146 | setShowSystemApps(true) 147 | } 148 | val whitelistNew = whitelistFlow.value.toMutableSet() 149 | if (selectAll) { 150 | for (appInfo in appListFlow.value) { 151 | if (appInfo.isSystemApp && !whitelistNew.contains(appInfo.packageName)) { 152 | whitelistNew.add(appInfo.packageName) 153 | 154 | val targetSharedUserId = appInfo.sharedUserId 155 | if (!targetSharedUserId.isNullOrEmpty()) { 156 | sharedUserIdMap[appInfo.packageName] = targetSharedUserId 157 | } 158 | } 159 | } 160 | } else { 161 | for (appInfo in appListFlow.value) { 162 | if (appInfo.isSystemApp) { 163 | whitelistNew.remove(appInfo.packageName) 164 | } 165 | } 166 | } 167 | whitelistFlow.value = whitelistNew 168 | showSelectAll.value = selectAll 169 | isModified = true 170 | } 171 | } -------------------------------------------------------------------------------- /app/src/main/java/cn/geektang/privacyspace/ui/screen/setwhitelist/SetWhitelistScreen.kt: -------------------------------------------------------------------------------- 1 | package cn.geektang.privacyspace.ui.screen.setwhitelist 2 | 3 | import androidx.compose.foundation.layout.* 4 | import androidx.compose.foundation.lazy.LazyColumn 5 | import androidx.compose.foundation.lazy.items 6 | import androidx.compose.material.Text 7 | import androidx.compose.runtime.* 8 | import androidx.compose.ui.Modifier 9 | import androidx.compose.ui.res.stringResource 10 | import androidx.compose.ui.unit.dp 11 | import androidx.lifecycle.Lifecycle 12 | import androidx.lifecycle.viewmodel.compose.viewModel 13 | import cn.geektang.privacyspace.R 14 | import cn.geektang.privacyspace.bean.AppInfo 15 | import cn.geektang.privacyspace.ui.widget.* 16 | import cn.geektang.privacyspace.util.LocalNavHostController 17 | import cn.geektang.privacyspace.util.OnLifecycleEvent 18 | import com.google.accompanist.insets.navigationBarsPadding 19 | 20 | @Composable 21 | fun SetWhitelistScreen(viewModel: SetWhitelistViewModel = viewModel()) { 22 | val allAllList by viewModel.allAppListFlow.collectAsState() 23 | val isLoading = allAllList.isEmpty() 24 | val appList by viewModel.appListFlow.collectAsState() 25 | val whitelist by viewModel.whitelistFlow.collectAsState() 26 | val blindApps by viewModel.blindAppsFlow.collectAsState() 27 | val showSystemApps by viewModel.showSystemAppsFlow.collectAsState() 28 | val searchText by viewModel.searchTextFlow.collectAsState() 29 | val showSelectAll by viewModel.showSelectAll.collectAsState() 30 | val actions = object : SetWhitelistActions { 31 | override fun addApp2Whitelist(appInfo: AppInfo) { 32 | viewModel.addApp2Whitelist(appInfo) 33 | } 34 | 35 | override fun removeApp2Whitelist(appInfo: AppInfo) { 36 | viewModel.removeApp2Whitelist(appInfo) 37 | } 38 | 39 | override fun setSystemAppsVisible(showSystemApps: Boolean) { 40 | viewModel.setSystemAppsVisible(showSystemApps) 41 | } 42 | 43 | override fun onSearchTextChange(searchText: String) { 44 | viewModel.updateSearchText(searchText) 45 | } 46 | 47 | override fun selectAllSystemApps(selectAll: Boolean) { 48 | viewModel.selectAllSystemApps(selectAll) 49 | } 50 | } 51 | SetWhiteListContent( 52 | appList = appList, 53 | whitelist = whitelist, 54 | blindApps = blindApps, 55 | searchText = searchText, 56 | isLoading = isLoading, 57 | showSystemApps = showSystemApps, 58 | showSelectAll = showSelectAll, 59 | actions = actions 60 | ) 61 | OnLifecycleEvent(onEvent = { event -> 62 | if (event == Lifecycle.Event.ON_PAUSE 63 | || event == Lifecycle.Event.ON_STOP 64 | || event == Lifecycle.Event.ON_DESTROY 65 | ) { 66 | viewModel.tryUpdateConfig() 67 | } 68 | }) 69 | } 70 | 71 | @Composable 72 | private fun SetWhiteListContent( 73 | appList: List, 74 | whitelist: Set, 75 | blindApps: Set, 76 | searchText: String, 77 | isLoading: Boolean, 78 | showSystemApps: Boolean, 79 | showSelectAll: Boolean, 80 | actions: SetWhitelistActions 81 | ) { 82 | val navHostController = LocalNavHostController.current 83 | val isPopupMenuShow = remember { 84 | mutableStateOf(false) 85 | } 86 | SetWhitelistPopupMenu( 87 | isPopupMenuShow, 88 | showSystemApps, 89 | showSelectAll, 90 | onSystemAppsVisibleChange = { showSystemApps -> 91 | isPopupMenuShow.value = false 92 | actions.setSystemAppsVisible(showSystemApps) 93 | }, selectAllCallback = { selectAll -> 94 | actions.selectAllSystemApps(selectAll) 95 | }) 96 | Column { 97 | SearchTopBar( 98 | title = stringResource(R.string.set_white_list), 99 | searchText = searchText, 100 | onSearchTextChange = { 101 | actions.onSearchTextChange(it) 102 | }, showMorePopupState = isPopupMenuShow, 103 | onNavigationIconClick = { 104 | navHostController.popBackStack() 105 | }) 106 | LoadingBox(modifier = Modifier.fillMaxSize(), showLoading = isLoading) { 107 | LazyColumn(content = { 108 | items(appList) { appInfo -> 109 | AppItem(whitelist, blindApps, appInfo, actions) 110 | } 111 | item { 112 | Box(modifier = Modifier.navigationBarsPadding()) 113 | } 114 | }) 115 | } 116 | } 117 | } 118 | 119 | @Composable 120 | private fun AppItem( 121 | whitelist: Set, 122 | blindApps: Set, 123 | appInfo: AppInfo, 124 | actions: SetWhitelistActions 125 | ) { 126 | var showConfirmDialog by remember { 127 | mutableStateOf(false) 128 | } 129 | val isChecked = whitelist.contains(appInfo.packageName) 130 | AppInfoColumnItem(appInfo = appInfo, isChecked = isChecked) { 131 | if (!whitelist.contains(appInfo.packageName)) { 132 | if (blindApps.contains(appInfo.packageName)) { 133 | showConfirmDialog = true 134 | } else { 135 | actions.addApp2Whitelist(appInfo) 136 | } 137 | } else { 138 | actions.removeApp2Whitelist(appInfo) 139 | } 140 | } 141 | if (showConfirmDialog) { 142 | MessageDialog( 143 | text = { 144 | Text(text = stringResource(id = R.string.tips_cancel_blind, appInfo.appName)) 145 | }, onPositiveButtonClick = { 146 | actions.addApp2Whitelist(appInfo) 147 | showConfirmDialog = false 148 | }, onDismissRequest = { showConfirmDialog = false } 149 | ) 150 | } 151 | } 152 | 153 | @Composable 154 | private fun SetWhitelistPopupMenu( 155 | popupMenuShow: MutableState, 156 | showSystemApps: Boolean, 157 | showSelectAll: Boolean, 158 | onSystemAppsVisibleChange: (Boolean) -> Unit, 159 | selectAllCallback: (Boolean) -> Unit 160 | ) { 161 | PopupMenu(isShow = popupMenuShow) { 162 | Column( 163 | modifier = Modifier 164 | .padding(vertical = 5.dp) 165 | .width(IntrinsicSize.Max) 166 | ) { 167 | PopupCheckboxItem( 168 | text = stringResource(R.string.display_system_apps), 169 | checked = showSystemApps, 170 | onCheckedChange = onSystemAppsVisibleChange 171 | ) 172 | PopupCheckboxItem( 173 | text = stringResource(R.string.select_all_system_apps), 174 | checked = showSelectAll, 175 | onCheckedChange = selectAllCallback 176 | ) 177 | } 178 | } 179 | } 180 | 181 | interface SetWhitelistActions { 182 | fun addApp2Whitelist(appInfo: AppInfo) { 183 | } 184 | 185 | fun removeApp2Whitelist(appInfo: AppInfo) { 186 | } 187 | 188 | fun setSystemAppsVisible(showSystemApps: Boolean) { 189 | } 190 | 191 | fun onSearchTextChange(searchText: String) { 192 | 193 | } 194 | 195 | fun selectAllSystemApps(selectAll: Boolean) { 196 | 197 | } 198 | } -------------------------------------------------------------------------------- /app/src/main/java/cn/geektang/privacyspace/ui/screen/setwhitelist/SetWhitelistViewModel.kt: -------------------------------------------------------------------------------- 1 | package cn.geektang.privacyspace.ui.screen.setwhitelist 2 | 3 | import android.app.Application 4 | import androidx.lifecycle.AndroidViewModel 5 | import androidx.lifecycle.viewModelScope 6 | import cn.geektang.privacyspace.bean.AppInfo 7 | import cn.geektang.privacyspace.constant.ConfigConstant 8 | import cn.geektang.privacyspace.util.AppHelper 9 | import cn.geektang.privacyspace.util.AppHelper.isMatch 10 | import cn.geektang.privacyspace.util.AppHelper.sortApps 11 | import cn.geektang.privacyspace.util.ConfigHelper 12 | import cn.geektang.privacyspace.util.setDifferentValue 13 | import kotlinx.coroutines.flow.MutableStateFlow 14 | import kotlinx.coroutines.flow.collect 15 | import kotlinx.coroutines.launch 16 | import java.util.* 17 | 18 | class SetWhitelistViewModel(private val context: Application) : AndroidViewModel(context) { 19 | val allAppListFlow = MutableStateFlow>(emptyList()) 20 | val appListFlow = MutableStateFlow>(emptyList()) 21 | val whitelistFlow = MutableStateFlow>(emptySet()) 22 | val blindAppsFlow = MutableStateFlow>(emptySet()) 23 | val showSystemAppsFlow = MutableStateFlow(true) 24 | val showSelectAll = MutableStateFlow(false) 25 | val searchTextFlow = MutableStateFlow("") 26 | private val sharedUserIdMap = mutableMapOf() 27 | private var isModified = false 28 | 29 | init { 30 | viewModelScope.launch { 31 | launch { 32 | ConfigHelper.configDataFlow.collect { 33 | whitelistFlow.value = it.whitelist 34 | blindAppsFlow.value = it.blind ?: emptySet() 35 | 36 | val sharedUserIdMapNew = it.sharedUserIdMap ?: emptyMap() 37 | sharedUserIdMap.clear() 38 | sharedUserIdMap.putAll(sharedUserIdMapNew) 39 | } 40 | } 41 | launch { 42 | val defaultWhitelist = ConfigConstant.defaultWhitelist 43 | AppHelper.allApps.collect { apps -> 44 | allAppListFlow.value = apps.filter { app -> 45 | !defaultWhitelist.contains(app.packageName) 46 | }.sortApps(context = context, toTopCollections = whitelistFlow.value) 47 | updateAppInfoListFlow() 48 | } 49 | } 50 | } 51 | } 52 | 53 | fun addApp2Whitelist(appInfo: AppInfo) { 54 | val targetSharedUserId = appInfo.sharedUserId 55 | if (!targetSharedUserId.isNullOrEmpty()) { 56 | sharedUserIdMap[appInfo.packageName] = targetSharedUserId 57 | } 58 | 59 | val whitelist = whitelistFlow.value.toMutableSet() 60 | whitelist.add(appInfo.packageName) 61 | whitelistFlow.value = whitelist 62 | isModified = true 63 | } 64 | 65 | fun removeApp2Whitelist(appInfo: AppInfo) { 66 | val whitelist = whitelistFlow.value.toMutableSet() 67 | whitelist.remove(appInfo.packageName) 68 | whitelistFlow.value = whitelist 69 | if (appInfo.isSystemApp) { 70 | showSelectAll.value = false 71 | } 72 | isModified = true 73 | } 74 | 75 | private fun updateAppInfoListFlow() { 76 | var appList = allAppListFlow.value 77 | val searchText = searchTextFlow.value 78 | val searchTextLowercase = searchText.lowercase(Locale.getDefault()) 79 | if (searchText.isNotEmpty()) { 80 | appList = appList.filter { 81 | it.isMatch(searchTextLowercase) 82 | } 83 | } 84 | 85 | if (showSystemAppsFlow.value) { 86 | appListFlow.setDifferentValue(appList) 87 | } else { 88 | val whitelist = whitelistFlow.value 89 | appListFlow.setDifferentValue(appList.filter { 90 | !it.isSystemApp || whitelist.contains(it.packageName) 91 | }) 92 | } 93 | } 94 | 95 | fun tryUpdateConfig() { 96 | if (isModified) { 97 | val whitelist = whitelistFlow.value 98 | val blindApps = blindAppsFlow.value.toMutableSet() 99 | for (app in whitelist) { 100 | if (blindApps.contains(app)) { 101 | blindApps.remove(app) 102 | } 103 | } 104 | 105 | ConfigHelper.updateWhitelist(whitelistFlow.value, sharedUserIdMap, blindApps) 106 | isModified = false 107 | } 108 | } 109 | 110 | fun setSystemAppsVisible(showSystemApps: Boolean) { 111 | if (showSystemAppsFlow.value != showSystemApps) { 112 | showSystemAppsFlow.value = showSystemApps 113 | updateAppInfoListFlow() 114 | } 115 | } 116 | 117 | fun updateSearchText(searchText: String) { 118 | searchTextFlow.value = searchText 119 | updateAppInfoListFlow() 120 | } 121 | 122 | fun selectAllSystemApps(selectAll: Boolean) { 123 | if (selectAll && !showSystemAppsFlow.value) { 124 | setSystemAppsVisible(true) 125 | } 126 | val whitelistNew = whitelistFlow.value.toMutableSet() 127 | if (selectAll) { 128 | for (appInfo in appListFlow.value) { 129 | if (appInfo.isSystemApp && !whitelistNew.contains(appInfo.packageName)) { 130 | whitelistNew.add(appInfo.packageName) 131 | 132 | val targetSharedUserId = appInfo.sharedUserId 133 | if (!targetSharedUserId.isNullOrEmpty()) { 134 | sharedUserIdMap[appInfo.packageName] = targetSharedUserId 135 | } 136 | } 137 | } 138 | } else { 139 | for (appInfo in appListFlow.value) { 140 | if (appInfo.isSystemApp) { 141 | whitelistNew.remove(appInfo.packageName) 142 | } 143 | } 144 | } 145 | whitelistFlow.value = whitelistNew 146 | showSelectAll.value = selectAll 147 | isModified = true 148 | } 149 | } -------------------------------------------------------------------------------- /app/src/main/java/cn/geektang/privacyspace/ui/theme/Color.kt: -------------------------------------------------------------------------------- 1 | package cn.geektang.privacyspace.ui.theme 2 | 3 | import androidx.compose.ui.graphics.Color 4 | 5 | val Purple200 = Color(0xFFBB86FC) 6 | val Purple500 = Color(0xFF6200EE) 7 | val Purple700 = Color(0xFF3700B3) 8 | val Teal200 = Color(0xFF03DAC5) -------------------------------------------------------------------------------- /app/src/main/java/cn/geektang/privacyspace/ui/theme/Shape.kt: -------------------------------------------------------------------------------- 1 | package cn.geektang.privacyspace.ui.theme 2 | 3 | import androidx.compose.foundation.shape.RoundedCornerShape 4 | import androidx.compose.material.Shapes 5 | import androidx.compose.ui.unit.dp 6 | 7 | val Shapes = Shapes( 8 | small = RoundedCornerShape(2.dp), 9 | medium = RoundedCornerShape(4.dp), 10 | large = RoundedCornerShape(10.dp) 11 | ) -------------------------------------------------------------------------------- /app/src/main/java/cn/geektang/privacyspace/ui/theme/Theme.kt: -------------------------------------------------------------------------------- 1 | package cn.geektang.privacyspace.ui.theme 2 | 3 | import androidx.compose.foundation.isSystemInDarkTheme 4 | import androidx.compose.material.MaterialTheme 5 | import androidx.compose.material.darkColors 6 | import androidx.compose.material.lightColors 7 | import androidx.compose.runtime.Composable 8 | 9 | private val DarkColorPalette = darkColors( 10 | primary = Purple200, 11 | primaryVariant = Purple700, 12 | secondary = Teal200 13 | ) 14 | 15 | private val LightColorPalette = lightColors( 16 | primary = Purple500, 17 | primaryVariant = Purple700, 18 | secondary = Teal200 19 | 20 | /* Other default colors to override 21 | background = Color.White, 22 | surface = Color.White, 23 | onPrimary = Color.White, 24 | onSecondary = Color.Black, 25 | onBackground = Color.Black, 26 | onSurface = Color.Black, 27 | */ 28 | ) 29 | 30 | @Composable 31 | fun PrivacySpaceTheme(darkTheme: Boolean = isSystemInDarkTheme(), content: @Composable () -> Unit) { 32 | val colors = if (darkTheme) { 33 | DarkColorPalette 34 | } else { 35 | LightColorPalette 36 | } 37 | 38 | MaterialTheme( 39 | colors = colors, 40 | typography = Typography, 41 | shapes = Shapes, 42 | content = content 43 | ) 44 | } -------------------------------------------------------------------------------- /app/src/main/java/cn/geektang/privacyspace/ui/theme/Type.kt: -------------------------------------------------------------------------------- 1 | package cn.geektang.privacyspace.ui.theme 2 | 3 | import androidx.compose.material.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 | body1 = TextStyle( 12 | fontFamily = FontFamily.Default, 13 | fontWeight = FontWeight.Normal, 14 | fontSize = 16.sp 15 | ) 16 | /* Other default text styles to override 17 | button = TextStyle( 18 | fontFamily = FontFamily.Default, 19 | fontWeight = FontWeight.W500, 20 | fontSize = 14.sp 21 | ), 22 | caption = TextStyle( 23 | fontFamily = FontFamily.Default, 24 | fontWeight = FontWeight.Normal, 25 | fontSize = 12.sp 26 | ) 27 | */ 28 | ) -------------------------------------------------------------------------------- /app/src/main/java/cn/geektang/privacyspace/ui/widget/AppInfoColumnItem.kt: -------------------------------------------------------------------------------- 1 | package cn.geektang.privacyspace.ui.widget 2 | 3 | import android.content.pm.ApplicationInfo 4 | import android.graphics.drawable.ColorDrawable 5 | import androidx.compose.foundation.Image 6 | import androidx.compose.foundation.clickable 7 | import androidx.compose.foundation.layout.* 8 | import androidx.compose.material.Checkbox 9 | import androidx.compose.material.MaterialTheme 10 | import androidx.compose.material.Text 11 | import androidx.compose.runtime.Composable 12 | import androidx.compose.ui.Alignment 13 | import androidx.compose.ui.Modifier 14 | import androidx.compose.ui.platform.LocalContext 15 | import androidx.compose.ui.tooling.preview.Preview 16 | import androidx.compose.ui.unit.dp 17 | import cn.geektang.privacyspace.BuildConfig 18 | import cn.geektang.privacyspace.R 19 | import cn.geektang.privacyspace.bean.AppInfo 20 | import coil.compose.rememberImagePainter 21 | import com.google.accompanist.flowlayout.FlowRow 22 | 23 | @Composable 24 | fun AppInfoColumnItem( 25 | appInfo: AppInfo, 26 | isChecked: Boolean, 27 | customButtons: List<(@Composable () -> Unit)> = emptyList(), 28 | onClick: () -> Unit 29 | ) { 30 | Row( 31 | modifier = Modifier 32 | .clickable(onClick = onClick) 33 | .padding(horizontal = 15.dp, vertical = 10.dp), 34 | verticalAlignment = Alignment.CenterVertically 35 | ) { 36 | Image( 37 | modifier = Modifier 38 | .size(48.dp), 39 | painter = rememberImagePainter(data = appInfo.appIcon), 40 | contentDescription = appInfo.appName 41 | ) 42 | Column( 43 | modifier = Modifier 44 | .weight(1f) 45 | .padding(horizontal = 10.dp) 46 | ) { 47 | Column( 48 | modifier = Modifier 49 | .padding(horizontal = 5.dp) 50 | ) { 51 | Text(text = appInfo.appName, style = MaterialTheme.typography.subtitle1) 52 | Text(text = appInfo.packageName, style = MaterialTheme.typography.body2) 53 | } 54 | val chipTexts = mutableListOf() 55 | if (appInfo.isSystemApp) { 56 | chipTexts.add("SystemApp") 57 | } 58 | if (appInfo.isXposedModule) { 59 | chipTexts.add("XposedModule") 60 | } 61 | if (!appInfo.sharedUserId.isNullOrEmpty()) { 62 | chipTexts.add(appInfo.sharedUserId) 63 | } 64 | val showSetConnectButton = customButtons.isNotEmpty() 65 | if (chipTexts.isNotEmpty() || showSetConnectButton) { 66 | FlowRow( 67 | modifier = Modifier 68 | .fillMaxWidth() 69 | .padding(all = 2.5.dp) 70 | ) { 71 | for (customButton in customButtons) { 72 | customButton() 73 | } 74 | 75 | for (chipText in chipTexts) { 76 | Chip( 77 | modifier = Modifier.padding(all = 2.5.dp), 78 | text = chipText 79 | ) 80 | } 81 | } 82 | } 83 | } 84 | Checkbox(checked = isChecked, onCheckedChange = { 85 | onClick() 86 | }) 87 | } 88 | } 89 | 90 | @Preview(showBackground = true) 91 | @Composable 92 | fun AppInfoColumnItemPreview() { 93 | val context = LocalContext.current 94 | val appInfo = AppInfo( 95 | appIcon = ColorDrawable(), 96 | packageName = BuildConfig.APPLICATION_ID, 97 | appName = context.getString(R.string.app_name), 98 | sharedUserId = null, 99 | isXposedModule = true, 100 | isSystemApp = true, 101 | applicationInfo = ApplicationInfo() 102 | ) 103 | AppInfoColumnItem(appInfo = appInfo, isChecked = false, onClick = {}) 104 | } -------------------------------------------------------------------------------- /app/src/main/java/cn/geektang/privacyspace/ui/widget/Chip.kt: -------------------------------------------------------------------------------- 1 | package cn.geektang.privacyspace.ui.widget 2 | 3 | import androidx.compose.foundation.background 4 | import androidx.compose.foundation.layout.padding 5 | import androidx.compose.foundation.shape.RoundedCornerShape 6 | import androidx.compose.material.MaterialTheme 7 | import androidx.compose.material.Text 8 | import androidx.compose.runtime.Composable 9 | import androidx.compose.ui.Modifier 10 | import androidx.compose.ui.graphics.Color 11 | import androidx.compose.ui.tooling.preview.Preview 12 | import androidx.compose.ui.unit.dp 13 | 14 | @Composable 15 | fun Chip( 16 | modifier: Modifier = Modifier, 17 | text: String, 18 | color: Color = MaterialTheme.colors.secondary 19 | ) { 20 | Text( 21 | modifier = modifier 22 | .background( 23 | color = color, 24 | shape = RoundedCornerShape(50) 25 | ) 26 | .padding(horizontal = 10.dp, vertical = 5.dp), 27 | text = text, 28 | color = Color.White, 29 | style = MaterialTheme.typography.caption 30 | ) 31 | } 32 | 33 | @Preview 34 | @Composable 35 | fun ChipPreview() { 36 | Chip(text = "XposedModule") 37 | } -------------------------------------------------------------------------------- /app/src/main/java/cn/geektang/privacyspace/ui/widget/Dialog.kt: -------------------------------------------------------------------------------- 1 | package cn.geektang.privacyspace.ui.widget 2 | 3 | import androidx.compose.foundation.layout.Arrangement 4 | import androidx.compose.foundation.layout.Row 5 | import androidx.compose.foundation.layout.fillMaxWidth 6 | import androidx.compose.foundation.layout.padding 7 | import androidx.compose.material.AlertDialog 8 | import androidx.compose.material.MaterialTheme 9 | import androidx.compose.material.Text 10 | import androidx.compose.material.TextButton 11 | import androidx.compose.runtime.Composable 12 | import androidx.compose.ui.Modifier 13 | import androidx.compose.ui.res.stringResource 14 | import androidx.compose.ui.unit.dp 15 | import androidx.compose.ui.window.DialogProperties 16 | import cn.geektang.privacyspace.R 17 | import kotlin.system.exitProcess 18 | 19 | @Composable 20 | fun MessageDialog( 21 | title: (@Composable () -> Unit)? = null, 22 | text: (@Composable () -> Unit)? = null, 23 | onDismissRequest: () -> Unit, 24 | properties: DialogProperties = DialogProperties(), 25 | positiveButtonText: String = stringResource(id = R.string.confirm), 26 | negativeButtonText: String = stringResource(id = R.string.cancel), 27 | onPositiveButtonClick: () -> Unit = onDismissRequest, 28 | onNegativeButtonClick: () -> Unit = onDismissRequest 29 | ) { 30 | AlertDialog( 31 | onDismissRequest = onDismissRequest, 32 | properties = properties, 33 | buttons = { 34 | Row( 35 | modifier = Modifier 36 | .fillMaxWidth() 37 | .padding(horizontal = 15.dp), 38 | horizontalArrangement = Arrangement.End 39 | ) { 40 | TextButton(onClick = onNegativeButtonClick) { 41 | Text( 42 | text = negativeButtonText, 43 | color = MaterialTheme.colors.secondary 44 | ) 45 | } 46 | TextButton(onClick = onPositiveButtonClick) { 47 | Text( 48 | text = positiveButtonText, 49 | color = MaterialTheme.colors.primary 50 | ) 51 | } 52 | } 53 | }, 54 | text = text, 55 | title = title 56 | ) 57 | } 58 | 59 | @Composable 60 | fun NoticeDialog( 61 | text: String, 62 | onDismissRequest: () -> Unit, 63 | onPositiveButtonClick: () -> Unit = onDismissRequest 64 | ) { 65 | MessageDialog( 66 | title = { 67 | Text(text = stringResource(R.string.tips)) 68 | }, 69 | text = { 70 | Text(text = text) 71 | }, 72 | properties = DialogProperties( 73 | dismissOnBackPress = false, 74 | dismissOnClickOutside = false 75 | ), 76 | negativeButtonText = stringResource(id = R.string.launcher_notice_cancel), 77 | positiveButtonText = stringResource(id = R.string.launcher_notice_confirm), 78 | onNegativeButtonClick = { 79 | exitProcess(0) 80 | }, 81 | onPositiveButtonClick = onPositiveButtonClick, 82 | onDismissRequest = onDismissRequest 83 | ) 84 | } -------------------------------------------------------------------------------- /app/src/main/java/cn/geektang/privacyspace/ui/widget/LoadingBox.kt: -------------------------------------------------------------------------------- 1 | package cn.geektang.privacyspace.ui.widget 2 | 3 | import androidx.compose.foundation.layout.Box 4 | import androidx.compose.foundation.layout.Column 5 | import androidx.compose.foundation.layout.fillMaxSize 6 | import androidx.compose.foundation.layout.padding 7 | import androidx.compose.foundation.lazy.LazyColumn 8 | import androidx.compose.foundation.lazy.items 9 | import androidx.compose.material.CircularProgressIndicator 10 | import androidx.compose.material.Text 11 | import androidx.compose.runtime.Composable 12 | import androidx.compose.ui.Alignment 13 | import androidx.compose.ui.Modifier 14 | import androidx.compose.ui.res.stringResource 15 | import androidx.compose.ui.unit.dp 16 | import cn.geektang.privacyspace.R 17 | 18 | @Composable 19 | fun LoadingBox( 20 | modifier: Modifier = Modifier, 21 | showLoading: Boolean, 22 | content: @Composable () -> Unit 23 | ) { 24 | Box(modifier = modifier) { 25 | content() 26 | if (showLoading) { 27 | Column( 28 | modifier = Modifier.align(Alignment.Center), 29 | horizontalAlignment = Alignment.CenterHorizontally 30 | ) { 31 | CircularProgressIndicator() 32 | Text( 33 | modifier = Modifier.padding(top = 5.dp), 34 | text = stringResource(R.string.loading) 35 | ) 36 | } 37 | } 38 | } 39 | } -------------------------------------------------------------------------------- /app/src/main/java/cn/geektang/privacyspace/ui/widget/Popup.kt: -------------------------------------------------------------------------------- 1 | package cn.geektang.privacyspace.ui.widget 2 | 3 | import androidx.compose.foundation.clickable 4 | import androidx.compose.foundation.layout.* 5 | import androidx.compose.foundation.shape.RoundedCornerShape 6 | import androidx.compose.material.Checkbox 7 | import androidx.compose.material.Surface 8 | import androidx.compose.material.Text 9 | import androidx.compose.runtime.Composable 10 | import androidx.compose.runtime.MutableState 11 | import androidx.compose.ui.Alignment 12 | import androidx.compose.ui.Modifier 13 | import androidx.compose.ui.unit.dp 14 | import androidx.compose.ui.window.Popup 15 | import androidx.compose.ui.window.PopupProperties 16 | 17 | @Composable 18 | fun PopupMenu(isShow: MutableState, content: @Composable () -> Unit) { 19 | Popup( 20 | alignment = Alignment.TopEnd, 21 | properties = PopupProperties(focusable = isShow.value), 22 | onDismissRequest = { isShow.value = false }) { 23 | if (isShow.value) { 24 | Surface( 25 | modifier = Modifier 26 | .padding(end = 5.dp, top = 10.dp), 27 | elevation = 3.dp, 28 | shape = RoundedCornerShape(5.dp) 29 | ) { 30 | content() 31 | } 32 | } 33 | } 34 | } 35 | 36 | @Composable 37 | fun PopupItem(text: String, onClick: () -> Unit) { 38 | Text( 39 | modifier = Modifier 40 | .clickable(onClick = onClick) 41 | .padding(horizontal = 15.dp, vertical = 10.dp) 42 | .fillMaxWidth(), 43 | maxLines = 1, 44 | text = text 45 | ) 46 | } 47 | 48 | @Composable 49 | fun PopupCheckboxItem(text: String, checked: Boolean, onCheckedChange: (Boolean) -> Unit) { 50 | Row( 51 | modifier = Modifier 52 | .clickable { 53 | onCheckedChange(!checked) 54 | } 55 | .fillMaxWidth(), verticalAlignment = Alignment.CenterVertically 56 | ) { 57 | Checkbox(checked, onCheckedChange = { checked -> 58 | onCheckedChange(checked) 59 | }) 60 | Text( 61 | maxLines = 1, 62 | text = text 63 | ) 64 | Spacer(modifier = Modifier.width(15.dp)) 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /app/src/main/java/cn/geektang/privacyspace/ui/widget/TopBar.kt: -------------------------------------------------------------------------------- 1 | package cn.geektang.privacyspace.ui.widget 2 | 3 | import androidx.compose.foundation.layout.Box 4 | import androidx.compose.foundation.layout.RowScope 5 | import androidx.compose.material.* 6 | import androidx.compose.material.icons.Icons 7 | import androidx.compose.material.icons.filled.ArrowBack 8 | import androidx.compose.material.icons.filled.Clear 9 | import androidx.compose.material.icons.filled.MoreVert 10 | import androidx.compose.material.icons.filled.Search 11 | import androidx.compose.runtime.* 12 | import androidx.compose.ui.Modifier 13 | import androidx.compose.ui.focus.FocusRequester 14 | import androidx.compose.ui.focus.focusRequester 15 | import androidx.compose.ui.graphics.Color 16 | import androidx.compose.ui.res.stringResource 17 | import androidx.compose.ui.unit.dp 18 | import cn.geektang.privacyspace.R 19 | import com.google.accompanist.insets.statusBarsPadding 20 | import kotlinx.coroutines.delay 21 | import kotlinx.coroutines.launch 22 | 23 | @Composable 24 | fun TopBar( 25 | title: String, 26 | actions: @Composable RowScope.() -> Unit = {}, 27 | showNavigationIcon: Boolean = true, 28 | onNavigationIconClick: (() -> Unit)? = null 29 | ) { 30 | var navigationIcon: @Composable (() -> Unit)? = null 31 | if (showNavigationIcon) { 32 | navigationIcon = { 33 | NavigationIcon(onNavigationIconClick) 34 | } 35 | } 36 | 37 | Surface( 38 | color = MaterialTheme.colors.primarySurface, 39 | elevation = AppBarDefaults.TopAppBarElevation 40 | ) { 41 | Box(modifier = Modifier.statusBarsPadding()) { 42 | TopAppBar( 43 | title = { 44 | Text(text = title) 45 | }, 46 | actions = actions, 47 | backgroundColor = Color.Transparent, 48 | elevation = 0.dp, 49 | navigationIcon = navigationIcon 50 | ) 51 | } 52 | } 53 | } 54 | 55 | @Composable 56 | private fun NavigationIcon(onNavigationIconClick: (() -> Unit)?) { 57 | IconButton(onClick = { 58 | onNavigationIconClick?.invoke() 59 | }) { 60 | Icon( 61 | imageVector = Icons.Default.ArrowBack, 62 | contentDescription = stringResource(R.string.back) 63 | ) 64 | } 65 | } 66 | 67 | @Composable 68 | fun SearchTopBar( 69 | title: String, 70 | searchText: String, 71 | onSearchTextChange: (String) -> Unit, 72 | showMorePopupState: MutableState, 73 | onNavigationIconClick: (() -> Unit)? = null 74 | ) { 75 | Surface( 76 | color = MaterialTheme.colors.primarySurface, 77 | elevation = AppBarDefaults.TopAppBarElevation 78 | ) { 79 | Box(modifier = Modifier.statusBarsPadding()) { 80 | SearchTopBarInner( 81 | title = title, 82 | searchText = searchText, 83 | onSearchTextChange = onSearchTextChange, 84 | showMorePopupState = showMorePopupState, 85 | onNavigationIconClick = onNavigationIconClick 86 | ) 87 | } 88 | } 89 | } 90 | 91 | @Composable 92 | private fun SearchTopBarInner( 93 | title: String, 94 | searchText: String, 95 | onSearchTextChange: (String) -> Unit, 96 | showMorePopupState: MutableState, 97 | onNavigationIconClick: (() -> Unit)? 98 | ) { 99 | var showSearchBox by remember { 100 | mutableStateOf(false) 101 | } 102 | val focusRequester = FocusRequester() 103 | TopAppBar( 104 | title = { 105 | if (!showSearchBox) { 106 | Text(text = title) 107 | } 108 | }, 109 | actions = { 110 | if (showSearchBox) { 111 | SearchBoxTextField(focusRequester, searchText, onSearchTextChange) 112 | } 113 | 114 | val scope = rememberCoroutineScope() 115 | if (!showSearchBox) { 116 | IconButton(onClick = { 117 | showSearchBox = true 118 | scope.launch { 119 | delay(100) 120 | focusRequester.smartRequestFocus() 121 | } 122 | }) { 123 | Icon( 124 | imageVector = Icons.Default.Search, 125 | contentDescription = stringResource(R.string.menu) 126 | ) 127 | } 128 | } 129 | 130 | IconButton(onClick = { 131 | showMorePopupState.value = true 132 | }) { 133 | Icon( 134 | imageVector = Icons.Default.MoreVert, 135 | contentDescription = stringResource(R.string.menu) 136 | ) 137 | } 138 | }, 139 | navigationIcon = { 140 | IconButton(onClick = { 141 | if (showSearchBox) { 142 | if (searchText.isNotEmpty()) { 143 | onSearchTextChange("") 144 | } 145 | showSearchBox = false 146 | } else { 147 | onNavigationIconClick?.invoke() 148 | } 149 | }) { 150 | Icon( 151 | imageVector = Icons.Default.ArrowBack, 152 | contentDescription = stringResource(R.string.back) 153 | ) 154 | } 155 | }, 156 | backgroundColor = Color.Transparent, 157 | elevation = 0.dp, 158 | ) 159 | } 160 | 161 | @Composable 162 | private fun RowScope.SearchBoxTextField( 163 | focusRequester: FocusRequester, 164 | searchText: String, 165 | onSearchTextChange: (String) -> Unit 166 | ) { 167 | TextField( 168 | modifier = Modifier 169 | .focusRequester(focusRequester) 170 | .weight(1f), 171 | placeholder = { 172 | Text(text = stringResource(R.string.search)) 173 | }, 174 | singleLine = true, 175 | colors = TextFieldDefaults.textFieldColors( 176 | backgroundColor = Color.Transparent, 177 | focusedIndicatorColor = Color.Transparent, 178 | unfocusedIndicatorColor = Color.Transparent, 179 | cursorColor = MaterialTheme.colors.secondary, 180 | textColor = Color.White, 181 | placeholderColor = Color(0xffcccccc) 182 | ), 183 | trailingIcon = { 184 | if (searchText.isNotEmpty()) { 185 | IconButton(onClick = { onSearchTextChange("") }) { 186 | Icon( 187 | imageVector = Icons.Default.Clear, 188 | tint = Color(0xccffffff), 189 | contentDescription = stringResource(R.string.clear) 190 | ) 191 | } 192 | } 193 | }, 194 | value = searchText, 195 | onValueChange = onSearchTextChange 196 | ) 197 | } 198 | 199 | private suspend fun FocusRequester.smartRequestFocus() { 200 | try { 201 | requestFocus() 202 | } catch (e: Throwable) { 203 | delay(100) 204 | } 205 | } -------------------------------------------------------------------------------- /app/src/main/java/cn/geektang/privacyspace/util/AppHelper.kt: -------------------------------------------------------------------------------- 1 | package cn.geektang.privacyspace.util 2 | 3 | import android.content.BroadcastReceiver 4 | import android.content.Context 5 | import android.content.Intent 6 | import android.content.IntentFilter 7 | import android.content.pm.ApplicationInfo 8 | import android.content.pm.PackageInfo 9 | import android.content.pm.PackageManager 10 | import android.net.Uri 11 | import androidx.collection.ArrayMap 12 | import cn.geektang.privacyspace.BuildConfig 13 | import cn.geektang.privacyspace.R 14 | import cn.geektang.privacyspace.bean.AppInfo 15 | import com.microsoft.appcenter.analytics.Analytics 16 | import kotlinx.coroutines.* 17 | import kotlinx.coroutines.flow.Flow 18 | import kotlinx.coroutines.flow.MutableStateFlow 19 | import java.util.* 20 | 21 | object AppHelper { 22 | private val _allApps = MutableStateFlow>(emptyList()) 23 | val allApps: Flow> = _allApps 24 | private var getAppsRetryTimes = 0 25 | 26 | suspend fun initialize(context: Context) { 27 | val apps = try { 28 | val result = context.loadAllAppList() 29 | getAppsRetryTimes = 0 30 | result 31 | } catch (ignored: CancellationException) { 32 | return 33 | } catch (e: Exception) { 34 | val properties = ArrayMap() 35 | properties["exception"] = e.javaClass.name 36 | Analytics.trackEvent("ReadAppsFailed", properties) 37 | getAppsRetryTimes++ 38 | 39 | e.printStackTrace() 40 | if (getAppsRetryTimes == 3) { 41 | context.showToast(R.string.tips_get_apps_failed) 42 | } 43 | delay(1000) 44 | // retry after 1 seconds 45 | initialize(context) 46 | return 47 | } 48 | _allApps.emit(apps) 49 | } 50 | 51 | private suspend fun Context.loadAllAppList(): List { 52 | return withContext(Dispatchers.IO) { 53 | val packageManager = packageManager 54 | return@withContext packageManager 55 | .getInstalledPackages(PackageManager.MATCH_UNINSTALLED_PACKAGES or PackageManager.GET_META_DATA) 56 | .mapNotNull { packageInfo -> 57 | val applicationInfo = packageInfo.applicationInfo 58 | val appName = applicationInfo.loadLabel(packageManager).toString() 59 | val appIcon = applicationInfo.loadIcon(packageManager) 60 | AppInfo( 61 | applicationInfo = applicationInfo, 62 | packageName = applicationInfo.packageName, 63 | appName = appName, 64 | appIcon = appIcon, 65 | sharedUserId = packageInfo.sharedUserId, 66 | isSystemApp = isSystemApp(applicationInfo), 67 | isXposedModule = applicationInfo.isXposedModule() 68 | ) 69 | } 70 | } 71 | } 72 | 73 | suspend fun List.sortApps( 74 | context: Context, 75 | toTopCollections: Collection 76 | ): List { 77 | return withContext(Dispatchers.Default) { 78 | val applicationInfoComparator = 79 | ApplicationInfo.DisplayNameComparator(context.packageManager) 80 | return@withContext sortedWith(Comparator { t, t2 -> 81 | val isContainsT = toTopCollections.contains(t.packageName) 82 | val isContainsT2 = toTopCollections.contains(t2.packageName) 83 | if (isContainsT && !isContainsT2) { 84 | return@Comparator -1 85 | } else if (!isContainsT && isContainsT2) { 86 | return@Comparator 1 87 | } 88 | if (t.isXposedModule && !t2.isXposedModule) { 89 | return@Comparator -1 90 | } else if (!t.isXposedModule && t2.isXposedModule) { 91 | return@Comparator 1 92 | } 93 | return@Comparator applicationInfoComparator.compare( 94 | t.applicationInfo, 95 | t2.applicationInfo 96 | ) 97 | }) 98 | } 99 | } 100 | 101 | fun isSystemApp(applicationInfo: ApplicationInfo): Boolean { 102 | return (applicationInfo.flags and ApplicationInfo.FLAG_SYSTEM) == ApplicationInfo.FLAG_SYSTEM 103 | } 104 | 105 | fun Context.getLauncherPackageName(): String? { 106 | val intent = Intent(Intent.ACTION_MAIN) 107 | intent.addCategory(Intent.CATEGORY_HOME) 108 | val res = packageManager.resolveActivity(intent, 0) 109 | if (res?.activityInfo == null) { 110 | return null 111 | } 112 | return if (res.activityInfo.packageName == "android") { 113 | null 114 | } else { 115 | res.activityInfo.packageName 116 | } 117 | } 118 | 119 | fun Context.getApkInstallerPackageName(): String? { 120 | val uri = Uri.fromParts("package", BuildConfig.APPLICATION_ID, null) 121 | val intent = Intent(Intent.ACTION_DELETE, uri) 122 | val res = packageManager.resolveActivity(intent, PackageManager.MATCH_UNINSTALLED_PACKAGES) 123 | if (res?.activityInfo == null) { 124 | return null 125 | } 126 | return if (res.activityInfo.packageName == "android") { 127 | null 128 | } else { 129 | res.activityInfo.packageName 130 | } 131 | } 132 | 133 | fun getXposedModuleScopeList(context: Context, app: ApplicationInfo): List { 134 | val pm = context.packageManager 135 | val scopeList = mutableListOf() 136 | try { 137 | val scopeListResourceId: Int = app.metaData.getInt("xposedscope") 138 | if (scopeListResourceId != 0) { 139 | scopeList.addAll( 140 | pm.getResourcesForApplication(app).getStringArray(scopeListResourceId) 141 | ) 142 | } else { 143 | val scopeListString: String? = app.metaData.getString("xposedscope") 144 | if (scopeListString != null) { 145 | scopeList.addAll(scopeListString.split(";")) 146 | } 147 | } 148 | } catch (e: Exception) { 149 | e.printStackTrace() 150 | } 151 | return scopeList 152 | } 153 | 154 | fun getPackageInfo(context: Context, packageName: String, flag: Int = 0): PackageInfo? { 155 | return try { 156 | context.packageManager.getPackageInfo( 157 | packageName, 158 | flag 159 | ) 160 | } catch (e: PackageManager.NameNotFoundException) { 161 | null 162 | } 163 | } 164 | 165 | fun ApplicationInfo.isXposedModule(): Boolean { 166 | return metaData?.getBoolean("xposedmodule") == true || 167 | metaData?.containsKey("xposedminversion") == true 168 | } 169 | 170 | fun startWatchingAppsCountChange( 171 | context: Context, 172 | onAppRemoved: (packageName: String) -> Unit 173 | ) { 174 | val packageFilter = IntentFilter() 175 | packageFilter.addAction(Intent.ACTION_PACKAGE_FULLY_REMOVED) 176 | packageFilter.addAction(Intent.ACTION_PACKAGE_ADDED) 177 | packageFilter.addDataScheme("package") 178 | val receiver = object : BroadcastReceiver() { 179 | val scope = MainScope() 180 | override fun onReceive(cotext: Context, intent: Intent) { 181 | val packageName = intent.dataString?.substringAfter("package:") ?: return 182 | 183 | when (intent.action) { 184 | Intent.ACTION_PACKAGE_FULLY_REMOVED -> { 185 | onAppRemoved(packageName) 186 | scope.launch { 187 | val apps = _allApps.value.toMutableList().filter { 188 | it.packageName != packageName 189 | } 190 | _allApps.emit(apps) 191 | } 192 | } 193 | Intent.ACTION_PACKAGE_ADDED -> { 194 | val appInfo = try { 195 | getAppInfo(context, packageName) ?: return 196 | } catch (e: PackageManager.NameNotFoundException) { 197 | e.printStackTrace() 198 | return 199 | } 200 | // switch to ui thread 201 | scope.launch { 202 | val apps = _allApps.value.toMutableList() 203 | apps.add(appInfo) 204 | _allApps.emit(apps) 205 | } 206 | } 207 | else -> {} 208 | } 209 | } 210 | } 211 | context.applicationContext.registerReceiver(receiver, packageFilter) 212 | } 213 | 214 | fun AppInfo.isMatch(searchTextLowercase: String): Boolean { 215 | return packageName.lowercase(Locale.getDefault()).contains(searchTextLowercase) 216 | || appName.lowercase(Locale.getDefault()).contains(searchTextLowercase) 217 | || (sharedUserId?.lowercase(Locale.getDefault()) ?: "").contains(searchTextLowercase) 218 | } 219 | 220 | @Throws(PackageManager.NameNotFoundException::class) 221 | private fun getAppInfo(context: Context, packageName: String): AppInfo? { 222 | val packageManager = context.packageManager 223 | val packageInfo = 224 | getPackageInfo(context, packageName, PackageManager.GET_META_DATA) ?: return null 225 | val applicationInfo = packageInfo.applicationInfo 226 | val appName = applicationInfo.loadLabel(packageManager).toString() 227 | val appIcon = applicationInfo.loadIcon(packageManager) 228 | return AppInfo( 229 | applicationInfo = applicationInfo, 230 | packageName = applicationInfo.packageName, 231 | appName = appName, 232 | appIcon = appIcon, 233 | sharedUserId = packageInfo.sharedUserId, 234 | isSystemApp = isSystemApp(applicationInfo), 235 | isXposedModule = packageInfo.applicationInfo.isXposedModule() 236 | ) 237 | } 238 | } -------------------------------------------------------------------------------- /app/src/main/java/cn/geektang/privacyspace/util/ClassLoader.kt: -------------------------------------------------------------------------------- 1 | package cn.geektang.privacyspace.util 2 | 3 | @Throws(ClassNotFoundException::class) 4 | fun ClassLoader.tryLoadClass(name: String): Class<*> { 5 | return loadClass(name) ?: throw ClassNotFoundException() 6 | } 7 | 8 | fun ClassLoader.loadClassSafe(name: String): Class<*>? { 9 | return try { 10 | tryLoadClass(name) 11 | } catch (e: ClassNotFoundException) { 12 | null 13 | } 14 | } -------------------------------------------------------------------------------- /app/src/main/java/cn/geektang/privacyspace/util/ConfigClient.kt: -------------------------------------------------------------------------------- 1 | package cn.geektang.privacyspace.util 2 | 3 | import android.content.Context 4 | import android.util.Log 5 | import cn.geektang.privacyspace.bean.ConfigData 6 | import cn.geektang.privacyspace.bean.SystemUserInfo 7 | import kotlinx.coroutines.Dispatchers 8 | import kotlinx.coroutines.withContext 9 | 10 | class ConfigClient(context: Context) { 11 | private val packageManager = context.packageManager 12 | 13 | fun serverVersion(): Int { 14 | return connectServer(ConfigServer.QUERY_SERVER_VERSION)?.toIntOrNull() ?: -1 15 | } 16 | 17 | fun rebootTheSystem() { 18 | connectServer(ConfigServer.REBOOT_THE_SYSTEM) 19 | } 20 | 21 | suspend fun migrateOldConfig() { 22 | withContext(Dispatchers.IO) { 23 | connectServer(ConfigServer.MIGRATE_OLD_CONFIG_FILE) 24 | } 25 | } 26 | 27 | suspend fun queryConfig(): ConfigData? { 28 | return withContext(Dispatchers.IO) { 29 | val configJson = connectServer(ConfigServer.QUERY_CONFIG) 30 | if (configJson.isNullOrBlank()) { 31 | return@withContext null 32 | } 33 | return@withContext try { 34 | JsonHelper.configAdapter().fromJson(configJson) 35 | } catch (e: Exception) { 36 | e.printStackTrace() 37 | Log.d("PrivacySpace", "Config is invalid.") 38 | null 39 | } 40 | } 41 | } 42 | 43 | suspend fun updateConfig(configData: ConfigData) { 44 | withContext(Dispatchers.IO) { 45 | val configJson = JsonHelper.configAdapter().toJson(configData) 46 | connectServer("${ConfigServer.UPDATE_CONFIG}$configJson") 47 | } 48 | } 49 | 50 | fun forceStop(packageName: String): Boolean { 51 | return connectServer("${ConfigServer.FORCE_STOP}$packageName") == ConfigServer.EXEC_SUCCEED 52 | } 53 | 54 | suspend fun querySystemUserList(): List? { 55 | return withContext(Dispatchers.IO) { 56 | val userListJson = connectServer(ConfigServer.GET_USERS) 57 | try { 58 | JsonHelper.systemUserInfoListAdapter().fromJson(userListJson) 59 | } catch (e: Exception) { 60 | null 61 | } 62 | } 63 | } 64 | 65 | private fun connectServer(methodName: String): String? { 66 | return try { 67 | packageManager.getInstallerPackageName(methodName) 68 | } catch (e: Exception) { 69 | null 70 | } 71 | } 72 | } -------------------------------------------------------------------------------- /app/src/main/java/cn/geektang/privacyspace/util/ConfigHelper.kt: -------------------------------------------------------------------------------- 1 | package cn.geektang.privacyspace.util 2 | 3 | import android.content.Context 4 | import android.content.pm.ResolveInfo 5 | import cn.geektang.privacyspace.BuildConfig 6 | import cn.geektang.privacyspace.bean.ConfigData 7 | import cn.geektang.privacyspace.bean.SystemUserInfo 8 | import cn.geektang.privacyspace.constant.ConfigConstant 9 | import cn.geektang.privacyspace.util.AppHelper.getApkInstallerPackageName 10 | import kotlinx.coroutines.Dispatchers 11 | import kotlinx.coroutines.MainScope 12 | import kotlinx.coroutines.flow.MutableStateFlow 13 | import kotlinx.coroutines.launch 14 | import kotlinx.coroutines.withContext 15 | import java.io.File 16 | import java.io.FileNotFoundException 17 | 18 | object ConfigHelper { 19 | const val LOADING_STATUS_INIT = 0 20 | const val LOADING_STATUS_LOADING = 1 21 | const val LOADING_STATUS_SUCCESSFUL = 2 22 | const val LOADING_STATUS_FAILED = 3 23 | 24 | private lateinit var configClient: ConfigClient 25 | private val scope = MainScope() 26 | val loadingStatusFlow = MutableStateFlow(LOADING_STATUS_INIT) 27 | val configDataFlow = MutableStateFlow(ConfigData.EMPTY) 28 | 29 | @Volatile 30 | private var isServerStart: Boolean = false 31 | 32 | fun initConfig(context: Context) { 33 | if (!::configClient.isInitialized) { 34 | configClient = ConfigClient(context.applicationContext) 35 | AppHelper.startWatchingAppsCountChange(context, onAppRemoved = { packageName -> 36 | removeConfigForApp(packageName) 37 | }) 38 | } 39 | val serverVersion = configClient.serverVersion() 40 | isServerStart = serverVersion > 0 41 | if (!isServerStart) { 42 | loadingStatusFlow.value = LOADING_STATUS_FAILED 43 | return 44 | } 45 | scope.launch { 46 | val installerPackageName = context.getApkInstallerPackageName() 47 | loadingStatusFlow.value = LOADING_STATUS_LOADING 48 | var configData = configClient.queryConfig() 49 | if (null == configData) { 50 | configClient.migrateOldConfig() 51 | configData = configClient.queryConfig() 52 | } 53 | configData = (configData ?: configDataFlow.value).copy( 54 | enableDetailLog = BuildConfig.DEBUG 55 | ) 56 | if (!installerPackageName.isNullOrEmpty() 57 | && !configData.whitelist.contains(installerPackageName) 58 | ) { 59 | val whitelistNew = configData.whitelist.toMutableSet() 60 | whitelistNew.add(installerPackageName) 61 | updateWhitelist( 62 | whitelistNew = whitelistNew, 63 | sharedUserIdMapNew = configData.sharedUserIdMap ?: emptyMap() 64 | ) 65 | } 66 | configDataFlow.value = configData 67 | loadingStatusFlow.value = LOADING_STATUS_SUCCESSFUL 68 | } 69 | } 70 | 71 | private fun removeConfigForApp(packageName: String) { 72 | // switch to ui thread 73 | scope.launch { 74 | val configData = configDataFlow.value 75 | if (configData == ConfigData.EMPTY) { 76 | return@launch 77 | } 78 | val hiddenAppListNew = configData.hiddenAppList.toMutableSet() 79 | val connectedAppsNew = configData.connectedApps.toMutableMap() 80 | val multiUserConfig = 81 | configData.multiUserConfig?.toMutableMap() ?: mutableMapOf() 82 | if (hiddenAppListNew.contains(packageName)) { 83 | hiddenAppListNew.remove(packageName) 84 | connectedAppsNew.remove(packageName) 85 | multiUserConfig.remove(packageName) 86 | updateHiddenListAndConnectedApps( 87 | hiddenAppListNew, 88 | connectedAppsNew, 89 | multiUserConfig 90 | ) 91 | } 92 | } 93 | } 94 | 95 | fun loadConfigWithSystemApp(packageName: String): ConfigData? { 96 | val configFile = 97 | File("${ConfigConstant.CONFIG_FILE_FOLDER}${ConfigConstant.CONFIG_FILE_JSON}") 98 | return try { 99 | JsonHelper.configAdapter().fromJson(configFile.readText()) 100 | } catch (e: FileNotFoundException) { 101 | XLog.i("$packageName ConfigHelper loadConfigWithSystemApp failed.") 102 | null 103 | } catch (e: Throwable) { 104 | XLog.e(e, "$packageName ConfigHelper loadConfigWithSystemApp failed.") 105 | null 106 | } 107 | } 108 | 109 | suspend fun updateConfig(configData: ConfigData) { 110 | withContext(Dispatchers.IO) { 111 | configDataFlow.value = configData 112 | configClient.updateConfig(configData) 113 | } 114 | } 115 | 116 | fun getServerVersion(): Int { 117 | return configClient.serverVersion() 118 | } 119 | 120 | private suspend fun updateConfigInner(configData: ConfigData) { 121 | withContext(Dispatchers.IO) { 122 | configClient.updateConfig(configData) 123 | } 124 | } 125 | 126 | fun updateHiddenListAndConnectedApps( 127 | hiddenAppListNew: Set, 128 | connectedAppsNew: Map>, 129 | multiUserConfig: Map>, 130 | sharedUserIdMapNew: Map? = null 131 | ) { 132 | val newConfigData = configDataFlow.value.copy( 133 | hiddenAppList = hiddenAppListNew.toSet(), 134 | connectedApps = connectedAppsNew.toMap(), 135 | multiUserConfig = multiUserConfig.toMap(), 136 | sharedUserIdMap = sharedUserIdMapNew?.toMap() 137 | ?: configDataFlow.value.sharedUserIdMap 138 | ) 139 | configDataFlow.value = newConfigData 140 | scope.launch { 141 | updateConfigInner(newConfigData) 142 | } 143 | } 144 | 145 | fun updateBlindApps( 146 | whitelistNew: Set, 147 | blindAppsListNew: Set, 148 | connectedAppsNew : Map> 149 | ) { 150 | val newConfigData = configDataFlow.value.copy( 151 | whitelist = whitelistNew.toSet(), 152 | blind = blindAppsListNew.toSet(), 153 | connectedApps = connectedAppsNew.toMap() 154 | ) 155 | configDataFlow.value = newConfigData 156 | scope.launch { 157 | updateConfigInner(newConfigData) 158 | } 159 | } 160 | 161 | fun updateWhitelist( 162 | whitelistNew: Set, 163 | sharedUserIdMapNew: Map, 164 | blindApps: Set? = null 165 | ) { 166 | val newConfigData = configDataFlow.value.copy( 167 | whitelist = whitelistNew.toSet(), 168 | sharedUserIdMap = sharedUserIdMapNew.toMap(), 169 | blind = blindApps ?: configDataFlow.value.blind 170 | ) 171 | configDataFlow.value = newConfigData 172 | scope.launch { 173 | updateConfigInner(newConfigData) 174 | } 175 | } 176 | 177 | fun updateConnectedApps( 178 | connectedAppsNew: Map>, 179 | sharedUserIdMapNew: Map 180 | ) { 181 | val newConfigData = configDataFlow.value.copy( 182 | connectedApps = connectedAppsNew.toMutableMap(), 183 | sharedUserIdMap = sharedUserIdMapNew.toMutableMap() 184 | ) 185 | configDataFlow.value = newConfigData 186 | scope.launch { 187 | updateConfigInner(newConfigData) 188 | } 189 | } 190 | 191 | fun rebootTheSystem() { 192 | configClient.rebootTheSystem() 193 | } 194 | 195 | suspend fun forceStop(packageName: String): Boolean { 196 | val result = configClient.forceStop(packageName) 197 | if(!result){ 198 | Su.exec("am force-stop $packageName") 199 | } 200 | return true 201 | } 202 | 203 | suspend fun queryAllUsers(): List? { 204 | return configClient.querySystemUserList() 205 | } 206 | 207 | fun ResolveInfo.getPackageName(): String? { 208 | var packageName = activityInfo?.packageName 209 | if (packageName.isNullOrEmpty()) { 210 | packageName = providerInfo?.packageName 211 | } 212 | if (packageName.isNullOrEmpty()) { 213 | packageName = serviceInfo?.packageName 214 | } 215 | if (packageName.isNullOrEmpty()) { 216 | packageName = resolvePackageName 217 | } 218 | return packageName 219 | } 220 | } -------------------------------------------------------------------------------- /app/src/main/java/cn/geektang/privacyspace/util/ConfigServer.kt: -------------------------------------------------------------------------------- 1 | package cn.geektang.privacyspace.util 2 | 3 | import android.app.ActivityThread 4 | import android.content.pm.PackageManager 5 | import android.content.pm.UserInfo 6 | import android.os.Binder 7 | import android.os.ServiceManager 8 | import android.os.SystemProperties 9 | import cn.geektang.privacyspace.BuildConfig 10 | import cn.geektang.privacyspace.bean.SystemUserInfo 11 | import cn.geektang.privacyspace.constant.ConfigConstant 12 | import cn.geektang.privacyspace.hook.HookMain 13 | import de.robv.android.xposed.XC_MethodHook 14 | import de.robv.android.xposed.XposedBridge 15 | import de.robv.android.xposed.XposedHelpers 16 | import java.io.File 17 | import java.lang.reflect.Method 18 | 19 | class ConfigServer : XC_MethodHook() { 20 | companion object { 21 | const val QUERY_SERVER_VERSION = "serverVersion" 22 | const val MIGRATE_OLD_CONFIG_FILE = "migrateOldConfigFile" 23 | const val QUERY_CONFIG = "queryConfig" 24 | const val UPDATE_CONFIG = "updateConfig:" 25 | const val REBOOT_THE_SYSTEM = "rebootTheSystem" 26 | const val GET_USERS = "getUsers" 27 | const val FORCE_STOP = "forceStop:" 28 | 29 | const val EXEC_SUCCEED = "1" 30 | const val EXEC_FAILED = "0" 31 | } 32 | 33 | private lateinit var classLoader: ClassLoader 34 | private var pmsClass: Class<*>? = null 35 | private var userInfoListCache: Collection<*>? = null 36 | 37 | fun start(classLoader: ClassLoader) { 38 | pmsClass = HookUtil.loadPms(classLoader) 39 | this.classLoader = classLoader 40 | if (pmsClass == null) { 41 | XLog.e("ConfigServer start failed.") 42 | return 43 | } 44 | 45 | XposedHelpers.findAndHookMethod( 46 | pmsClass, 47 | "getInstallerPackageName", 48 | String::class.java, 49 | this 50 | ) 51 | 52 | val userManagerClass = try { 53 | classLoader.tryLoadClass("com.android.server.pm.UserManagerService") 54 | } catch (e: ClassNotFoundException) { 55 | XLog.e(e, "Find UserManagerService failed.") 56 | return 57 | } 58 | userManagerClass.declaredMethods.filter { method -> 59 | method.checkIsGetUsersMethod() 60 | }.forEach { method -> 61 | XposedBridge.hookMethod(method, object : XC_MethodHook() { 62 | override fun afterHookedMethod(param: MethodHookParam) { 63 | val userInfoListTmp = param.result as? Collection<*>? ?: return 64 | userInfoListCache = userInfoListTmp 65 | } 66 | }) 67 | } 68 | } 69 | 70 | override fun beforeHookedMethod(param: MethodHookParam) { 71 | when (param.method.name) { 72 | "getInstallerPackageName" -> { 73 | hookGetInstallerPackageName(param) 74 | } 75 | else -> { 76 | } 77 | } 78 | } 79 | 80 | private fun hookGetInstallerPackageName(param: MethodHookParam) { 81 | val callingUid = Binder.getCallingUid() 82 | if (callingUid != getPackageUid(BuildConfig.APPLICATION_ID) && callingUid != getPackageUid("com.android.settings") 83 | ) { 84 | return 85 | } 86 | val firstArg = param.args.first()?.toString() ?: return 87 | when { 88 | firstArg == QUERY_SERVER_VERSION -> { 89 | param.result = BuildConfig.VERSION_CODE.toString() 90 | } 91 | firstArg == MIGRATE_OLD_CONFIG_FILE -> { 92 | tryMigrateOldConfig() 93 | param.result = "" 94 | } 95 | firstArg == QUERY_CONFIG -> { 96 | param.result = queryConfig() 97 | } 98 | firstArg == REBOOT_THE_SYSTEM -> { 99 | SystemProperties.set("sys.powerctl", "reboot") 100 | param.result = "" 101 | } 102 | firstArg == GET_USERS -> { 103 | val users = userInfoListCache 104 | val systemUsers = mutableListOf() 105 | users?.forEach { userInfo -> 106 | if (userInfo !is UserInfo) return 107 | val systemUserInfo = SystemUserInfo( 108 | id = userInfo.id, 109 | name = userInfo.name 110 | ) 111 | systemUsers.add(systemUserInfo) 112 | } 113 | param.result = JsonHelper.systemUserInfoListAdapter().toJson(systemUsers) 114 | } 115 | firstArg.startsWith(UPDATE_CONFIG) -> { 116 | val arg = firstArg.substring(UPDATE_CONFIG.length) 117 | updateConfig(arg) 118 | param.result = "" 119 | } 120 | firstArg.startsWith(FORCE_STOP) -> { 121 | val arg = firstArg.substring(FORCE_STOP.length) 122 | param.result = forceStopPackage(arg) 123 | } 124 | } 125 | } 126 | 127 | private fun forceStopPackage(packageName: String): String { 128 | XLog.d("forceStopPackage = $packageName") 129 | val callingUid = Binder.getCallingUid() 130 | val ams = ServiceManager.getService("activity") 131 | val checkPermissionUnhook = XposedHelpers.findAndHookMethod( 132 | ams.javaClass, 133 | "checkPermission", 134 | String::class.java, 135 | Int::class.javaPrimitiveType, 136 | Int::class.javaPrimitiveType, 137 | object : XC_MethodHook() { 138 | override fun beforeHookedMethod(param: MethodHookParam) { 139 | val uid = param.args[2] 140 | if (callingUid == uid) { 141 | param.result = PackageManager.PERMISSION_GRANTED 142 | } 143 | } 144 | } 145 | ) 146 | 147 | val isExecSucceed = try { 148 | val method = ams.javaClass.getDeclaredMethod( 149 | "forceStopPackage", 150 | String::class.java, 151 | Int::class.javaPrimitiveType 152 | ) 153 | method.isAccessible = true 154 | method.invoke(ams, packageName, 0) 155 | EXEC_SUCCEED 156 | } catch (e: Throwable) { 157 | XLog.e(e, "forceStopPackage $packageName failed.") 158 | EXEC_FAILED 159 | } finally { 160 | checkPermissionUnhook.unhook() 161 | } 162 | return isExecSucceed 163 | } 164 | 165 | private fun queryConfig(): String { 166 | val configFile = 167 | File("${ConfigConstant.CONFIG_FILE_FOLDER}${ConfigConstant.CONFIG_FILE_JSON}") 168 | return try { 169 | configFile.readText() 170 | } catch (e: Exception) { 171 | "" 172 | } 173 | } 174 | 175 | private fun updateConfig(configJson: String) { 176 | val configFile = 177 | File("${ConfigConstant.CONFIG_FILE_FOLDER}${ConfigConstant.CONFIG_FILE_JSON}") 178 | configFile.parentFile?.mkdirs() 179 | try { 180 | val configData = JsonHelper.configAdapter().fromJson(configJson) 181 | if (null != configData) { 182 | HookMain.updateConfigData(configData) 183 | configFile.writeText(configJson) 184 | } 185 | } catch (e: Exception) { 186 | XLog.e(e, "Update config error.") 187 | } 188 | } 189 | 190 | private fun getPackageUid(packageName: String): Int { 191 | return try { 192 | ActivityThread.getPackageManager() 193 | .getPackageUid(packageName, 0, 0) 194 | } catch (e: Throwable) { 195 | XLog.d("ConfigServer (${Binder.getCallingUid()}).getClientUid failed.") 196 | -1 197 | } 198 | } 199 | 200 | private fun tryMigrateOldConfig() { 201 | val originalFile = 202 | File("${ConfigConstant.CONFIG_FILE_FOLDER_ORIGINAL}${ConfigConstant.CONFIG_FILE_JSON}") 203 | val newConfigFile = 204 | File("${ConfigConstant.CONFIG_FILE_FOLDER}${ConfigConstant.CONFIG_FILE_JSON}") 205 | if (!newConfigFile.exists()) { 206 | newConfigFile.parentFile?.mkdirs() 207 | originalFile.copyTo(newConfigFile) 208 | } 209 | } 210 | 211 | private fun Method.checkIsGetUsersMethod(): Boolean { 212 | if (name != "getUsers") { 213 | return false 214 | } 215 | var isGetUsersMethod = false 216 | if (parameterCount == 1 && parameterTypes.first() == Boolean::class.javaPrimitiveType) { 217 | isGetUsersMethod = true 218 | } else if (parameterCount == 3) { 219 | isGetUsersMethod = true 220 | for (parameterType in parameterTypes) { 221 | if (parameterType != Boolean::class.javaPrimitiveType) { 222 | isGetUsersMethod = false 223 | break 224 | } 225 | } 226 | } 227 | return isGetUsersMethod 228 | } 229 | } -------------------------------------------------------------------------------- /app/src/main/java/cn/geektang/privacyspace/util/Context.kt: -------------------------------------------------------------------------------- 1 | package cn.geektang.privacyspace.util 2 | 3 | import android.annotation.StringRes 4 | import android.content.Context 5 | import android.content.Intent 6 | import android.content.SharedPreferences 7 | import android.net.Uri 8 | import android.widget.Toast 9 | 10 | 11 | val Context.sp: SharedPreferences 12 | get() { 13 | return getSharedPreferences("config", Context.MODE_PRIVATE) 14 | } 15 | 16 | var SharedPreferences.hasReadNotice: Boolean 17 | get() { 18 | return getBoolean("readNotice", false) 19 | } 20 | set(value) { 21 | edit().putBoolean("readNotice", value).apply() 22 | } 23 | 24 | var SharedPreferences.hasReadNotice2: Boolean 25 | get() { 26 | return getBoolean("readNotice2", false) 27 | } 28 | set(value) { 29 | edit().putBoolean("readNotice2", value).apply() 30 | } 31 | 32 | var SharedPreferences.iconCellCountLandscape: Int 33 | get() { 34 | return getInt("iconCellCountLandscape", 8) 35 | } 36 | set(value) { 37 | edit().putInt("iconCellCountLandscape", value).apply() 38 | } 39 | 40 | var SharedPreferences.iconPaddingLandscape: Float 41 | get() { 42 | return getFloat("iconPaddingLandscape", 0.5f) 43 | } 44 | set(value) { 45 | edit().putFloat("iconPaddingLandscape", value).apply() 46 | } 47 | 48 | var SharedPreferences.iconCellCountPortrait: Int 49 | get() { 50 | return getInt("iconCellPortrait", 4) 51 | } 52 | set(value) { 53 | edit().putInt("iconCellPortrait", value).apply() 54 | } 55 | 56 | var SharedPreferences.iconPaddingPortrait: Float 57 | get() { 58 | return getFloat("iconPaddingPortrait", 0.5f) 59 | } 60 | set(value) { 61 | edit().putFloat("iconPaddingPortrait", value).apply() 62 | } 63 | 64 | fun Context.showToast(@StringRes textRes: Int) { 65 | showToast(getString(textRes)) 66 | } 67 | 68 | fun Context.showToast(text: String) { 69 | Toast.makeText( 70 | this, 71 | String.format(text), 72 | Toast.LENGTH_SHORT 73 | ).show() 74 | } 75 | 76 | fun Context.openUrl(url: String) { 77 | val uri = Uri.parse(url) 78 | val intent = Intent(Intent.ACTION_VIEW, uri) 79 | intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) 80 | startActivity(intent) 81 | } -------------------------------------------------------------------------------- /app/src/main/java/cn/geektang/privacyspace/util/EqualsHelper.kt: -------------------------------------------------------------------------------- 1 | package cn.geektang.privacyspace.util 2 | 3 | import cn.geektang.privacyspace.bean.AppInfo 4 | import kotlinx.coroutines.flow.MutableStateFlow 5 | 6 | fun List.isAllEquals(others: List): Boolean { 7 | if (this.size != others.size) { 8 | return false 9 | } 10 | 11 | var isAllEquals = true 12 | for (index in 0 until size) { 13 | val thisApp = get(index) 14 | val othersApp = others[index] 15 | if ( 16 | thisApp.packageName != othersApp.packageName 17 | || thisApp.appName != othersApp.appName 18 | || thisApp.isXposedModule != othersApp.isXposedModule 19 | || thisApp.isSystemApp != othersApp.isSystemApp 20 | ) { 21 | isAllEquals = false 22 | break 23 | } 24 | } 25 | return isAllEquals 26 | } 27 | 28 | fun MutableStateFlow>.setDifferentValue(newValue: List) { 29 | if (!value.isAllEquals(newValue)) { 30 | value = newValue 31 | } 32 | } -------------------------------------------------------------------------------- /app/src/main/java/cn/geektang/privacyspace/util/HookUtil.kt: -------------------------------------------------------------------------------- 1 | package cn.geektang.privacyspace.util 2 | 3 | object HookUtil { 4 | private val pmsClassNameArray = arrayOf( 5 | "com.android.server.pm.PackageManagerService", 6 | "com.android.server.pm.OplusPackageManagerService", 7 | "com.android.server.pm.OppoPackageManagerService" 8 | ) 9 | 10 | fun loadPms(classLoader: ClassLoader): Class<*>? { 11 | var pmsClass: Class<*>? = null 12 | for (pmsClassName in pmsClassNameArray) { 13 | try { 14 | pmsClass = classLoader.loadClass(pmsClassName) 15 | if (pmsClass != null) { 16 | break 17 | } 18 | } catch (ignored: ClassNotFoundException) { 19 | } 20 | } 21 | return pmsClass 22 | } 23 | } -------------------------------------------------------------------------------- /app/src/main/java/cn/geektang/privacyspace/util/JsonHelper.kt: -------------------------------------------------------------------------------- 1 | package cn.geektang.privacyspace.util 2 | 3 | import cn.geektang.privacyspace.bean.ConfigData 4 | import cn.geektang.privacyspace.bean.SystemUserInfo 5 | import com.squareup.moshi.JsonAdapter 6 | import com.squareup.moshi.Moshi 7 | import com.squareup.moshi.Types 8 | 9 | object JsonHelper { 10 | private val moshi = Moshi.Builder().build() 11 | 12 | fun configAdapter(): JsonAdapter { 13 | return moshi.adapter(ConfigData::class.java) 14 | } 15 | 16 | fun systemUserInfoListAdapter(): JsonAdapter> { 17 | return moshi.adapter( 18 | Types.newParameterizedType( 19 | List::class.java, 20 | SystemUserInfo::class.java 21 | ) 22 | ) 23 | } 24 | } -------------------------------------------------------------------------------- /app/src/main/java/cn/geektang/privacyspace/util/NavHostControllerWrapper.kt: -------------------------------------------------------------------------------- 1 | package cn.geektang.privacyspace.util 2 | 3 | import androidx.compose.runtime.compositionLocalOf 4 | import androidx.navigation.NavHostController 5 | 6 | class NavHostControllerWrapper(private val navHostController: NavHostController?) { 7 | 8 | fun navigate(route: String) { 9 | navHostController?.navigate(route) 10 | } 11 | 12 | fun popBackStack(){ 13 | navHostController?.popBackStack() 14 | } 15 | } 16 | 17 | val LocalNavHostController = compositionLocalOf { NavHostControllerWrapper(null) } -------------------------------------------------------------------------------- /app/src/main/java/cn/geektang/privacyspace/util/OnLifecycleEvent.kt: -------------------------------------------------------------------------------- 1 | package cn.geektang.privacyspace.util 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.runtime.DisposableEffect 5 | import androidx.compose.runtime.getValue 6 | import androidx.compose.runtime.rememberUpdatedState 7 | import androidx.compose.ui.platform.LocalLifecycleOwner 8 | import androidx.lifecycle.Lifecycle 9 | import androidx.lifecycle.LifecycleEventObserver 10 | import androidx.lifecycle.LifecycleOwner 11 | 12 | @Composable 13 | fun OnLifecycleEvent( 14 | lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current, 15 | onEvent: (Lifecycle.Event) -> Unit 16 | ) { 17 | // Safely update the current lambdas when a new one is provided 18 | val currentOnResume by rememberUpdatedState(onEvent) 19 | 20 | // If `lifecycleOwner` changes, dispose and reset the effect 21 | DisposableEffect(lifecycleOwner) { 22 | // Create an observer that triggers our remembered callbacks 23 | // for sending analytics events 24 | val observer = LifecycleEventObserver { _, event -> 25 | currentOnResume(event) 26 | } 27 | 28 | // Add the observer to the lifecycle 29 | lifecycleOwner.lifecycle.addObserver(observer) 30 | 31 | onDispose { 32 | lifecycleOwner.lifecycle.removeObserver(observer) 33 | } 34 | } 35 | } -------------------------------------------------------------------------------- /app/src/main/java/cn/geektang/privacyspace/util/Su.kt: -------------------------------------------------------------------------------- 1 | package cn.geektang.privacyspace.util 2 | 3 | import kotlinx.coroutines.Dispatchers 4 | import kotlinx.coroutines.withContext 5 | import java.io.DataOutputStream 6 | import java.lang.Exception 7 | 8 | object Su { 9 | suspend fun exec(command: String): Boolean { 10 | return withContext(Dispatchers.IO) { 11 | try { 12 | val process = Runtime.getRuntime().exec("su -c $command") 13 | process.outputStream.use { outputStream -> 14 | DataOutputStream(outputStream).use { 15 | it.writeBytes("exit\n") 16 | it.flush() 17 | } 18 | } 19 | process.waitFor() == 0 20 | } catch (e: Exception) { 21 | e.printStackTrace() 22 | false 23 | } 24 | } 25 | } 26 | } -------------------------------------------------------------------------------- /app/src/main/java/cn/geektang/privacyspace/util/XLog.kt: -------------------------------------------------------------------------------- 1 | package cn.geektang.privacyspace.util 2 | 3 | import de.robv.android.xposed.XposedBridge 4 | 5 | object XLog { 6 | var enableLog : Boolean = false 7 | 8 | fun d(message: String) { 9 | if (enableLog) { 10 | XposedBridge.log("[PrivacySpace] [Debug] $message") 11 | } 12 | } 13 | 14 | fun i(message: String) { 15 | XposedBridge.log("[PrivacySpace] [Info] $message") 16 | } 17 | 18 | fun e(message: String) { 19 | XposedBridge.log("[PrivacySpace] [Error] $message") 20 | } 21 | 22 | fun e(cause: Throwable, message: String) { 23 | XposedBridge.log("[PrivacySpace] [Error] $message") 24 | XposedBridge.log(cause) 25 | } 26 | } -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/ic_avatar.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GeekTR/PrivacySpace/46eb0c846abb57cff0ca3a92b4a2e403765b9c31/app/src/main/res/drawable-xxhdpi/ic_avatar.webp -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/ic_coolapk.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GeekTR/PrivacySpace/46eb0c846abb57cff0ca3a92b4a2e403765b9c31/app/src/main/res/drawable-xxhdpi/ic_coolapk.webp -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/ic_telegram.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GeekTR/PrivacySpace/46eb0c846abb57cff0ca3a92b4a2e403765b9c31/app/src/main/res/drawable-xxhdpi/ic_telegram.webp -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_github.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GeekTR/PrivacySpace/46eb0c846abb57cff0ca3a92b4a2e403765b9c31/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GeekTR/PrivacySpace/46eb0c846abb57cff0ca3a92b4a2e403765b9c31/app/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GeekTR/PrivacySpace/46eb0c846abb57cff0ca3a92b4a2e403765b9c31/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GeekTR/PrivacySpace/46eb0c846abb57cff0ca3a92b4a2e403765b9c31/app/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GeekTR/PrivacySpace/46eb0c846abb57cff0ca3a92b4a2e403765b9c31/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GeekTR/PrivacySpace/46eb0c846abb57cff0ca3a92b4a2e403765b9c31/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GeekTR/PrivacySpace/46eb0c846abb57cff0ca3a92b4a2e403765b9c31/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GeekTR/PrivacySpace/46eb0c846abb57cff0ca3a92b4a2e403765b9c31/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GeekTR/PrivacySpace/46eb0c846abb57cff0ca3a92b4a2e403765b9c31/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GeekTR/PrivacySpace/46eb0c846abb57cff0ca3a92b4a2e403765b9c31/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/values-zh-rCN/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 隐秘空间 3 | 菜单 4 | 设置白名单 5 | 显示系统App 6 | 隐藏App 7 | 重启桌面 8 | 重启系统 9 | 加载中... 10 | 设置关联应用 11 | 提示 12 | 废话太多了,跳过 13 | 朕已仔细阅读 14 | %s已卸载 15 | 卸载 16 | 取消隐藏 17 | 1、如已启用Magisk随机包名,请务必将其加入白名单,否则隐藏的应用将无法正确获取到Root权限\n2、从老版本升级后务必重启一次手机,否则功能将失效\n3、本页面图标长按可以设置\"关联应用\",关联应用与被设置应用间互相可见\n4、Xposed模块会在添加后自动将其推荐的APP加入\"关联应用\"\n5、不建议隐藏系统应用,隐藏系统应用可能导致系统无法开机 18 | 正在为\'%s\'设置关联应用,关联应用与\'%s\'之间互相可见 19 | 查看当前版本 20 | 当前版本为:%s 21 | 查看更新信息(酷安) 22 | 查看更新信息(github) 23 | 似乎没有安装酷安app 24 | 将App藏到另一个空间 25 | 重试 26 | 模块未生效 27 | 模块未生效,您可能需要:\n1、激活本模块;\n2、重启手机。 28 | 返回 29 | 搜索... 30 | 清空 31 | 桌面重启成功! 32 | 桌面重启失败... 33 | 不要忘记将Magisk加入白名单 (如果开启了Magisk随机包名功能的话) 34 | 调整布局 35 | 图标大小 36 | 每行%d个 37 | 获取App列表失败,请授予获取应用列表权限 38 | 与桌面关联 39 | 取消关联桌面 40 | 勾选则代表需要对其隐藏 41 | 多用户配置 42 | 此配置需要重启手机系统生效 43 | 全选系统App 44 | 取消 45 | 确认 46 | 您确认重启系统吗? 47 | 致盲 48 | \'%s\'已被致盲,加入白名单后将取消致盲,您确认这样操作吗? 49 | \'%s\'已被加入白名单,致盲后将移出白名单,您确认这样操作吗? 50 | 重启手机管家 51 | 手机管家重启成功! 52 | 手机管家重启失败… 53 | 点我设置关联应用 54 | 关于 55 | 本应用是一个全局隐藏应用列表的Xposed模块 56 | 酷安 57 | 隐秘空间Alpha版本 58 | 最新稳定版本 59 | 重启Settings 60 | -------------------------------------------------------------------------------- /app/src/main/res/values-zh-rTW/strings.xml: -------------------------------------------------------------------------------- 1 |  2 | 隱秘空間 3 | 選單 4 | 設定白名單 5 | 顯示系統App 6 | 隱藏App 7 | 重啟桌面 8 | 重啟系統 9 | 載入中... 10 | 設定關聯應用 11 | 提示 12 | 廢話太多了,跳過 13 | 朕已仔細閱讀 14 | %s已解除安裝 15 | 解除安裝 16 | 取消隱藏 17 | 1、如已啟用Magisk隨機包名,請務必將其加入白名單,否則隱藏的應用將無法正確獲取到 Root 許可權\n2、從老版本升級後務必重啟一次手機,否則功能將失效\n3、本頁面圖示長按可以設定\"關聯應用\",關聯應用與被設定應用間互相可見\n4、Xposed 模組會在新增後自動將其推薦的 APP 加入\"關聯應用\"\n5、不建議隱藏系統應用,隱藏系統應用可能導致系統無法開機 18 | 正在為\'%s\'設定關聯應用,關聯應用與\'%s\'之間互相可見 19 | 檢視當前版本 20 | 當前版本為:%s 21 | 檢視更新訊息(酷安) 22 | 檢視更新訊息(github) 23 | 似乎沒有安裝酷安app 24 | 將App藏到另一個空間 25 | 重試 26 | 模組未生效 27 | 模組未生效,您可能需要:\n1、啟動本模組;\n2、重啟手機。 28 | 返回 29 | 搜尋... 30 | 清空 31 | 桌面重啟成功! 32 | 桌面重啟失敗... 33 | 不要忘記將 Magisk 加入白名單 (如果開啟了 Magisk 隨機包名功能的話) 34 | 調整佈局 35 | 圖示大小 36 | 每行%d個 37 | 獲取App列表失敗,請授予獲取應用列表許可權 38 | 與桌面關聯 39 | 取消關聯桌面 40 | 勾選則代表需要對其隱藏 41 | 多使用者設定 42 | 此配置需要重啟手機系統後生效 43 | 全選系統App 44 | 取消 45 | 確定 46 | 您確定重啟系統嗎? 47 | 致盲 48 | \'%s\'已被致盲,加入白名單後將取消致盲,您確認這樣操作嗎? 49 | \'%s\'已被加入白名單,致盲後將移出白名單,您確認這樣操作嗎? 50 | 重啟手機管家 51 | 手機管家重啟成功! 52 | 手機管家重啟成功… 53 | 點我設定關聯應用 54 | 關於 55 | 本應用是一個全局隱藏應用列表的Xposed模組 56 | 酷安 57 | 隱秘空間Alpha版本 58 | 最新穩定版本 59 | 重啟Settings 60 | -------------------------------------------------------------------------------- /app/src/main/res/values/arrays.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | android 5 | com.miui.securitycenter 6 | com.miui.cleanmaster 7 | com.android.settings 8 | 9 | -------------------------------------------------------------------------------- /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/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | PrivacySpace 3 | Menu 4 | Whitelist 5 | Show System Apps 6 | Hidden Apps 7 | Restart Launcher 8 | Restart System 9 | Loading… 10 | Connected with Others 11 | Tips 12 | @android:string/cancel 13 | CONFIRM 14 | %s is uninstalled 15 | Uninstall 16 | Unhide 17 | 1、If the random package name of Magisk is enabled, add it to the whitelist. Otherwise, hidden apps cannot obtain Root permission\n2、Make sure to restart the mobile phone after upgrading from the old version; otherwise, the function will be invalid\n3、On this page, you can hold down the icon to set \"Connected Apps\". \"Connected Apps\" and the app configured in this method are not hidden from each other\n4、Hiding the system apps may cause the system to fail to boot 18 | Setting up for \'%s\',the Connected Apps and \'%s\' are mutually visible. 19 | Current App Version 20 | Current app version is v%s 21 | View Update Info(Coolapk) 22 | View Update Info(Github) 23 | Coolapk not found. 24 | Hide the app in another space. 25 | Retry 26 | The module does not take effect 27 | The module does not take effect, you may need to: \n1. Activate this module;\n2. Restart the System. 28 | Back 29 | Search… 30 | Clear 31 | Launcher restart successfully! 32 | Launcher restart failed… 33 | Don\'t forget to whitelist Magisk (if it has random package names enabled) 34 | Screen Layout 35 | Icon Size 36 | %d per line 37 | Failed to get Apps, please grant permission. 38 | Connect with Launcher 39 | Disconnect with Launcher 40 | If checked, it means that it needs to be hidden 41 | Set for Multi-user 42 | This configuration takes effect after restarting the phone system 43 | Select All System Apps 44 | CANCEL 45 | CONFIRM 46 | Are you sure to restart the system? 47 | Blacklist 48 | \'%s\' has been blacklisted. After it is added to the whitelist, it will be automatically removed from the blacklist. Are you sure you want to do this? 49 | \'%s\' has been whitelisted. After it is added to the blacklist, it will be automatically removed from the whitelist. Are you sure you want to do this? 50 | Restart Security 51 | Security restart successfully! 52 | Security restart failed… 53 | Connected with Others 54 | About 55 | This app is an Xposed module that hides the app list globally. 56 | Coolapk 57 | Privacy Space Alpha 58 | Latest stable version 59 | Restart Settings App 60 | -------------------------------------------------------------------------------- /app/src/main/res/values/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 8 | -------------------------------------------------------------------------------- /app/src/test/java/cn/geektang/privacyspace/ExampleUnitTest.kt: -------------------------------------------------------------------------------- 1 | package cn.geektang.privacyspace 2 | 3 | import org.junit.Test 4 | 5 | import org.junit.Assert.* 6 | 7 | /** 8 | * Example local unit test, which will execute on the development machine (host). 9 | * 10 | * See [testing documentation](http://d.android.com/tools/testing). 11 | */ 12 | class ExampleUnitTest { 13 | @Test 14 | fun addition_isCorrect() { 15 | assertEquals(4, 2 + 2) 16 | } 17 | } -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | ext { 3 | compose_version = '1.1.1' 4 | hilt_version = "2.40.5" 5 | } 6 | 7 | dependencies { 8 | classpath 'com.android.tools.build:gradle:7.0.4' 9 | } 10 | }// Top-level build file where you can add configuration options common to all sub-projects/modules. 11 | plugins { 12 | id 'com.android.application' version '7.1.1' apply false 13 | id 'com.android.library' version '7.1.1' apply false 14 | id 'org.jetbrains.kotlin.android' version '1.6.10' apply false 15 | id 'com.google.devtools.ksp' version("1.6.0-1.0.1") apply false 16 | } 17 | 18 | task clean(type: Delete) { 19 | delete rootProject.buildDir 20 | } -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | # IDE (e.g. Android Studio) users: 3 | # Gradle settings configured through the IDE *will override* 4 | # any settings specified in this file. 5 | # For more details on how to configure your build environment visit 6 | # http://www.gradle.org/docs/current/userguide/build_environment.html 7 | # Specifies the JVM arguments used for the daemon process. 8 | # The setting is particularly useful for tweaking memory settings. 9 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 10 | # When configured, Gradle will run in incubating parallel mode. 11 | # This option should only be used with decoupled projects. More details, visit 12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 13 | # org.gradle.parallel=true 14 | # AndroidX package structure to make it clearer which packages are bundled with the 15 | # Android operating system, and which are packaged with your app"s APK 16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn 17 | android.useAndroidX=true 18 | # Kotlin code style for this project: "official" or "obsolete": 19 | kotlin.code.style=official 20 | # Enables namespacing of each library's R class so that its R class includes only the 21 | # resources declared in the library itself and none from the library's dependencies, 22 | # thereby reducing the size of the R class for that library 23 | android.nonTransitiveRClass=true -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GeekTR/PrivacySpace/46eb0c846abb57cff0ca3a92b4a2e403765b9c31/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Wed Feb 09 16:41:49 CST 2022 2 | distributionBase=GRADLE_USER_HOME 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-bin.zip 4 | distributionPath=wrapper/dists 5 | zipStorePath=wrapper/dists 6 | zipStoreBase=GRADLE_USER_HOME 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # 4 | # Copyright 2015 the original author or authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | ## 21 | ## Gradle start up script for UN*X 22 | ## 23 | ############################################################################## 24 | 25 | # Attempt to set APP_HOME 26 | # Resolve links: $0 may be a link 27 | PRG="$0" 28 | # Need this for relative symlinks. 29 | while [ -h "$PRG" ] ; do 30 | ls=`ls -ld "$PRG"` 31 | link=`expr "$ls" : '.*-> \(.*\)$'` 32 | if expr "$link" : '/.*' > /dev/null; then 33 | PRG="$link" 34 | else 35 | PRG=`dirname "$PRG"`"/$link" 36 | fi 37 | done 38 | SAVED="`pwd`" 39 | cd "`dirname \"$PRG\"`/" >/dev/null 40 | APP_HOME="`pwd -P`" 41 | cd "$SAVED" >/dev/null 42 | 43 | APP_NAME="Gradle" 44 | APP_BASE_NAME=`basename "$0"` 45 | 46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 48 | 49 | # Use the maximum available, or set MAX_FD != -1 to use that value. 50 | MAX_FD="maximum" 51 | 52 | warn () { 53 | echo "$*" 54 | } 55 | 56 | die () { 57 | echo 58 | echo "$*" 59 | echo 60 | exit 1 61 | } 62 | 63 | # OS specific support (must be 'true' or 'false'). 64 | cygwin=false 65 | msys=false 66 | darwin=false 67 | nonstop=false 68 | case "`uname`" in 69 | CYGWIN* ) 70 | cygwin=true 71 | ;; 72 | Darwin* ) 73 | darwin=true 74 | ;; 75 | MINGW* ) 76 | msys=true 77 | ;; 78 | NONSTOP* ) 79 | nonstop=true 80 | ;; 81 | esac 82 | 83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 84 | 85 | 86 | # Determine the Java command to use to start the JVM. 87 | if [ -n "$JAVA_HOME" ] ; then 88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 89 | # IBM's JDK on AIX uses strange locations for the executables 90 | JAVACMD="$JAVA_HOME/jre/sh/java" 91 | else 92 | JAVACMD="$JAVA_HOME/bin/java" 93 | fi 94 | if [ ! -x "$JAVACMD" ] ; then 95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 96 | 97 | Please set the JAVA_HOME variable in your environment to match the 98 | location of your Java installation." 99 | fi 100 | else 101 | JAVACMD="java" 102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 103 | 104 | Please set the JAVA_HOME variable in your environment to match the 105 | location of your Java installation." 106 | fi 107 | 108 | # Increase the maximum file descriptors if we can. 109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 110 | MAX_FD_LIMIT=`ulimit -H -n` 111 | if [ $? -eq 0 ] ; then 112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 113 | MAX_FD="$MAX_FD_LIMIT" 114 | fi 115 | ulimit -n $MAX_FD 116 | if [ $? -ne 0 ] ; then 117 | warn "Could not set maximum file descriptor limit: $MAX_FD" 118 | fi 119 | else 120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 121 | fi 122 | fi 123 | 124 | # For Darwin, add options to specify how the application appears in the dock 125 | if $darwin; then 126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 127 | fi 128 | 129 | # For Cygwin or MSYS, switch paths to Windows format before running java 130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then 131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 133 | 134 | JAVACMD=`cygpath --unix "$JAVACMD"` 135 | 136 | # We build the pattern for arguments to be converted via cygpath 137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 138 | SEP="" 139 | for dir in $ROOTDIRSRAW ; do 140 | ROOTDIRS="$ROOTDIRS$SEP$dir" 141 | SEP="|" 142 | done 143 | OURCYGPATTERN="(^($ROOTDIRS))" 144 | # Add a user-defined pattern to the cygpath arguments 145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 147 | fi 148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 149 | i=0 150 | for arg in "$@" ; do 151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 153 | 154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 156 | else 157 | eval `echo args$i`="\"$arg\"" 158 | fi 159 | i=`expr $i + 1` 160 | done 161 | case $i in 162 | 0) set -- ;; 163 | 1) set -- "$args0" ;; 164 | 2) set -- "$args0" "$args1" ;; 165 | 3) set -- "$args0" "$args1" "$args2" ;; 166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;; 167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 172 | esac 173 | fi 174 | 175 | # Escape application args 176 | save () { 177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 178 | echo " " 179 | } 180 | APP_ARGS=`save "$@"` 181 | 182 | # Collect all arguments for the java command, following the shell quoting and substitution rules 183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 184 | 185 | exec "$JAVACMD" "$@" 186 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | repositories { 3 | gradlePluginPortal() 4 | google() 5 | mavenCentral() 6 | } 7 | } 8 | dependencyResolutionManagement { 9 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) 10 | repositories { 11 | google() 12 | mavenCentral() 13 | jcenter() 14 | } 15 | } 16 | rootProject.name = "PrivacySpace" 17 | include ':app' 18 | --------------------------------------------------------------------------------