├── .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 | [](https://github.com/GeekTR/PrivacySpace)
4 | [](https://github.com/Xposed-Modules-Repo/cn.geektang.privacyspace/releases/latest)
5 | [](https://github.com/Xposed-Modules-Repo/cn.geektang.privacyspace/releases/latest)
6 | [](https://t.me/PrivacySpaceAlpha)
7 | [](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 |
--------------------------------------------------------------------------------