├── .gitignore
├── .idea
├── .gitignore
├── compiler.xml
├── gradle.xml
├── misc.xml
└── vcs.xml
├── README.md
├── app
├── .gitignore
├── build.gradle.kts
├── proguard-rules.pro
└── src
│ ├── androidTest
│ └── java
│ │ └── fivecc
│ │ └── tools
│ │ └── shortcut_helper
│ │ └── ExampleInstrumentedTest.kt
│ ├── main
│ ├── AndroidManifest.xml
│ ├── aidl
│ │ └── fivecc
│ │ │ └── tools
│ │ │ └── shortcut_helper
│ │ │ └── IRootHelper.aidl
│ ├── ic_launcher-playstore.png
│ ├── java
│ │ └── fivecc
│ │ │ └── tools
│ │ │ └── shortcut_helper
│ │ │ ├── App.kt
│ │ │ ├── AppInfoCache.kt
│ │ │ ├── MainActivity.kt
│ │ │ ├── MainActivityViewModel.kt
│ │ │ ├── RootHelperService.kt
│ │ │ ├── Settings.kt
│ │ │ ├── coil
│ │ │ ├── ShortcutIconFetcher.kt
│ │ │ └── ShortcutInfoKeyer.kt
│ │ │ ├── ui
│ │ │ └── theme
│ │ │ │ ├── Color.kt
│ │ │ │ ├── Theme.kt
│ │ │ │ └── Type.kt
│ │ │ └── utils
│ │ │ ├── PrefUtils.kt
│ │ │ ├── ShortcutInfoHelper.kt
│ │ │ ├── ShortcutParser.java
│ │ │ └── XmlUtils.java
│ └── res
│ │ ├── drawable
│ │ └── ic_launcher_foreground.xml
│ │ ├── mipmap-anydpi-v26
│ │ ├── ic_launcher.xml
│ │ └── ic_launcher_round.xml
│ │ ├── values
│ │ ├── colors.xml
│ │ ├── ic_launcher_background.xml
│ │ ├── strings.xml
│ │ └── themes.xml
│ │ └── xml
│ │ ├── backup_rules.xml
│ │ └── data_extraction_rules.xml
│ └── test
│ └── java
│ └── fivecc
│ └── tools
│ └── shortcut_helper
│ └── ExampleUnitTest.kt
├── build.gradle.kts
├── gradle.properties
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
├── hidden-api
├── .gitignore
├── build.gradle.kts
├── consumer-rules.pro
├── proguard-rules.pro
└── src
│ └── main
│ ├── AndroidManifest.xml
│ └── java
│ ├── android
│ ├── content
│ │ └── pm
│ │ │ ├── BaseParceledListSlice.java
│ │ │ ├── ILauncherApps.java
│ │ │ ├── IPackageManager.java
│ │ │ ├── IShortcutService.java
│ │ │ ├── IShortcutServiceForS.java
│ │ │ ├── PackageManagerHidden.java
│ │ │ ├── ParceledListSlice.java
│ │ │ └── ShortcutInfoHidden.java
│ ├── ddm
│ │ └── DdmHandleAppName.java
│ ├── os
│ │ ├── PersistableBundleHidden.java
│ │ ├── ServiceManager.java
│ │ └── UserHandleHidden.java
│ └── util
│ │ ├── TypedXmlPullParser.java
│ │ └── XmlHidden.java
│ └── com
│ └── android
│ └── internal
│ └── infra
│ └── AndroidFuture.java
├── img
└── 1.png
└── settings.gradle.kts
/.gitignore:
--------------------------------------------------------------------------------
1 | *.iml
2 | .gradle
3 | /local.properties
4 | /.idea/caches
5 | /.idea/libraries
6 | /.idea/modules.xml
7 | /.idea/workspace.xml
8 | /.idea/navEditor.xml
9 | /.idea/assetWizardSettings.xml
10 | .DS_Store
11 | /build
12 | /captures
13 | .externalNativeBuild
14 | .cxx
15 | local.properties
16 | keystore.properties
17 |
--------------------------------------------------------------------------------
/.idea/.gitignore:
--------------------------------------------------------------------------------
1 | # Default ignored files
2 | /shelf/
3 | /workspace.xml
4 |
--------------------------------------------------------------------------------
/.idea/compiler.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/gradle.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
19 |
20 |
--------------------------------------------------------------------------------
/.idea/misc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/.idea/vcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Android Shortcut Helper
2 |
3 | Android 快捷方式助手。
4 |
5 | ## 功能
6 |
7 | 获取系统中注册的所有应用的 shortcuts (快捷方式) ,以及它们的 intent —— 包括桌面快捷方式、长按桌面应用图标的快捷方式等。
8 |
9 | > 具体可参见[应用快捷方式概览](https://developer.android.com/guide/topics/ui/shortcuts?hl=zh-cn)
10 |
11 | 你可以直接获得快捷方式的 intent URI (包括所有的 extras)。
12 |
13 | > 快捷方式的 extras 被限制为基本类型(`PersistenceBundle` 支持的类型)
14 |
15 | 
16 |
17 | 弹出的 Dialog 中可以点击复制 uri 或点击下面的 launch 启动这个 intent 。
18 | 应用也可以发起 `Intent.CREATE_SHORTCUT` 的 Intent 来选择一个快捷方式
19 | (如 [Anywhere](https://github.com/zhaobozhen/Anywhere-) 或 [FV 悬浮球](https://play.google.com/store/apps/details?id=com.fooview.android.fooview&hl=zh&gl=US))。
20 |
21 | ## 原理
22 |
23 | 1. ShortcutInfo 的获取
24 |
25 | 由于系统 API 的限制,通过 `LauncherApps` 或者 `ShortcutService` 获取的 shortcuts 都会被剔除部分信息,导致无法获得 intent 。
26 | 因此目前通过解析目录 `/data/system_ce/0/shortcut_services` 下的 xml 获取 ShortcutInfo 。
27 |
28 | 2. 图标的获取
29 |
30 | 参考了 `LauncherApps` 的实现。
31 |
32 | ## 注意
33 |
34 | 1. 目前需要 root 权限(存在 shell 权限的方案,即解析 `dumpsys shortcut` )。
35 | 2. 需要特殊权限的 Intent 暂时无法在 App 内启动。
36 | 3. 目前仅支持解析主用户的 shortcut 。
37 | 4. 有概率出现获取不完全的情况,此时可以尝试刷新。
38 | 5. 目前 `CREATE_SHORTCUT` 返回的快捷方式不含图标。
39 | 6. ~~UI 真的很烂,期待 UI 大师指点~~
40 |
41 | ## TODO
42 |
43 | 1. 支持 shell 权限(计划使用 Shizuku API)
44 | 2. 更好的 UI
45 |
--------------------------------------------------------------------------------
/app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/app/build.gradle.kts:
--------------------------------------------------------------------------------
1 | import java.io.FileInputStream
2 | import java.util.*
3 |
4 | plugins {
5 | id("com.android.application")
6 | id("org.jetbrains.kotlin.android")
7 | id("dev.rikka.tools.refine")
8 | kotlin("kapt") version "1.7.10"
9 | }
10 |
11 | val composeVersion: String by project
12 |
13 | val keystorePropertiesFile = rootProject.file("keystore.properties")
14 | val keystoreProperties = if (keystorePropertiesFile.exists() && keystorePropertiesFile.isFile) {
15 | Properties().apply {
16 | load(FileInputStream(keystorePropertiesFile))
17 | }
18 | } else null
19 |
20 | android {
21 | if (keystoreProperties != null) {
22 | signingConfigs {
23 | create("release") {
24 | keyAlias = keystoreProperties["keyAlias"] as String
25 | keyPassword = keystoreProperties["keyPassword"] as String
26 | storeFile = file(keystoreProperties["storeFile"] as String)
27 | storePassword = keystoreProperties["storePassword"] as String
28 | }
29 | }
30 | }
31 |
32 | compileSdk = 33
33 |
34 | defaultConfig {
35 | applicationId = "fivecc.tools.shortcut_helper"
36 | minSdk = 26
37 | targetSdk = 33
38 | versionCode = 1
39 | versionName = "1.0"
40 |
41 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
42 | vectorDrawables {
43 | useSupportLibrary = true
44 | }
45 | }
46 |
47 | buildTypes {
48 | debug {
49 | applicationIdSuffix = ".debug"
50 | }
51 | release {
52 | isMinifyEnabled = true
53 | isShrinkResources = true
54 | proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
55 | if (keystoreProperties != null) {
56 | signingConfig = signingConfigs["release"]
57 | }
58 | }
59 | }
60 | compileOptions {
61 | sourceCompatibility = JavaVersion.VERSION_11
62 | targetCompatibility = JavaVersion.VERSION_11
63 | }
64 | kotlinOptions {
65 | jvmTarget = "11"
66 | }
67 | buildFeatures {
68 | compose = true
69 | }
70 | composeOptions {
71 | kotlinCompilerExtensionVersion = "1.3.1"
72 | }
73 | packagingOptions {
74 | resources {
75 | excludes += "/META-INF/{AL2.0,LGPL2.1}"
76 | }
77 | }
78 | }
79 |
80 | dependencies {
81 | compileOnly(project(":hidden-api"))
82 | implementation("org.lsposed.hiddenapibypass:hiddenapibypass:4.3")
83 |
84 | implementation("androidx.core:core-ktx:1.9.0")
85 | implementation("androidx.compose.ui:ui:$composeVersion")
86 | implementation("androidx.compose.material3:material3:1.0.0-beta02")
87 | implementation("androidx.compose.runtime:runtime-livedata:$composeVersion")
88 | implementation("androidx.compose.ui:ui-tooling-preview:$composeVersion")
89 | implementation("androidx.activity:activity-compose:1.5.1")
90 | implementation("androidx.datastore:datastore-preferences:1.0.0")
91 |
92 | val lifecycleVersion = "2.6.0-alpha01"
93 |
94 | implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycleVersion")
95 | implementation("androidx.lifecycle:lifecycle-viewmodel-compose:$lifecycleVersion")
96 | implementation("androidx.lifecycle:lifecycle-livedata-ktx:$lifecycleVersion")
97 | implementation("androidx.lifecycle:lifecycle-runtime-ktx:$lifecycleVersion")
98 | implementation("androidx.lifecycle:lifecycle-viewmodel-savedstate:$lifecycleVersion")
99 | kapt("androidx.lifecycle:lifecycle-compiler:$lifecycleVersion")
100 |
101 | val libsuVersion = "5.0.2"
102 | implementation("com.github.topjohnwu.libsu:core:$libsuVersion")
103 | implementation("com.github.topjohnwu.libsu:service:$libsuVersion")
104 | implementation("com.github.topjohnwu.libsu:nio:$libsuVersion")
105 |
106 | val coilVersion = "2.2.1"
107 | implementation("io.coil-kt:coil:$coilVersion")
108 | implementation("io.coil-kt:coil-compose:$coilVersion")
109 | implementation("io.coil-kt:coil-svg:$coilVersion")
110 |
111 | implementation("com.google.accompanist:accompanist-swiperefresh:0.26.2-beta")
112 |
113 | testImplementation("junit:junit:4.13.2")
114 | androidTestImplementation("androidx.test.ext:junit:1.1.3")
115 | androidTestImplementation("androidx.test.espresso:espresso-core:3.4.0")
116 | androidTestImplementation("androidx.compose.ui:ui-test-junit4:$composeVersion")
117 | debugImplementation("androidx.compose.ui:ui-tooling:$composeVersion")
118 | debugImplementation("androidx.compose.ui:ui-test-manifest:$composeVersion")
119 | }
--------------------------------------------------------------------------------
/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/fivecc/tools/shortcut_helper/ExampleInstrumentedTest.kt:
--------------------------------------------------------------------------------
1 | package fivecc.tools.shortcut_helper
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("fivecc.tools.shortcut_helper", appContext.packageName)
23 | }
24 | }
--------------------------------------------------------------------------------
/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
8 |
9 |
20 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
--------------------------------------------------------------------------------
/app/src/main/aidl/fivecc/tools/shortcut_helper/IRootHelper.aidl:
--------------------------------------------------------------------------------
1 | // IRootHelper.aidl
2 | package fivecc.tools.shortcut_helper;
3 |
4 | import android.content.pm.ShortcutInfo;
5 | import android.os.ParcelFileDescriptor;
6 |
7 | // Declare any non-default types here with import statements
8 |
9 | interface IRootHelper {
10 | List getShortcuts(String method, int user, int flags);
11 | ParcelFileDescriptor getShortcutIconFd(String packageName, String id, int userId);
12 | void startShortcut(in ShortcutInfo shortcutInfo);
13 | }
--------------------------------------------------------------------------------
/app/src/main/ic_launcher-playstore.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/5ec1cff/AndroidShortcutHelper/a9a3e9babe33805a447aee36985b24cc7b62cd55/app/src/main/ic_launcher-playstore.png
--------------------------------------------------------------------------------
/app/src/main/java/fivecc/tools/shortcut_helper/App.kt:
--------------------------------------------------------------------------------
1 | package fivecc.tools.shortcut_helper
2 |
3 | import android.app.Application
4 | import android.content.Context
5 | import android.os.Build
6 | import coil.ImageLoader
7 | import coil.ImageLoaderFactory
8 | import coil.decode.SvgDecoder
9 | import coil.disk.DiskCache
10 | import coil.memory.MemoryCache
11 | import fivecc.tools.shortcut_helper.coil.ShortcutIconFetcher
12 | import fivecc.tools.shortcut_helper.coil.ShortcutInfoKeyer
13 | import org.lsposed.hiddenapibypass.HiddenApiBypass
14 |
15 | class App : Application(), ImageLoaderFactory {
16 | companion object {
17 | lateinit var instance: Application
18 | }
19 | override fun attachBaseContext(base: Context?) {
20 | super.attachBaseContext(base)
21 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
22 | HiddenApiBypass.addHiddenApiExemptions("")
23 | }
24 | instance = this
25 | }
26 |
27 | override fun newImageLoader(): ImageLoader {
28 | return ImageLoader.Builder(this)
29 | .components {
30 | add(ShortcutIconFetcher.Factory())
31 | add(SvgDecoder.Factory())
32 | add(ShortcutInfoKeyer())
33 | }.build()
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/app/src/main/java/fivecc/tools/shortcut_helper/AppInfoCache.kt:
--------------------------------------------------------------------------------
1 | package fivecc.tools.shortcut_helper
2 |
3 | import android.content.pm.ApplicationInfo
4 | import java.util.concurrent.ConcurrentHashMap
5 |
6 | object AppInfoCache {
7 | private val appInfoCache = ConcurrentHashMap()
8 | private val appLabelCache = ConcurrentHashMap()
9 |
10 | fun getAppInfo(packageName: String): ApplicationInfo {
11 | appInfoCache[packageName]?.let { return it }
12 | val pm = App.instance.packageManager
13 | return pm.getApplicationInfo(packageName, 0).also { appInfoCache[packageName] = it }
14 | }
15 |
16 | fun getAppLabel(packageName: String): String {
17 | appLabelCache[packageName]?.let { return it }
18 | val pm = App.instance.packageManager
19 | val appInfo = getAppInfo(packageName)
20 | return appInfo.loadLabel(pm).toString().also { appLabelCache[packageName] = it }
21 | }
22 | }
--------------------------------------------------------------------------------
/app/src/main/java/fivecc/tools/shortcut_helper/MainActivity.kt:
--------------------------------------------------------------------------------
1 | @file:Suppress("DEPRECATION")
2 |
3 | package fivecc.tools.shortcut_helper
4 |
5 | import android.app.Activity
6 | import android.content.Intent
7 | import android.content.pm.ShortcutInfo
8 | import android.os.Bundle
9 | import androidx.activity.ComponentActivity
10 | import androidx.activity.compose.setContent
11 | import androidx.compose.foundation.clickable
12 | import androidx.compose.foundation.layout.*
13 | import androidx.compose.foundation.lazy.LazyColumn
14 | import androidx.compose.foundation.lazy.items
15 | import androidx.compose.foundation.rememberScrollState
16 | import androidx.compose.foundation.shape.RoundedCornerShape
17 | import androidx.compose.foundation.verticalScroll
18 | import androidx.compose.material.icons.Icons
19 | import androidx.compose.material.icons.filled.Settings
20 | import androidx.compose.material3.*
21 | import androidx.compose.runtime.*
22 | import androidx.compose.runtime.livedata.observeAsState
23 | import androidx.compose.ui.Alignment
24 | import androidx.compose.ui.Modifier
25 | import androidx.compose.ui.draw.shadow
26 | import androidx.compose.ui.platform.LocalClipboardManager
27 | import androidx.compose.ui.platform.LocalContext
28 | import androidx.compose.ui.text.AnnotatedString
29 | import androidx.compose.ui.text.font.FontWeight
30 | import androidx.compose.ui.text.style.TextAlign
31 | import androidx.compose.ui.unit.dp
32 | import androidx.compose.ui.window.Dialog
33 | import androidx.lifecycle.viewmodel.compose.viewModel
34 | import coil.compose.AsyncImage
35 | import com.google.accompanist.swiperefresh.SwipeRefresh
36 | import com.google.accompanist.swiperefresh.rememberSwipeRefreshState
37 | import fivecc.tools.shortcut_helper.ui.theme.ShortcutTheme
38 | import fivecc.tools.shortcut_helper.utils.getLabel
39 | import kotlinx.coroutines.launch
40 |
41 | class MainActivity : ComponentActivity() {
42 | @OptIn(ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class)
43 | override fun onCreate(savedInstanceState: Bundle?) {
44 | super.onCreate(savedInstanceState)
45 | RootHelperService.start()
46 | val selectButtonText: String
47 | val action = intent.action
48 | setContent {
49 | ShortcutTheme {
50 | Surface(
51 | modifier = Modifier.fillMaxSize(),
52 | color = MaterialTheme.colorScheme.background
53 | ) {
54 | var settingsDialogShown by remember { mutableStateOf(false) }
55 | val snackBarHostState = remember { SnackbarHostState() }
56 | Scaffold(
57 | snackbarHost = { SnackbarHost(hostState = snackBarHostState) },
58 | topBar = {
59 | TopAppBar(title = { Text("Shortcuts") },
60 | actions = {
61 | IconButton(onClick = { settingsDialogShown = true }) {
62 | Icon(Icons.Filled.Settings, contentDescription = "settings")
63 | }
64 | },
65 | modifier = Modifier.shadow(8.dp))
66 | },
67 | content = {
68 | Box(modifier = Modifier
69 | .fillMaxSize()
70 | .padding(it)
71 | .consumedWindowInsets(it),
72 | contentAlignment = Alignment.Center
73 | ) {
74 | val state by RootHelperService.serviceState.observeAsState()
75 | when (state) {
76 | ServiceState.RUNNING -> {
77 | ShortcutScreen(snackBarHostState = snackBarHostState, action = action)
78 | }
79 | ServiceState.STARTING -> {
80 | TipScreen("service is starting")
81 | }
82 | ServiceState.STOPPED -> {
83 | TipScreen("service is unavailable (root permission required)")
84 | }
85 | else -> {
86 | TipScreen("WTF?")
87 | }
88 | }
89 | }
90 | if (settingsDialogShown) {
91 | BaseDialog(onDismiss = { settingsDialogShown = false }) {
92 | Box(modifier = Modifier
93 | .defaultMinSize(minHeight = 200.dp)
94 | .padding(16.dp)
95 | ) {
96 | MyListPreference(listPreference = Settings.WORK_MODE)
97 | }
98 | }
99 | }
100 | }
101 | )
102 | }
103 | }
104 | }
105 | }
106 | }
107 |
108 | @Composable
109 | fun TipScreen(text: String) {
110 | Text(text = text, textAlign = TextAlign.Center, modifier = Modifier.wrapContentHeight())
111 | }
112 |
113 | @Composable
114 | fun ShortcutScreen(
115 | snackBarHostState: SnackbarHostState,
116 | action: String?
117 | ) {
118 | val viewModel: MainActivityViewModel = viewModel()
119 | val isRefreshing by viewModel.isRefreshing.collectAsState()
120 | val shortcuts = viewModel.shortcutList
121 | var dialogShowing by remember { mutableStateOf(null) }
122 | val context = LocalContext.current
123 | val settings = Settings(context)
124 | val workMode by settings.getValue(Settings.WORK_MODE.preferencesKey).collectAsState(initial = null)
125 | val scope = rememberCoroutineScope()
126 | val selectButtonText = if (action == Intent.ACTION_CREATE_SHORTCUT) "select" else "launch"
127 | if (workMode == null) {
128 | Box(
129 | modifier = Modifier.fillMaxSize(),
130 | contentAlignment = Alignment.Center
131 | ) {
132 | TipScreen(text = "Please configure work mode first")
133 | }
134 | } else {
135 | SwipeRefresh(state = rememberSwipeRefreshState(isRefreshing),
136 | onRefresh = {
137 | viewModel.loadShortcuts(workMode!!, onError = {
138 | scope.launch {
139 | snackBarHostState.showSnackbar("Error: ${it.message}")
140 | }
141 | })
142 | }
143 | ) {
144 | if (shortcuts.isEmpty()) {
145 | Box(
146 | modifier = Modifier
147 | .fillMaxSize()
148 | .verticalScroll(rememberScrollState()),
149 | contentAlignment = Alignment.Center
150 | ) {
151 | TipScreen(text = "Nothing to show, try refresh")
152 | }
153 | } else {
154 | LazyColumn(
155 | modifier = Modifier
156 | .fillMaxWidth()
157 | .fillMaxHeight()
158 | ) {
159 | items(shortcuts) { s ->
160 | ShortcutCard(shortcut = s, onClick = { dialogShowing = it })
161 | }
162 | }
163 | }
164 | }
165 | }
166 | dialogShowing?.let { s ->
167 | ShortcutDialog(shortcut = s,
168 | onDismiss = { dialogShowing = null },
169 | onSelected = {
170 | dialogShowing = null
171 | if (action == Intent.ACTION_CREATE_SHORTCUT) {
172 | if (context is Activity) {
173 | context.setResult(
174 | ComponentActivity.RESULT_OK, Intent()
175 | .putExtra(Intent.EXTRA_SHORTCUT_INTENT, it.intent)
176 | .putExtra(Intent.EXTRA_SHORTCUT_NAME, it.getLabel())
177 | )
178 | context.finish()
179 | }
180 | } else {
181 | runCatching {
182 | s.intent?.also { intent ->
183 | val launchIntent = Intent(intent)
184 | launchIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
185 | context.startActivity(launchIntent)
186 | }
187 | }.onFailure { t ->
188 | scope.launch {
189 | snackBarHostState.showSnackbar("Error: ${t.message}")
190 | }
191 | }
192 | }
193 | },
194 | selectButtonText = selectButtonText
195 | )
196 | }
197 | }
198 |
199 | @Composable
200 | fun ShortcutCard(
201 | shortcut: ShortcutInfo,
202 | onClick: ((ShortcutInfo) -> Unit)? = null
203 | ) {
204 | Row(modifier = Modifier
205 | .clickable(onClick = { onClick?.invoke(shortcut) })
206 | .padding(all = 8.dp)
207 | .fillMaxWidth()) {
208 | AsyncImage(
209 | model = shortcut,
210 | contentDescription = "icon",
211 | modifier = Modifier.size(40.dp)
212 | )
213 | Spacer(modifier = Modifier.width(8.dp))
214 | Column {
215 | Text(text = shortcut.getLabel(), maxLines = 1, fontWeight = FontWeight.Bold)
216 | Spacer(modifier = Modifier.height(4.dp))
217 | Text(text = AppInfoCache.getAppLabel(shortcut.`package`))
218 | }
219 | }
220 | }
221 |
222 | @Composable
223 | fun BaseDialog(
224 | onDismiss: () -> Unit,
225 | content: @Composable () -> Unit
226 | ) {
227 | Dialog(onDismissRequest = { onDismiss() }) {
228 | Card(modifier = Modifier.shadow(8.dp), shape = RoundedCornerShape(12.dp)) {
229 | content()
230 | }
231 | }
232 | }
233 |
234 | @Composable
235 | fun ShortcutDialog(
236 | shortcut: ShortcutInfo,
237 | onDismiss: () -> Unit,
238 | selectButtonText: String? = null,
239 | onSelected: ((ShortcutInfo) -> Unit)? = null
240 | ) {
241 | Dialog(onDismissRequest = { onDismiss() }) {
242 | Card(modifier = Modifier.shadow(8.dp), shape = RoundedCornerShape(12.dp)) {
243 | Column(modifier = Modifier.padding(16.dp)) {
244 | Row(modifier = Modifier.fillMaxWidth()) {
245 | AsyncImage(
246 | model = shortcut,
247 | contentDescription = "icon",
248 | modifier = Modifier.size(50.dp)
249 | )
250 | Spacer(modifier = Modifier.width(8.dp))
251 | Column {
252 | Text(
253 | text = shortcut.shortLabel.toString(),
254 | maxLines = 1,
255 | fontWeight = FontWeight.Bold
256 | )
257 | Spacer(modifier = Modifier.height(4.dp))
258 | Text(text = AppInfoCache.getAppLabel(shortcut.`package`))
259 | }
260 | }
261 | Spacer(modifier = Modifier.height(4.dp))
262 | MyTextField(title = "Package Name", content = shortcut.`package`)
263 | MyTextField(title = "ID", content = shortcut.id)
264 | MyTextField(title = "Uri", content = "${shortcut.intent?.toUri(0)}")
265 | if (onSelected != null && selectButtonText != null) {
266 | Box(contentAlignment = Alignment.BottomEnd, modifier = Modifier.fillMaxWidth()) {
267 | Button(
268 | onClick = { onSelected.invoke(shortcut) },
269 | shape = RoundedCornerShape(8.dp),
270 | ) {
271 | Text(text = selectButtonText)
272 | }
273 | }
274 | }
275 | }
276 | }
277 | }
278 | }
279 |
280 | @OptIn(ExperimentalMaterial3Api::class)
281 | @Composable
282 | fun MyTextField(title: String, content: String) {
283 | val clipboard = LocalClipboardManager.current
284 | OutlinedTextField(value = content, onValueChange = {}, enabled = false,
285 | label = { Text(text = title) }, modifier = Modifier
286 | .fillMaxWidth()
287 | .clickable {
288 | clipboard.setText(AnnotatedString(content))
289 | })
290 | }
291 |
--------------------------------------------------------------------------------
/app/src/main/java/fivecc/tools/shortcut_helper/MainActivityViewModel.kt:
--------------------------------------------------------------------------------
1 | package fivecc.tools.shortcut_helper
2 |
3 | import android.content.pm.ShortcutInfo
4 | import android.os.UserHandleHidden
5 | import androidx.compose.runtime.mutableStateListOf
6 | import androidx.lifecycle.ViewModel
7 | import androidx.lifecycle.viewModelScope
8 | import fivecc.tools.shortcut_helper.utils.MATCH_ALL
9 | import kotlinx.coroutines.Dispatchers
10 | import kotlinx.coroutines.flow.MutableStateFlow
11 | import kotlinx.coroutines.flow.StateFlow
12 | import kotlinx.coroutines.flow.asStateFlow
13 | import kotlinx.coroutines.launch
14 | import kotlinx.coroutines.withContext
15 |
16 | class MainActivityViewModel : ViewModel() {
17 | private val _isRefreshing = MutableStateFlow(false)
18 |
19 | val isRefreshing: StateFlow
20 | get() = _isRefreshing.asStateFlow()
21 |
22 | val shortcutList = mutableStateListOf()
23 |
24 | fun loadShortcuts(
25 | method: String,
26 | onError: ((t: Throwable) -> Unit)? = null
27 | ) {
28 | viewModelScope.launch {
29 | _isRefreshing.emit(true)
30 | val newList = kotlin.runCatching {
31 | withContext(Dispatchers.IO) {
32 | RootHelperService.helper?.getShortcuts(
33 | method,
34 | UserHandleHidden.myUserId(),
35 | MATCH_ALL
36 | )
37 | }
38 | }.onFailure { onError?.invoke(it) }.getOrNull()
39 | shortcutList.clear()
40 | newList?.also { shortcutList.addAll(it) }
41 | _isRefreshing.emit(false)
42 | }
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/app/src/main/java/fivecc/tools/shortcut_helper/RootHelperService.kt:
--------------------------------------------------------------------------------
1 | @file:Suppress("DEPRECATION", "CAST_NEVER_SUCCEEDS")
2 |
3 | package fivecc.tools.shortcut_helper
4 |
5 | import android.content.ComponentName
6 | import android.content.Context
7 | import android.content.Intent
8 | import android.content.ServiceConnection
9 | import android.content.pm.ILauncherApps
10 | import android.content.pm.IShortcutService
11 | import android.content.pm.PackageManagerHidden
12 | import android.content.pm.ShortcutInfo
13 | import android.os.Build
14 | import android.os.IBinder
15 | import android.os.ParcelFileDescriptor
16 | import android.os.ServiceManager
17 | import android.system.Os
18 | import androidx.annotation.MainThread
19 | import androidx.lifecycle.MutableLiveData
20 | import com.topjohnwu.superuser.ipc.RootService
21 | import fivecc.tools.shortcut_helper.utils.ShortcutParser
22 | import fivecc.tools.shortcut_helper.utils.getShortcutInfoCompat
23 | import fivecc.tools.shortcut_helper.utils.getUserId
24 |
25 | enum class ServiceState {
26 | STOPPED,
27 | STARTING,
28 | RUNNING
29 | }
30 |
31 | @Suppress("Unchecked_Cast")
32 | class RootHelperService : RootService() {
33 | companion object {
34 | const val METHOD_PARSE_FILE = "parse_file"
35 | const val METHOD_SYSTEM_API = "system_api"
36 | val serviceState = MutableLiveData(ServiceState.STOPPED)
37 | var helper: IRootHelper? = null
38 | private set
39 | private val mConnection = object : ServiceConnection, IBinder.DeathRecipient {
40 | override fun onServiceConnected(p0: ComponentName?, binder: IBinder) {
41 | binder.linkToDeath(this, 0)
42 | helper = IRootHelper.Stub.asInterface(binder)
43 | serviceState.value = ServiceState.RUNNING
44 | }
45 |
46 | override fun onServiceDisconnected(p0: ComponentName?) {
47 | helper = null
48 | serviceState.value = ServiceState.STOPPED
49 | }
50 |
51 | override fun binderDied() {
52 | helper = null
53 | serviceState.postValue(ServiceState.STOPPED)
54 | }
55 | }
56 |
57 | @MainThread
58 | fun start() {
59 | if (serviceState.value == ServiceState.STOPPED) {
60 | bind(
61 | Intent().setComponent(
62 | ComponentName(
63 | BuildConfig.APPLICATION_ID,
64 | RootHelperService::class.java.name
65 | )
66 | ),
67 | mConnection
68 | )
69 | serviceState.value = ServiceState.STARTING
70 | }
71 | }
72 | }
73 |
74 | private val shortcutService by lazy {
75 | IShortcutService.Stub.asInterface(ServiceManager.getService(Context.SHORTCUT_SERVICE))
76 | }
77 |
78 | private val launcherAppsService by lazy {
79 | ILauncherApps.Stub.asInterface(ServiceManager.getService(Context.LAUNCHER_APPS_SERVICE))
80 | }
81 |
82 | private val mHelper = object : IRootHelper.Stub() {
83 | override fun getShortcuts(method: String, user: Int, flags: Int): MutableList? {
84 | return when (method) {
85 | METHOD_PARSE_FILE -> {
86 | ShortcutParser.loadUserLocked(user)
87 | }
88 | METHOD_SYSTEM_API -> {
89 | val result = mutableListOf()
90 | (packageManager as PackageManagerHidden).getInstalledPackagesAsUser(0, user).forEach {
91 | result.addAll(shortcutService.getShortcutInfoCompat(it.packageName, user, flags))
92 | }
93 | result
94 | }
95 | else -> {
96 | throw IllegalArgumentException("unknown method $method")
97 | }
98 | }
99 | }
100 |
101 | override fun getShortcutIconFd(
102 | packageName: String?,
103 | id: String?,
104 | userId: Int
105 | ): ParcelFileDescriptor {
106 | return launcherAppsService.getShortcutIconFd("android", packageName, id, userId)
107 | }
108 |
109 | override fun startShortcut(shortcutInfo: ShortcutInfo) {
110 | // Start shortcuts by this API may be rejected by system
111 | // for starting from background, even if we're system ...
112 | val id = shortcutInfo.id
113 | val packageName = shortcutInfo.`package`
114 | val userId = shortcutInfo.getUserId()
115 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
116 | launcherAppsService.startShortcut(
117 | "android", packageName, null,
118 | id, null, null, userId)
119 | } else {
120 | launcherAppsService.startShortcut(
121 | "android", packageName,
122 | id, null, null, userId)
123 | }
124 | }
125 | }
126 |
127 | override fun onCreate() {
128 | super.onCreate()
129 | Os.seteuid(1000)
130 | }
131 |
132 | override fun onBind(intent: Intent): IBinder = mHelper.asBinder()
133 | }
--------------------------------------------------------------------------------
/app/src/main/java/fivecc/tools/shortcut_helper/Settings.kt:
--------------------------------------------------------------------------------
1 | package fivecc.tools.shortcut_helper
2 |
3 | import android.content.Context
4 | import androidx.annotation.StringRes
5 | import androidx.compose.foundation.clickable
6 | import androidx.compose.foundation.layout.*
7 | import androidx.compose.material3.DropdownMenu
8 | import androidx.compose.material3.DropdownMenuItem
9 | import androidx.compose.material3.Text
10 | import androidx.compose.runtime.*
11 | import androidx.compose.ui.Modifier
12 | import androidx.compose.ui.platform.LocalContext
13 | import androidx.compose.ui.res.stringResource
14 | import androidx.compose.ui.text.style.TextAlign
15 | import androidx.compose.ui.unit.dp
16 | import androidx.datastore.core.DataStore
17 | import androidx.datastore.preferences.core.Preferences
18 | import androidx.datastore.preferences.core.edit
19 | import androidx.datastore.preferences.core.stringPreferencesKey
20 | import androidx.datastore.preferences.preferencesDataStore
21 | import kotlinx.coroutines.flow.Flow
22 | import kotlinx.coroutines.flow.map
23 | import kotlinx.coroutines.launch
24 |
25 | data class ListPreference(
26 | val preferencesKey: Preferences.Key,
27 | @StringRes val titleRes: Int,
28 | val values: Map
29 | )
30 |
31 | class Settings(private val context: Context) {
32 | companion object {
33 | private val Context.dataStore: DataStore by preferencesDataStore(name = "settings")
34 | val WORK_MODE = ListPreference(
35 | stringPreferencesKey("work_mode"),
36 | R.string.work_mode_title,
37 | mapOf(
38 | RootHelperService.METHOD_SYSTEM_API to R.string.work_mode_system_api,
39 | RootHelperService.METHOD_PARSE_FILE to R.string.work_mode_parse_xml
40 | )
41 | )
42 | }
43 |
44 | fun getValue(key: Preferences.Key, defaultValue: String? = null): Flow = context.dataStore.data
45 | .map {
46 | it[key] ?: defaultValue
47 | }
48 |
49 | suspend fun setValue(key: Preferences.Key, value: String) {
50 | context.dataStore.edit {
51 | it[key] = value
52 | }
53 | }
54 | }
55 |
56 | @Composable
57 | fun MyListPreference(
58 | listPreference: ListPreference
59 | ) {
60 | var expanded by remember { mutableStateOf(false) }
61 | val context = LocalContext.current
62 | val dataStore = Settings(context)
63 | val scope = rememberCoroutineScope()
64 | val item by dataStore.getValue(listPreference.preferencesKey).collectAsState(initial = null)
65 | Row(modifier = Modifier
66 | .clickable { expanded = true }
67 | .padding(8.dp)) {
68 | Text(text = stringResource(id = listPreference.titleRes))
69 | Spacer(Modifier.weight(1f))
70 | Box(modifier = Modifier.width(100.dp)) {
71 | Text(text = stringResource(id = listPreference.values[item] ?: R.string.empty_string), textAlign = TextAlign.Right, modifier = Modifier.fillMaxWidth())
72 | DropdownMenu(
73 | expanded = expanded,
74 | onDismissRequest = { expanded = false }) {
75 | listPreference.values.forEach { (k, v) ->
76 | DropdownMenuItem(
77 | text = { Text(text = stringResource(id = v)) },
78 | onClick = {
79 | scope.launch { dataStore.setValue(listPreference.preferencesKey, k) }
80 | expanded = false
81 | }
82 | )
83 | }
84 | }
85 | }
86 | }
87 | }
88 |
--------------------------------------------------------------------------------
/app/src/main/java/fivecc/tools/shortcut_helper/coil/ShortcutIconFetcher.kt:
--------------------------------------------------------------------------------
1 | package fivecc.tools.shortcut_helper.coil
2 |
3 | import android.content.pm.ShortcutInfo
4 | import android.os.ParcelFileDescriptor
5 | import android.util.Log
6 | import android.webkit.MimeTypeMap
7 | import coil.ImageLoader
8 | import coil.annotation.ExperimentalCoilApi
9 | import coil.decode.DataSource
10 | import coil.fetch.FetchResult
11 | import coil.fetch.Fetcher
12 | import coil.fetch.SourceResult
13 | import coil.decode.ImageSource
14 | import coil.fetch.DrawableResult
15 | import coil.request.Options
16 | import fivecc.tools.shortcut_helper.RootHelperService
17 | import fivecc.tools.shortcut_helper.utils.*
18 | import okio.buffer
19 | import okio.source
20 |
21 | // frameworks/base/core/java/android/content/pm/LauncherApps.java getShortcutIconDrawable
22 | class ShortcutIconFetcher(
23 | private val info: ShortcutInfo,
24 | private val options: Options
25 | ) : Fetcher {
26 | companion object {
27 | private const val TAG = "ShortcutIconFetcher"
28 | }
29 |
30 | @OptIn(ExperimentalCoilApi::class)
31 | override suspend fun fetch(): FetchResult? {
32 | if (info.hasIconFile()) {
33 | RootHelperService.helper?.getShortcutIconFd(
34 | info.`package`, info.id, 0
35 | )?.also { fd ->
36 | return SourceResult(
37 | source = ImageSource(
38 | source = ParcelFileDescriptor.AutoCloseInputStream(fd).source()
39 | .buffer(),
40 | context = options.context,
41 | metadata = null
42 | ),
43 | mimeType = null,
44 | dataSource = DataSource.DISK
45 | )
46 | }
47 | } else if (info.hasIconResource()) {
48 | val res = options.context.packageManager.getResourcesForApplication(info.`package`)
49 | return DrawableResult(
50 | drawable = res.getDrawable(info.getIconResourceId()),
51 | isSampled = false,
52 | dataSource = DataSource.MEMORY
53 | )
54 | }
55 | return null
56 | }
57 |
58 | class Factory : Fetcher.Factory {
59 | override fun create(
60 | data: ShortcutInfo,
61 | options: Options,
62 | imageLoader: ImageLoader
63 | ): Fetcher {
64 | return ShortcutIconFetcher(data, options)
65 | }
66 | }
67 | }
--------------------------------------------------------------------------------
/app/src/main/java/fivecc/tools/shortcut_helper/coil/ShortcutInfoKeyer.kt:
--------------------------------------------------------------------------------
1 | package fivecc.tools.shortcut_helper.coil
2 |
3 | import android.content.pm.ShortcutInfo
4 | import coil.key.Keyer
5 | import coil.request.Options
6 |
7 | class ShortcutInfoKeyer : Keyer {
8 | override fun key(data: ShortcutInfo, options: Options): String {
9 | return "${data.`package`}_${data.id}"
10 | }
11 | }
--------------------------------------------------------------------------------
/app/src/main/java/fivecc/tools/shortcut_helper/ui/theme/Color.kt:
--------------------------------------------------------------------------------
1 | package fivecc.tools.shortcut_helper.ui.theme
2 |
3 | import androidx.compose.ui.graphics.Color
4 |
5 | val Purple80 = Color(0xFFD0BCFF)
6 | val PurpleGrey80 = Color(0xFFCCC2DC)
7 | val Pink80 = Color(0xFFEFB8C8)
8 |
9 | val Purple40 = Color(0xFF6650a4)
10 | val PurpleGrey40 = Color(0xFF625b71)
11 | val Pink40 = Color(0xFF7D5260)
--------------------------------------------------------------------------------
/app/src/main/java/fivecc/tools/shortcut_helper/ui/theme/Theme.kt:
--------------------------------------------------------------------------------
1 | package fivecc.tools.shortcut_helper.ui.theme
2 |
3 | import android.app.Activity
4 | import android.os.Build
5 | import androidx.compose.foundation.isSystemInDarkTheme
6 | import androidx.compose.material3.MaterialTheme
7 | import androidx.compose.material3.darkColorScheme
8 | import androidx.compose.material3.dynamicDarkColorScheme
9 | import androidx.compose.material3.dynamicLightColorScheme
10 | import androidx.compose.material3.lightColorScheme
11 | import androidx.compose.runtime.Composable
12 | import androidx.compose.runtime.SideEffect
13 | import androidx.compose.ui.graphics.toArgb
14 | import androidx.compose.ui.platform.LocalContext
15 | import androidx.compose.ui.platform.LocalView
16 | import androidx.core.view.ViewCompat
17 |
18 | private val DarkColorScheme = darkColorScheme(
19 | primary = Purple80,
20 | secondary = PurpleGrey80,
21 | tertiary = Pink80
22 | )
23 |
24 | private val LightColorScheme = lightColorScheme(
25 | primary = Purple40,
26 | secondary = PurpleGrey40,
27 | tertiary = Pink40
28 |
29 | /* Other default colors to override
30 | background = Color(0xFFFFFBFE),
31 | surface = Color(0xFFFFFBFE),
32 | onPrimary = Color.White,
33 | onSecondary = Color.White,
34 | onTertiary = Color.White,
35 | onBackground = Color(0xFF1C1B1F),
36 | onSurface = Color(0xFF1C1B1F),
37 | */
38 | )
39 |
40 | @Composable
41 | fun ShortcutTheme(
42 | darkTheme: Boolean = isSystemInDarkTheme(),
43 | // Dynamic color is available on Android 12+
44 | dynamicColor: Boolean = true,
45 | content: @Composable () -> Unit
46 | ) {
47 | val colorScheme = when {
48 | dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
49 | val context = LocalContext.current
50 | if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
51 | }
52 | darkTheme -> DarkColorScheme
53 | else -> LightColorScheme
54 | }
55 | val view = LocalView.current
56 | if (!view.isInEditMode) {
57 | SideEffect {
58 | (view.context as Activity).window.statusBarColor = colorScheme.primary.toArgb()
59 | ViewCompat.getWindowInsetsController(view)?.isAppearanceLightStatusBars = darkTheme
60 | }
61 | }
62 |
63 | MaterialTheme(
64 | colorScheme = colorScheme,
65 | typography = Typography,
66 | content = content
67 | )
68 | }
--------------------------------------------------------------------------------
/app/src/main/java/fivecc/tools/shortcut_helper/ui/theme/Type.kt:
--------------------------------------------------------------------------------
1 | package fivecc.tools.shortcut_helper.ui.theme
2 |
3 | import androidx.compose.material3.Typography
4 | import androidx.compose.ui.text.TextStyle
5 | import androidx.compose.ui.text.font.FontFamily
6 | import androidx.compose.ui.text.font.FontWeight
7 | import androidx.compose.ui.unit.sp
8 |
9 | // Set of Material typography styles to start with
10 | val Typography = Typography(
11 | bodyLarge = TextStyle(
12 | fontFamily = FontFamily.Default,
13 | fontWeight = FontWeight.Normal,
14 | fontSize = 16.sp,
15 | lineHeight = 24.sp,
16 | letterSpacing = 0.5.sp
17 | )
18 | /* Other default text styles to override
19 | titleLarge = TextStyle(
20 | fontFamily = FontFamily.Default,
21 | fontWeight = FontWeight.Normal,
22 | fontSize = 22.sp,
23 | lineHeight = 28.sp,
24 | letterSpacing = 0.sp
25 | ),
26 | labelSmall = TextStyle(
27 | fontFamily = FontFamily.Default,
28 | fontWeight = FontWeight.Medium,
29 | fontSize = 11.sp,
30 | lineHeight = 16.sp,
31 | letterSpacing = 0.5.sp
32 | )
33 | */
34 | )
--------------------------------------------------------------------------------
/app/src/main/java/fivecc/tools/shortcut_helper/utils/PrefUtils.kt:
--------------------------------------------------------------------------------
1 | package fivecc.tools.shortcut_helper.utils
2 |
3 | import android.util.Log
4 |
5 | const val TRACE_TAG = "Trace"
6 |
7 | inline fun trace(desc: String, block: () -> T): T {
8 | Log.d(TRACE_TAG, "on ${Thread.currentThread()}: $desc")
9 | val startTime = System.currentTimeMillis()
10 | try {
11 | return block()
12 | } finally {
13 | val time = System.currentTimeMillis() - startTime
14 | Log.d(TRACE_TAG, "execute done ($time): $desc")
15 | }
16 | }
--------------------------------------------------------------------------------
/app/src/main/java/fivecc/tools/shortcut_helper/utils/ShortcutInfoHelper.kt:
--------------------------------------------------------------------------------
1 | @file:Suppress("CAST_NEVER_SUCCEEDS", "Unchecked_Cast")
2 |
3 | package fivecc.tools.shortcut_helper.utils
4 |
5 | import android.app.Person
6 | import android.content.ComponentName
7 | import android.content.Intent
8 | import android.content.LocusId
9 | import android.content.pm.*
10 | import android.content.res.Resources
11 | import android.graphics.drawable.Icon
12 | import android.os.Build
13 | import android.os.PersistableBundle
14 | import android.util.Log
15 | import fivecc.tools.shortcut_helper.App
16 |
17 | const val MATCH_PINNED = 1
18 | const val MATCH_DYNAMIC = 1 shl 1
19 | const val MATCH_MANIFEST = 1 shl 2
20 | const val MATCH_CACHED = 1 shl 3
21 | const val MATCH_ALL = MATCH_PINNED or MATCH_DYNAMIC or MATCH_MANIFEST or MATCH_CACHED
22 |
23 | /**
24 | * Use IShortcutManager API to get ShortcutInfo s
25 | * which `bitmapPath` are empty
26 | */
27 | fun IShortcutService.getShortcutInfoCompat(
28 | packageName: String, userId: Int, matchFlags: Int = MATCH_ALL
29 | ): List {
30 | if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
31 | val result = mutableListOf()
32 | if (matchFlags and MATCH_PINNED != 0) {
33 | result.addAll(getPinnedShortcuts(packageName, userId).list as List)
34 | }
35 | if (matchFlags and MATCH_MANIFEST != 0) {
36 | result.addAll(getManifestShortcuts(packageName, userId).list as List)
37 | }
38 | if (matchFlags and MATCH_DYNAMIC != 0) {
39 | result.addAll(getDynamicShortcuts(packageName, userId).list as List)
40 | }
41 | return result
42 | } else {
43 | var flags = 0
44 | if (matchFlags and MATCH_PINNED != 0) {
45 | flags = flags or ShortcutManager.FLAG_MATCH_PINNED
46 | }
47 | if (matchFlags and MATCH_MANIFEST != 0) {
48 | flags = flags or ShortcutManager.FLAG_MATCH_MANIFEST
49 | }
50 | if (matchFlags and MATCH_DYNAMIC != 0) {
51 | flags = flags or ShortcutManager.FLAG_MATCH_DYNAMIC
52 | }
53 | if (matchFlags and MATCH_CACHED != 0) {
54 | flags = flags or ShortcutManager.FLAG_MATCH_CACHED
55 | }
56 | if (Build.VERSION.SDK_INT == Build.VERSION_CODES.R || Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
57 | return getShortcuts(packageName, flags, userId).list as List
58 | } else {
59 | return (this as IShortcutServiceForS).getShortcuts(packageName, flags, userId).get().list as List
60 | }
61 | }
62 | }
63 |
64 | fun ShortcutInfo.hasIconFile(): Boolean {
65 | this as ShortcutInfoHidden
66 | return hasIconFile()
67 | }
68 |
69 | fun ShortcutInfo.hasIconResource(): Boolean {
70 | this as ShortcutInfoHidden
71 | return hasIconResource()
72 | }
73 |
74 | fun ShortcutInfo.getIconResourceId(): Int {
75 | this as ShortcutInfoHidden
76 | return getIconResourceId()
77 | }
78 |
79 | fun ShortcutInfo.getUserId(): Int {
80 | this as ShortcutInfoHidden
81 | return userId
82 | }
83 |
84 | fun ShortcutInfo.getLabel(): String {
85 | shortLabel?.also { return it.toString() }
86 | longLabel?.also { return it.toString() }
87 | val packageName = `package`
88 | val defaultName = "Shortcut:$packageName:$id"
89 | this as ShortcutInfoHidden
90 | val pm = App.instance.packageManager
91 | val shortId = shortLabelResourceId
92 | val longId = longLabelResourceId
93 | val id = if (shortId != 0) shortId
94 | else if (longId != 0) longId
95 | else return defaultName
96 | try {
97 | return pm.getResourcesForApplication(packageName).getString(id)
98 | } catch (e: Resources.NotFoundException) {
99 | Log.e("ShortcutInfoHelper", "getLabel $id for $packageName not found", e)
100 | }
101 | return defaultName
102 | }
103 |
104 | fun newShortcutInfoCompat(
105 | userId: Int,
106 | id: String?,
107 | packageName: String?,
108 | activity: ComponentName?,
109 | icon: Icon?,
110 | title: CharSequence?,
111 | titleResId: Int,
112 | titleResName: String?,
113 | text: CharSequence?,
114 | textResId: Int,
115 | textResName: String?,
116 | disabledMessage: CharSequence?,
117 | disabledMessageResId: Int,
118 | disabledMessageResName: String?,
119 | categories: Set?,
120 | intentsWithExtras: Array?,
121 | rank: Int,
122 | extras: PersistableBundle?,
123 | lastChangedTimestamp: Long,
124 | flags: Int,
125 | iconResId: Int,
126 | iconResName: String?,
127 | bitmapPath: String?,
128 | iconUri: String?,
129 | disabledReason: Int,
130 | persons: Array?,
131 | locusId: LocusId?,
132 | ): ShortcutInfo {
133 | val compat = when (Build.VERSION.SDK_INT) {
134 | Build.VERSION_CODES.O,
135 | Build.VERSION_CODES.O_MR1 -> ShortcutInfoHidden(
136 | userId, id, packageName, activity, icon,
137 | title, titleResId, titleResName, text, textResId, textResName,
138 | disabledMessage, disabledMessageResId, disabledMessageResName,
139 | categories,
140 | intentsWithExtras,
141 | rank, extras, lastChangedTimestamp, flags,
142 | iconResId, iconResName, bitmapPath
143 | )
144 | Build.VERSION_CODES.P -> ShortcutInfoHidden(
145 | userId, id, packageName, activity, icon,
146 | title, titleResId, titleResName, text, textResId, textResName,
147 | disabledMessage, disabledMessageResId, disabledMessageResName,
148 | categories,
149 | intentsWithExtras,
150 | rank, extras, lastChangedTimestamp, flags,
151 | iconResId, iconResName, bitmapPath,
152 | disabledReason
153 | )
154 | Build.VERSION_CODES.Q -> ShortcutInfoHidden(
155 | userId, id, packageName, activity, icon,
156 | title, titleResId, titleResName, text, textResId, textResName,
157 | disabledMessage, disabledMessageResId, disabledMessageResName,
158 | categories,
159 | intentsWithExtras,
160 | rank, extras, lastChangedTimestamp, flags,
161 | iconResId, iconResName, bitmapPath,
162 | disabledReason, persons, locusId
163 | )
164 | Build.VERSION_CODES.R -> ShortcutInfoHidden(
165 | userId, id, packageName, activity, icon,
166 | title, titleResId, titleResName, text, textResId, textResName,
167 | disabledMessage, disabledMessageResId, disabledMessageResName,
168 | categories,
169 | intentsWithExtras,
170 | rank, extras, lastChangedTimestamp, flags,
171 | iconResId, iconResName, bitmapPath, iconUri,
172 | disabledReason, persons, locusId
173 | )
174 | Build.VERSION_CODES.S, Build.VERSION_CODES.S_V2 -> ShortcutInfoHidden(
175 | userId, id, packageName, activity, icon,
176 | title, titleResId, titleResName, text, textResId, textResName,
177 | disabledMessage, disabledMessageResId, disabledMessageResName,
178 | categories,
179 | intentsWithExtras,
180 | rank, extras, lastChangedTimestamp, flags,
181 | iconResId, iconResName, bitmapPath, iconUri,
182 | disabledReason, persons, locusId, null
183 | )
184 | else -> ShortcutInfoHidden(
185 | userId, id, packageName, activity, icon,
186 | title, titleResId, titleResName, text, textResId, textResName,
187 | disabledMessage, disabledMessageResId, disabledMessageResName,
188 | categories,
189 | intentsWithExtras,
190 | rank, extras, lastChangedTimestamp, flags,
191 | iconResId, iconResName, bitmapPath, iconUri,
192 | disabledReason, persons, locusId, null, null
193 | )
194 | }
195 | return compat as ShortcutInfo
196 | }
197 |
--------------------------------------------------------------------------------
/app/src/main/java/fivecc/tools/shortcut_helper/utils/ShortcutParser.java:
--------------------------------------------------------------------------------
1 | package fivecc.tools.shortcut_helper.utils;
2 |
3 | import android.app.Person;
4 | import android.content.ComponentName;
5 | import android.content.Intent;
6 | import android.content.LocusId;
7 | import android.content.pm.ShortcutInfo;
8 | import android.content.pm.ShortcutInfoHidden;
9 | import android.os.Build;
10 | import android.os.PersistableBundle;
11 | import android.os.PersistableBundleHidden;
12 | import android.text.TextUtils;
13 | import android.util.ArraySet;
14 | import android.util.AtomicFile;
15 | import android.util.Log;
16 | import android.util.Xml;
17 | import android.util.XmlHidden;
18 |
19 | import androidx.annotation.Nullable;
20 | import androidx.annotation.RequiresApi;
21 |
22 | import org.xmlpull.v1.XmlPullParser;
23 | import org.xmlpull.v1.XmlPullParserException;
24 |
25 | import java.io.BufferedInputStream;
26 | import java.io.File;
27 | import java.io.FileInputStream;
28 | import java.io.FileNotFoundException;
29 | import java.io.IOException;
30 | import java.io.InputStream;
31 | import java.net.URISyntaxException;
32 | import java.nio.charset.StandardCharsets;
33 | import java.util.ArrayList;
34 | import java.util.Arrays;
35 | import java.util.List;
36 | import java.util.Objects;
37 | import java.util.function.Consumer;
38 |
39 | // frameworks/base/services/core/java/com/android/server/pm/ShortcutService.java
40 | // frameworks/base/services/core/java/com/android/server/pm/ShortcutUser.java
41 | // frameworks/base/services/core/java/com/android/server/pm/ShortcutPackage.java
42 | public class ShortcutParser {
43 | private static final String TAG = "ShortcutParser";
44 |
45 | static final String DIRECTORY_PACKAGES = "packages";
46 | static final String FILENAME_USER_PACKAGES = "shortcuts.xml";
47 | static final String DIRECTORY_PER_USER = "shortcut_service";
48 |
49 | static final String TAG_PACKAGE_ROOT = "package";
50 | static final String TAG_USER_ROOT = "user";
51 | private static final String TAG_INTENT_EXTRAS_LEGACY = "intent-extras";
52 | private static final String TAG_INTENT = "intent";
53 | private static final String TAG_EXTRAS = "extras";
54 | private static final String TAG_SHORTCUT = "shortcut";
55 | private static final String TAG_CATEGORIES = "categories";
56 | private static final String TAG_PERSON = "person";
57 |
58 | private static final String ATTR_NAME = "name";
59 | private static final String ATTR_ID = "id";
60 | private static final String ATTR_ACTIVITY = "activity";
61 | private static final String ATTR_TITLE = "title";
62 | private static final String ATTR_TITLE_RES_ID = "titleid";
63 | private static final String ATTR_TITLE_RES_NAME = "titlename";
64 | private static final String ATTR_TEXT = "text";
65 | private static final String ATTR_TEXT_RES_ID = "textid";
66 | private static final String ATTR_TEXT_RES_NAME = "textname";
67 | private static final String ATTR_DISABLED_MESSAGE = "dmessage";
68 | private static final String ATTR_DISABLED_MESSAGE_RES_ID = "dmessageid";
69 | private static final String ATTR_DISABLED_MESSAGE_RES_NAME = "dmessagename";
70 | private static final String ATTR_DISABLED_REASON = "disabled-reason";
71 | private static final String ATTR_INTENT_LEGACY = "intent";
72 | private static final String ATTR_INTENT_NO_EXTRA = "intent-base";
73 | private static final String ATTR_RANK = "rank";
74 | private static final String ATTR_TIMESTAMP = "timestamp";
75 | private static final String ATTR_FLAGS = "flags";
76 | private static final String ATTR_ICON_RES_ID = "icon-res";
77 | private static final String ATTR_ICON_RES_NAME = "icon-resname";
78 | private static final String ATTR_BITMAP_PATH = "bitmap-path";
79 | private static final String ATTR_ICON_URI = "icon-uri";
80 | private static final String ATTR_LOCUS_ID = "locus-id";
81 |
82 | private static final String ATTR_PERSON_NAME = "name";
83 | private static final String ATTR_PERSON_URI = "uri";
84 | private static final String ATTR_PERSON_KEY = "key";
85 | private static final String ATTR_PERSON_IS_BOT = "is-bot";
86 | private static final String ATTR_PERSON_IS_IMPORTANT = "is-important";
87 |
88 | private static final String NAME_CATEGORIES = "categories";
89 |
90 | private static final String TAG_STRING_ARRAY_XMLUTILS = "string-array";
91 | private static final String ATTR_NAME_XMLUTILS = "name";
92 |
93 | private static File getUserFile(int userId) {
94 | return new File(injectUserDataPath(userId), FILENAME_USER_PACKAGES);
95 | }
96 |
97 | private static File injectUserDataPath(int userId) {
98 | // Environment.getDataSystemCeDirectory(userId)
99 | return new File("/data/system_ce/" + userId, DIRECTORY_PER_USER);
100 | }
101 |
102 | public static void closeQuietly(@Nullable AutoCloseable closeable) {
103 | if (closeable != null) {
104 | try {
105 | closeable.close();
106 | } catch (RuntimeException rethrown) {
107 | throw rethrown;
108 | } catch (Exception ignored) {
109 | }
110 | }
111 | }
112 |
113 | @Nullable
114 | public static List loadUserLocked(int userId) {
115 | final File path = getUserFile(userId);
116 | final AtomicFile file = new AtomicFile(path);
117 |
118 | final FileInputStream in;
119 | try {
120 | in = file.openRead();
121 | } catch (FileNotFoundException e) {
122 | Log.d(TAG, "Not found " + path);
123 | return null;
124 | }
125 | try {
126 | return loadUserInternal(userId, in, /* forBackup= */ false);
127 | } catch (IOException | XmlPullParserException e) {
128 | Log.e(TAG, "Failed to read file " + file.getBaseFile(), e);
129 | return null;
130 | } finally {
131 | closeQuietly(in);
132 | }
133 | }
134 |
135 | private static XmlPullParser newPullParserCompat(InputStream is) throws XmlPullParserException, IOException {
136 | XmlPullParser parser;
137 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
138 | parser = XmlHidden.resolvePullParser(is);
139 | } else {
140 | parser = Xml.newPullParser();
141 | parser.setInput(is, StandardCharsets.UTF_8.name());
142 | }
143 | return parser;
144 | }
145 |
146 | private static List loadUserInternal(int userId, InputStream is,
147 | boolean fromBackup) throws XmlPullParserException, IOException {
148 | XmlPullParser parser = newPullParserCompat(is);
149 | List ret = null;
150 |
151 | int type;
152 | while ((type = parser.next()) != XmlPullParser.END_DOCUMENT) {
153 | if (type != XmlPullParser.START_TAG) {
154 | continue;
155 | }
156 | final int depth = parser.getDepth();
157 |
158 | final String tag = parser.getName();
159 | if ((depth == 1) && TAG_USER_ROOT.equals(tag)) {
160 | ret = loadUserFromXml(parser, userId, fromBackup);
161 | }
162 | // throwForInvalidTag(depth, tag);
163 | }
164 | return ret;
165 | }
166 |
167 | private static List loadUserFromXml(XmlPullParser parser, int userId,
168 | boolean fromBackup) throws IOException, XmlPullParserException {
169 | List ret = new ArrayList<>();
170 | boolean readShortcutItems = false;
171 | final int outerDepth = parser.getDepth();
172 | int type;
173 | while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
174 | && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) {
175 | if (type != XmlPullParser.START_TAG) {
176 | continue;
177 | }
178 | final int depth = parser.getDepth();
179 | final String tag = parser.getName();
180 |
181 | if (depth == outerDepth + 1) {
182 | if (Objects.equals(tag, ShortcutParser.TAG_PACKAGE_ROOT)) {
183 | ret.addAll(loadPackageFromXml(parser, fromBackup, userId));
184 | readShortcutItems = true;
185 | continue;
186 | }
187 | }
188 | warnForInvalidTag(depth, tag);
189 | }
190 |
191 | if (!readShortcutItems) {
192 | // If the shortcuts info was read from the main Xml, skip reading from individual files.
193 | // Data will get stored in the new format during the next call to saveToXml().
194 | final File root = injectUserDataPath(userId);
195 |
196 | forAllFilesIn(new File(root, DIRECTORY_PACKAGES), (File f) -> {
197 | final List sp = loadPackageFromFile(f, fromBackup, userId);
198 | if (sp != null) {
199 | ret.addAll(sp);
200 | }
201 | });
202 | }
203 |
204 | return ret;
205 | }
206 |
207 | private static void forAllFilesIn(File path, Consumer callback) {
208 | if (!path.exists()) {
209 | return;
210 | }
211 | File[] list = path.listFiles();
212 | assert list != null;
213 | for (File f : list) {
214 | callback.accept(f);
215 | }
216 | }
217 |
218 | private static List loadPackageFromFile(File path, boolean fromBackup, int userId) {
219 |
220 | final AtomicFile file = new AtomicFile(path);
221 | final FileInputStream in;
222 | try {
223 | in = file.openRead();
224 | } catch (FileNotFoundException e) {
225 | return null;
226 | }
227 |
228 | try {
229 | final BufferedInputStream bis = new BufferedInputStream(in);
230 |
231 | List ret = null;
232 | XmlPullParser parser = newPullParserCompat(bis);
233 |
234 | int type;
235 | while ((type = parser.next()) != XmlPullParser.END_DOCUMENT) {
236 | if (type != XmlPullParser.START_TAG) {
237 | continue;
238 | }
239 | final int depth = parser.getDepth();
240 |
241 | final String tag = parser.getName();
242 | if ((depth == 1) && TAG_PACKAGE_ROOT.equals(tag)) {
243 | ret = loadPackageFromXml(parser, fromBackup, userId);
244 | }
245 | }
246 | return ret;
247 | } catch (XmlPullParserException | IOException e) {
248 | Log.e(TAG, "loadPackageFromFile", e);
249 | return null;
250 | } finally {
251 | closeQuietly(in);
252 | }
253 | }
254 |
255 | private static List loadPackageFromXml(XmlPullParser parser, boolean fromBackup, int userId)
256 | throws IOException, XmlPullParserException {
257 |
258 | final String packageName = parseStringAttribute(parser,
259 | ATTR_NAME);
260 |
261 | List ret = new ArrayList<>();
262 |
263 | final int outerDepth = parser.getDepth();
264 | int type;
265 | while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
266 | && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) {
267 | if (type != XmlPullParser.START_TAG) {
268 | continue;
269 | }
270 | final int depth = parser.getDepth();
271 | final String tag = parser.getName();
272 | if (depth == outerDepth + 1) {
273 | if (TAG_SHORTCUT.equals(tag)) {
274 | final ShortcutInfo si = parseShortcut(parser, packageName,
275 | userId, fromBackup);
276 | ret.add(si);
277 | continue;
278 | }
279 | }
280 | warnForInvalidTag(depth, tag);
281 | }
282 | return ret;
283 | }
284 |
285 | private static ShortcutInfo parseShortcut(XmlPullParser parser, String packageName,
286 | int userId, boolean fromBackup)
287 | throws IOException, XmlPullParserException {
288 | String id;
289 | ComponentName activityComponent;
290 | // Icon icon;
291 | String title;
292 | int titleResId;
293 | String titleResName;
294 | String text;
295 | int textResId;
296 | String textResName;
297 | String disabledMessage;
298 | int disabledMessageResId;
299 | String disabledMessageResName;
300 | int disabledReason;
301 | Intent intentLegacy;
302 | PersistableBundle intentPersistableExtrasLegacy = null;
303 | ArrayList intents = new ArrayList<>();
304 | int rank;
305 | PersistableBundle extras = null;
306 | long lastChangedTimestamp;
307 | int flags;
308 | int iconResId;
309 | String iconResName;
310 | String bitmapPath;
311 | String iconUri;
312 | final String locusIdString;
313 | ArraySet categories = null;
314 | ArrayList persons = new ArrayList<>();
315 |
316 | id = parseStringAttribute(parser, ATTR_ID);
317 | activityComponent = parseComponentNameAttribute(parser,
318 | ATTR_ACTIVITY);
319 | title = parseStringAttribute(parser, ATTR_TITLE);
320 | titleResId = parseIntAttribute(parser, ATTR_TITLE_RES_ID);
321 | titleResName = parseStringAttribute(parser, ATTR_TITLE_RES_NAME);
322 | text = parseStringAttribute(parser, ATTR_TEXT);
323 | textResId = parseIntAttribute(parser, ATTR_TEXT_RES_ID);
324 | textResName = parseStringAttribute(parser, ATTR_TEXT_RES_NAME);
325 | disabledMessage = parseStringAttribute(parser, ATTR_DISABLED_MESSAGE);
326 | disabledMessageResId = parseIntAttribute(parser,
327 | ATTR_DISABLED_MESSAGE_RES_ID);
328 | disabledMessageResName = parseStringAttribute(parser,
329 | ATTR_DISABLED_MESSAGE_RES_NAME);
330 | disabledReason = parseIntAttribute(parser, ATTR_DISABLED_REASON);
331 | intentLegacy = parseIntentAttributeNoDefault(parser, ATTR_INTENT_LEGACY);
332 | rank = (int) parseLongAttribute(parser, ATTR_RANK);
333 | lastChangedTimestamp = parseLongAttribute(parser, ATTR_TIMESTAMP);
334 | flags = (int) parseLongAttribute(parser, ATTR_FLAGS);
335 | iconResId = (int) parseLongAttribute(parser, ATTR_ICON_RES_ID);
336 | iconResName = parseStringAttribute(parser, ATTR_ICON_RES_NAME);
337 | bitmapPath = parseStringAttribute(parser, ATTR_BITMAP_PATH);
338 | iconUri = parseStringAttribute(parser, ATTR_ICON_URI);
339 | locusIdString = parseStringAttribute(parser, ATTR_LOCUS_ID);
340 |
341 | final int outerDepth = parser.getDepth();
342 | int type;
343 | while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
344 | && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) {
345 | if (type != XmlPullParser.START_TAG) {
346 | continue;
347 | }
348 | final int depth = parser.getDepth();
349 | final String tag = parser.getName();
350 | switch (tag) {
351 | case TAG_INTENT_EXTRAS_LEGACY:
352 | intentPersistableExtrasLegacy = PersistableBundleHidden.restoreFromXml(parser);
353 | continue;
354 | case TAG_INTENT:
355 | intents.add(parseIntent(parser));
356 | continue;
357 | case TAG_EXTRAS:
358 | extras = PersistableBundleHidden.restoreFromXml(parser);
359 | continue;
360 | case TAG_CATEGORIES:
361 | // This just contains string-array.
362 | continue;
363 | case TAG_PERSON:
364 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
365 | persons.add(parsePerson(parser));
366 | }
367 | continue;
368 | case TAG_STRING_ARRAY_XMLUTILS:
369 | if (NAME_CATEGORIES.equals(parseStringAttribute(parser,
370 | ATTR_NAME_XMLUTILS))) {
371 | final String[] ar = XmlUtils.readThisStringArrayXml(
372 | parser, TAG_STRING_ARRAY_XMLUTILS, null);
373 | categories = new ArraySet<>(ar.length);
374 | categories.addAll(Arrays.asList(ar));
375 | }
376 | continue;
377 | }
378 | throw throwForInvalidTag(depth, tag);
379 | }
380 |
381 | if (intentLegacy != null) {
382 | // For the legacy file format which supported only one intent per shortcut.
383 | ShortcutInfoHidden.setIntentExtras(intentLegacy, intentPersistableExtrasLegacy);
384 | intents.clear();
385 | intents.add(intentLegacy);
386 | }
387 |
388 | LocusId locusId = null;
389 | if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.Q) {
390 | locusId = locusIdString == null ? null : new LocusId(locusIdString);
391 | }
392 |
393 | return ShortcutInfoHelperKt.newShortcutInfoCompat(
394 | userId, id, packageName, activityComponent, /* icon= */ null,
395 | title, titleResId, titleResName, text, textResId, textResName,
396 | disabledMessage, disabledMessageResId, disabledMessageResName,
397 | categories,
398 | intents.toArray(new Intent[intents.size()]),
399 | rank, extras, lastChangedTimestamp, flags,
400 | iconResId, iconResName, bitmapPath, iconUri,
401 | disabledReason, persons.toArray(new Person[persons.size()]), locusId);
402 | }
403 |
404 | private static Intent parseIntent(XmlPullParser parser)
405 | throws IOException, XmlPullParserException {
406 |
407 | Intent intent = parseIntentAttribute(parser,
408 | ATTR_INTENT_NO_EXTRA);
409 |
410 | final int outerDepth = parser.getDepth();
411 | int type;
412 | while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
413 | && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) {
414 | if (type != XmlPullParser.START_TAG) {
415 | continue;
416 | }
417 | final int depth = parser.getDepth();
418 | final String tag = parser.getName();
419 | if (TAG_EXTRAS.equals(tag)) {
420 | ShortcutInfoHidden.setIntentExtras(intent,
421 | PersistableBundleHidden.restoreFromXml(parser));
422 | continue;
423 | }
424 | throw throwForInvalidTag(depth, tag);
425 | }
426 | return intent;
427 | }
428 |
429 | static IOException throwForInvalidTag(int depth, String tag) throws IOException {
430 | throw new IOException(String.format("Invalid tag '%s' found at depth %d", tag, depth));
431 | }
432 |
433 | static void warnForInvalidTag(int depth, String tag) throws IOException {
434 | Log.w(TAG, String.format("Invalid tag '%s' found at depth %d", tag, depth));
435 | }
436 |
437 | @RequiresApi(api = Build.VERSION_CODES.P)
438 | private static Person parsePerson(XmlPullParser parser)
439 | throws IOException, XmlPullParserException {
440 | CharSequence name = parseStringAttribute(parser, ATTR_PERSON_NAME);
441 | String uri = parseStringAttribute(parser, ATTR_PERSON_URI);
442 | String key = parseStringAttribute(parser, ATTR_PERSON_KEY);
443 | boolean isBot = parseBooleanAttribute(parser, ATTR_PERSON_IS_BOT);
444 | boolean isImportant = parseBooleanAttribute(parser,
445 | ATTR_PERSON_IS_IMPORTANT);
446 |
447 | Person.Builder builder = new Person.Builder();
448 | builder.setName(name).setUri(uri).setKey(key).setBot(isBot).setImportant(isImportant);
449 | return builder.build();
450 | }
451 |
452 | static String parseStringAttribute(XmlPullParser parser, String attribute) {
453 | return parser.getAttributeValue(null, attribute);
454 | }
455 |
456 | static boolean parseBooleanAttribute(XmlPullParser parser, String attribute) {
457 | return parseLongAttribute(parser, attribute) == 1;
458 | }
459 |
460 | static boolean parseBooleanAttribute(XmlPullParser parser, String attribute, boolean def) {
461 | return parseLongAttribute(parser, attribute, (def ? 1 : 0)) == 1;
462 | }
463 |
464 | static int parseIntAttribute(XmlPullParser parser, String attribute) {
465 | return (int) parseLongAttribute(parser, attribute);
466 | }
467 |
468 | static int parseIntAttribute(XmlPullParser parser, String attribute, int def) {
469 | return (int) parseLongAttribute(parser, attribute, def);
470 | }
471 |
472 | static long parseLongAttribute(XmlPullParser parser, String attribute) {
473 | return parseLongAttribute(parser, attribute, 0);
474 | }
475 |
476 | static long parseLongAttribute(XmlPullParser parser, String attribute, long def) {
477 | final String value = parseStringAttribute(parser, attribute);
478 | if (TextUtils.isEmpty(value)) {
479 | return def;
480 | }
481 | try {
482 | return Long.parseLong(value);
483 | } catch (NumberFormatException e) {
484 | return def;
485 | }
486 | }
487 |
488 | @Nullable
489 | static ComponentName parseComponentNameAttribute(XmlPullParser parser, String attribute) {
490 | final String value = parseStringAttribute(parser, attribute);
491 | if (TextUtils.isEmpty(value)) {
492 | return null;
493 | }
494 | return ComponentName.unflattenFromString(value);
495 | }
496 |
497 | @Nullable
498 | static Intent parseIntentAttributeNoDefault(XmlPullParser parser, String attribute) {
499 | final String value = parseStringAttribute(parser, attribute);
500 | Intent parsed = null;
501 | if (!TextUtils.isEmpty(value)) {
502 | try {
503 | parsed = Intent.parseUri(value, /* flags =*/ 0);
504 | } catch (URISyntaxException e) {
505 | Log.e(TAG, "Error parsing intent", e);
506 | }
507 | }
508 | return parsed;
509 | }
510 |
511 | @Nullable
512 | static Intent parseIntentAttribute(XmlPullParser parser, String attribute) {
513 | Intent parsed = parseIntentAttributeNoDefault(parser, attribute);
514 | if (parsed == null) {
515 | // Default intent.
516 | parsed = new Intent(Intent.ACTION_VIEW);
517 | }
518 | return parsed;
519 | }
520 | }
521 |
--------------------------------------------------------------------------------
/app/src/main/java/fivecc/tools/shortcut_helper/utils/XmlUtils.java:
--------------------------------------------------------------------------------
1 | package fivecc.tools.shortcut_helper.utils;
2 |
3 | import org.xmlpull.v1.XmlPullParser;
4 | import org.xmlpull.v1.XmlPullParserException;
5 |
6 | // from android 11 source
7 | // frameworks/base/core/java/com/android/internal/util/XmlUtils.java
8 | public class XmlUtils {
9 | public static final String[] readThisStringArrayXml(XmlPullParser parser, String endTag,
10 | String[] name) throws XmlPullParserException, java.io.IOException {
11 |
12 | int num;
13 | try {
14 | num = Integer.parseInt(parser.getAttributeValue(null, "num"));
15 | } catch (NullPointerException e) {
16 | throw new XmlPullParserException("Need num attribute in string-array");
17 | } catch (NumberFormatException e) {
18 | throw new XmlPullParserException("Not a number in num attribute in string-array");
19 | }
20 | parser.next();
21 |
22 | String[] array = new String[num];
23 | int i = 0;
24 |
25 | int eventType = parser.getEventType();
26 | do {
27 | if (eventType == parser.START_TAG) {
28 | if (parser.getName().equals("item")) {
29 | try {
30 | array[i] = parser.getAttributeValue(null, "value");
31 | } catch (NullPointerException e) {
32 | throw new XmlPullParserException("Need value attribute in item");
33 | } catch (NumberFormatException e) {
34 | throw new XmlPullParserException("Not a number in value attribute in item");
35 | }
36 | } else {
37 | throw new XmlPullParserException("Expected item tag at: " + parser.getName());
38 | }
39 | } else if (eventType == parser.END_TAG) {
40 | if (parser.getName().equals(endTag)) {
41 | return array;
42 | } else if (parser.getName().equals("item")) {
43 | i++;
44 | } else {
45 | throw new XmlPullParserException("Expected " + endTag + " end tag at: " +
46 | parser.getName());
47 | }
48 | }
49 | eventType = parser.next();
50 | } while (eventType != parser.END_DOCUMENT);
51 |
52 | throw new XmlPullParserException("Document ended before " + endTag + " end tag");
53 | }
54 |
55 | }
56 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_launcher_foreground.xml:
--------------------------------------------------------------------------------
1 |
7 |
11 |
14 |
17 |
20 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/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/ic_launcher_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #C47AEC
4 |
--------------------------------------------------------------------------------
/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | Shortcut
3 |
4 |
5 | Work Mode
6 | System API
7 | Parse XML
8 |
--------------------------------------------------------------------------------
/app/src/main/res/values/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/xml/backup_rules.xml:
--------------------------------------------------------------------------------
1 |
8 |
9 |
13 |
--------------------------------------------------------------------------------
/app/src/main/res/xml/data_extraction_rules.xml:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
12 |
13 |
19 |
--------------------------------------------------------------------------------
/app/src/test/java/fivecc/tools/shortcut_helper/ExampleUnitTest.kt:
--------------------------------------------------------------------------------
1 | package fivecc.tools.shortcut_helper
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.kts:
--------------------------------------------------------------------------------
1 | buildscript {
2 | }
3 |
4 | plugins {
5 |
6 | }
7 |
8 | tasks.register("clean", Delete::class) {
9 | delete(rootProject.buildDir)
10 | }
--------------------------------------------------------------------------------
/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
24 |
25 | hiddenApiRefineVersion=3.1.1
26 | composeVersion=1.2.1
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/5ec1cff/AndroidShortcutHelper/a9a3e9babe33805a447aee36985b24cc7b62cd55/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Sun Sep 11 14:56:29 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 |
--------------------------------------------------------------------------------
/hidden-api/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/hidden-api/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | id("com.android.library")
3 | }
4 |
5 | android {
6 | compileSdk = 33
7 |
8 | defaultConfig {
9 | minSdk = 26
10 | targetSdk = 33
11 |
12 | consumerProguardFiles("consumer-rules.pro")
13 | }
14 |
15 | buildTypes {
16 | release {
17 | isMinifyEnabled = false
18 | proguardFiles(
19 | getDefaultProguardFile("proguard-android-optimize.txt"),
20 | "proguard-rules.pro"
21 | )
22 | }
23 | }
24 | compileOptions {
25 | sourceCompatibility = JavaVersion.VERSION_1_8
26 | targetCompatibility = JavaVersion.VERSION_1_8
27 | }
28 | }
29 |
30 | dependencies {
31 | implementation("org.jetbrains:annotations:15.0")
32 | implementation("androidx.annotation:annotation:1.4.0")
33 | val hiddenApiRefineVersion: String by project
34 | annotationProcessor("dev.rikka.tools.refine:annotation-processor:$hiddenApiRefineVersion")
35 | compileOnly("dev.rikka.tools.refine:annotation:$hiddenApiRefineVersion")
36 | }
--------------------------------------------------------------------------------
/hidden-api/consumer-rules.pro:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/5ec1cff/AndroidShortcutHelper/a9a3e9babe33805a447aee36985b24cc7b62cd55/hidden-api/consumer-rules.pro
--------------------------------------------------------------------------------
/hidden-api/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
--------------------------------------------------------------------------------
/hidden-api/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
--------------------------------------------------------------------------------
/hidden-api/src/main/java/android/content/pm/BaseParceledListSlice.java:
--------------------------------------------------------------------------------
1 | package android.content.pm;
2 |
3 | import android.os.IBinder;
4 | import android.os.Parcel;
5 | import android.os.Parcelable;
6 |
7 | import java.util.ArrayList;
8 | import java.util.List;
9 |
10 | abstract class BaseParceledListSlice implements Parcelable {
11 |
12 | /*
13 | * TODO get this number from somewhere else. For now set it to a quarter of
14 | * the 1MB limit.
15 | */
16 |
17 | public BaseParceledListSlice(List list) {
18 | throw new RuntimeException("STUB");
19 | }
20 |
21 | @SuppressWarnings("unchecked")
22 | BaseParceledListSlice(Parcel p, ClassLoader loader) {
23 | throw new RuntimeException("STUB");
24 | }
25 |
26 | public List getList() {
27 | throw new RuntimeException("STUB");
28 | }
29 |
30 | /**
31 | * Set a limit on the maximum number of entries in the array that will be included
32 | * inline in the initial parcelling of this object.
33 | */
34 | public void setInlineCountLimit(int maxCount) {
35 | throw new RuntimeException("STUB");
36 | }
37 |
38 | /**
39 | * Write this to another Parcel. Note that this discards the internal Parcel
40 | * and should not be used anymore. This is so we can pass this to a Binder
41 | * where we won't have a chance to call recycle on this.
42 | */
43 | @Override
44 | public void writeToParcel(Parcel dest, int flags) {
45 | throw new RuntimeException("STUB");
46 | }
47 |
48 | protected abstract void writeElement(T parcelable, Parcel reply, int callFlags);
49 |
50 | protected abstract void writeParcelableCreator(T parcelable, Parcel dest);
51 |
52 | protected abstract Parcelable.Creator> readParcelableCreator(Parcel from, ClassLoader loader);
53 | }
--------------------------------------------------------------------------------
/hidden-api/src/main/java/android/content/pm/ILauncherApps.java:
--------------------------------------------------------------------------------
1 | package android.content.pm;
2 |
3 | import android.graphics.Rect;
4 | import android.os.Binder;
5 | import android.os.Build;
6 | import android.os.Bundle;
7 | import android.os.IBinder;
8 | import android.os.ParcelFileDescriptor;
9 |
10 | import androidx.annotation.RequiresApi;
11 |
12 | public interface ILauncherApps {
13 | ParcelFileDescriptor getShortcutIconFd(String callingPackage, String packageName, String id,
14 | int userId);
15 |
16 | @RequiresApi(Build.VERSION_CODES.R)
17 | boolean startShortcut(String callingPackage, String packageName, String featureId, String id,
18 | Rect sourceBounds, Bundle startActivityOptions, int userId);
19 |
20 | boolean startShortcut(String callingPackage, String packageName, String id,
21 | Rect sourceBounds, Bundle startActivityOptions, int userId);
22 |
23 | class Stub extends Binder {
24 | public static ILauncherApps asInterface(IBinder b) {
25 | throw new UnsupportedOperationException("STUB!");
26 | }
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/hidden-api/src/main/java/android/content/pm/IPackageManager.java:
--------------------------------------------------------------------------------
1 | package android.content.pm;
2 |
3 | import android.content.ComponentName;
4 | import android.content.IntentFilter;
5 | import android.os.Binder;
6 | import android.os.IBinder;
7 | import android.os.IInterface;
8 |
9 | import java.util.List;
10 |
11 |
12 | public interface IPackageManager extends IInterface {
13 | String[] getPackagesForUid(int uid);
14 | int getPackageUid(String packageName, int flags, int userId);
15 |
16 | void addPreferredActivity(IntentFilter filter, int match,
17 | ComponentName[] set, ComponentName activity, int userId);
18 |
19 | void replacePreferredActivity(IntentFilter filter, int match,
20 | ComponentName[] set, ComponentName activity, int userId);
21 |
22 | void clearPackagePreferredActivities(String packageName);
23 |
24 | int getPreferredActivities(List outFilters,
25 | List outActivities, String packageName);
26 |
27 | class Stub extends Binder {
28 | public static IPackageManager asInterface(IBinder b) {
29 | throw new UnsupportedOperationException("STUB!");
30 | }
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/hidden-api/src/main/java/android/content/pm/IShortcutService.java:
--------------------------------------------------------------------------------
1 | package android.content.pm;
2 |
3 | import android.os.Binder;
4 | import android.os.Build;
5 | import android.os.IBinder;
6 |
7 | import androidx.annotation.RequiresApi;
8 |
9 | @SuppressWarnings("rawtypes")
10 | public interface IShortcutService {
11 | // API 29 and below
12 | ParceledListSlice getPinnedShortcuts(String packageName, int userId);
13 | ParceledListSlice getDynamicShortcuts(String packageName, int userId);
14 | ParceledListSlice getManifestShortcuts(String packageName, int userId);
15 |
16 | // API 30 & 33
17 | @RequiresApi(Build.VERSION_CODES.R)
18 | ParceledListSlice getShortcuts(String packageName, int matchFlags, int userId);
19 |
20 | // API 31 & 32
21 | // AndroidFuture getShortcuts(String packageName, int matchFlags, int userId);
22 |
23 | class Stub extends Binder {
24 | public static IShortcutService asInterface(IBinder b) {
25 | throw new UnsupportedOperationException("STUB!");
26 | }
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/hidden-api/src/main/java/android/content/pm/IShortcutServiceForS.java:
--------------------------------------------------------------------------------
1 | package android.content.pm;
2 |
3 | import com.android.internal.infra.AndroidFuture;
4 |
5 | import dev.rikka.tools.refine.RefineAs;
6 |
7 | @RefineAs(IShortcutService.class)
8 | public interface IShortcutServiceForS {
9 | AndroidFuture getShortcuts(String packageName, int matchFlags, int userId);
10 | }
11 |
--------------------------------------------------------------------------------
/hidden-api/src/main/java/android/content/pm/PackageManagerHidden.java:
--------------------------------------------------------------------------------
1 | package android.content.pm;
2 |
3 | import android.os.Build;
4 |
5 | import androidx.annotation.NonNull;
6 | import androidx.annotation.RequiresApi;
7 |
8 | import java.util.List;
9 |
10 | import dev.rikka.tools.refine.RefineAs;
11 |
12 | @RefineAs(PackageManager.class)
13 | public abstract class PackageManagerHidden {
14 | public abstract List getInstalledPackagesAsUser(int flags,
15 | int userId);
16 |
17 | @RequiresApi(Build.VERSION_CODES.TIRAMISU)
18 | public List getInstalledPackagesAsUser(@NonNull PackageManager.PackageInfoFlags flags,
19 | int userId) {
20 | throw new UnsupportedOperationException(
21 | "getApplicationInfoAsUser not implemented in subclass");
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/hidden-api/src/main/java/android/content/pm/ParceledListSlice.java:
--------------------------------------------------------------------------------
1 | package android.content.pm;
2 |
3 | import android.os.Parcel;
4 | import android.os.Parcelable;
5 |
6 | import java.util.List;
7 |
8 | public class ParceledListSlice extends BaseParceledListSlice {
9 | public ParceledListSlice(List list) {
10 | super(list);
11 | }
12 |
13 | private ParceledListSlice(Parcel in, ClassLoader loader) {
14 | super(in, loader);
15 | }
16 |
17 | public static ParceledListSlice emptyList() {
18 | throw new RuntimeException("STUB");
19 | }
20 |
21 | @Override
22 | public int describeContents() {
23 | throw new RuntimeException("STUB");
24 | }
25 |
26 | @Override
27 | protected void writeElement(T parcelable, Parcel dest, int callFlags) {
28 | throw new RuntimeException("STUB");
29 | }
30 |
31 | @Override
32 | protected void writeParcelableCreator(T parcelable, Parcel dest) {
33 | throw new RuntimeException("STUB");
34 | }
35 |
36 | @Override
37 | protected Parcelable.Creator> readParcelableCreator(Parcel from, ClassLoader loader) {
38 | throw new RuntimeException("STUB");
39 | }
40 |
41 | public static final Parcelable.ClassLoaderCreator CREATOR =
42 | new Parcelable.ClassLoaderCreator() {
43 | public ParceledListSlice createFromParcel(Parcel in) {
44 | throw new RuntimeException("STUB");
45 | }
46 |
47 | @Override
48 | public ParceledListSlice createFromParcel(Parcel in, ClassLoader loader) {
49 | throw new RuntimeException("STUB");
50 | }
51 |
52 | @Override
53 | public ParceledListSlice[] newArray(int size) {
54 | throw new RuntimeException("STUB");
55 | }
56 | };
57 | }
--------------------------------------------------------------------------------
/hidden-api/src/main/java/android/content/pm/ShortcutInfoHidden.java:
--------------------------------------------------------------------------------
1 | package android.content.pm;
2 |
3 | import android.app.Person;
4 | import android.content.ComponentName;
5 | import android.content.Intent;
6 | import android.content.LocusId;
7 | import android.graphics.drawable.Icon;
8 | import android.os.Bundle;
9 | import android.os.PersistableBundle;
10 |
11 | import org.jetbrains.annotations.Nullable;
12 |
13 | import java.util.List;
14 | import java.util.Map;
15 | import java.util.Set;
16 |
17 | import dev.rikka.tools.refine.RefineAs;
18 |
19 | @RefineAs(ShortcutInfo.class)
20 | public class ShortcutInfoHidden {
21 | public String getBitmapPath() {
22 | throw new RuntimeException("");
23 | }
24 |
25 | public String toInsecureString() {
26 | throw new RuntimeException("");
27 | }
28 |
29 | public static Intent setIntentExtras(Intent intent, PersistableBundle extras) {
30 | if (extras == null) {
31 | intent.replaceExtras((Bundle) null);
32 | } else {
33 | intent.replaceExtras(new Bundle(extras));
34 | }
35 | return intent;
36 | }
37 |
38 | public boolean hasIconUri() {
39 | throw new RuntimeException("");
40 | }
41 |
42 | public boolean hasIconFile() {
43 | throw new RuntimeException("");
44 | }
45 |
46 | public boolean hasIconResource() {
47 | throw new RuntimeException("");
48 | }
49 |
50 | public boolean hasAdaptiveBitmap() {
51 | throw new RuntimeException("");
52 | }
53 |
54 | public int getIconResourceId() {
55 | throw new RuntimeException("");
56 | }
57 |
58 | public int getShortLabelResourceId() {
59 | throw new RuntimeException("");
60 | }
61 |
62 | public int getLongLabelResourceId() {
63 | throw new RuntimeException("");
64 | }
65 |
66 | public int getUserId() {
67 | throw new RuntimeException("");
68 | }
69 |
70 | // API 27 and below
71 | public ShortcutInfoHidden(
72 | int userId, String id, String packageName, ComponentName activity,
73 | Icon icon, CharSequence title, int titleResId, String titleResName,
74 | CharSequence text, int textResId, String textResName,
75 | CharSequence disabledMessage, int disabledMessageResId, String disabledMessageResName,
76 | Set categories, Intent[] intentsWithExtras, int rank, PersistableBundle extras,
77 | long lastChangedTimestamp,
78 | int flags, int iconResId, String iconResName, String bitmapPath) {}
79 |
80 | // API 28
81 | public ShortcutInfoHidden(
82 | int userId, String id, String packageName, ComponentName activity,
83 | Icon icon, CharSequence title, int titleResId, String titleResName,
84 | CharSequence text, int textResId, String textResName,
85 | CharSequence disabledMessage, int disabledMessageResId, String disabledMessageResName,
86 | Set categories, Intent[] intentsWithExtras, int rank, PersistableBundle extras,
87 | long lastChangedTimestamp,
88 | int flags, int iconResId, String iconResName, String bitmapPath, int disabledReason) {}
89 |
90 | // API 29 (Q)
91 | public ShortcutInfoHidden(
92 | int userId, String id, String packageName, ComponentName activity,
93 | Icon icon, CharSequence title, int titleResId, String titleResName,
94 | CharSequence text, int textResId, String textResName,
95 | CharSequence disabledMessage, int disabledMessageResId, String disabledMessageResName,
96 | Set categories, Intent[] intentsWithExtras, int rank, PersistableBundle extras,
97 | long lastChangedTimestamp,
98 | int flags, int iconResId, String iconResName, String bitmapPath, int disabledReason,
99 | Person[] persons, LocusId locusId) {
100 | }
101 |
102 | // API 30 (R)
103 | public ShortcutInfoHidden(
104 | int userId, String id, String packageName, ComponentName activity,
105 | Icon icon, CharSequence title, int titleResId, String titleResName,
106 | CharSequence text, int textResId, String textResName,
107 | CharSequence disabledMessage, int disabledMessageResId, String disabledMessageResName,
108 | Set categories, Intent[] intentsWithExtras, int rank, PersistableBundle extras,
109 | long lastChangedTimestamp,
110 | int flags, int iconResId, String iconResName, String bitmapPath, String iconUri,
111 | int disabledReason, Person[] persons, LocusId locusId) {
112 | }
113 |
114 | // API 31 (S), API 32 (Sv2)
115 | public ShortcutInfoHidden(
116 | int userId, String id, String packageName, ComponentName activity,
117 | Icon icon, CharSequence title, int titleResId, String titleResName,
118 | CharSequence text, int textResId, String textResName,
119 | CharSequence disabledMessage, int disabledMessageResId, String disabledMessageResName,
120 | Set categories, Intent[] intentsWithExtras, int rank, PersistableBundle extras,
121 | long lastChangedTimestamp,
122 | int flags, int iconResId, String iconResName, String bitmapPath, String iconUri,
123 | int disabledReason, Person[] persons, LocusId locusId,
124 | @Nullable String startingThemeResName) {
125 | }
126 |
127 | // API 33
128 | public ShortcutInfoHidden(
129 | int userId, String id, String packageName, ComponentName activity,
130 | Icon icon, CharSequence title, int titleResId, String titleResName,
131 | CharSequence text, int textResId, String textResName,
132 | CharSequence disabledMessage, int disabledMessageResId, String disabledMessageResName,
133 | Set categories, Intent[] intentsWithExtras, int rank, PersistableBundle extras,
134 | long lastChangedTimestamp,
135 | int flags, int iconResId, String iconResName, String bitmapPath, String iconUri,
136 | int disabledReason, Person[] persons, LocusId locusId,
137 | @Nullable String startingThemeResName,
138 | @Nullable Map>> capabilityBindings) {
139 | }
140 | }
141 |
--------------------------------------------------------------------------------
/hidden-api/src/main/java/android/ddm/DdmHandleAppName.java:
--------------------------------------------------------------------------------
1 | package android.ddm;
2 |
3 | public class DdmHandleAppName {
4 | public static void setAppName(String name, int userId) {
5 | throw new UnsupportedOperationException("STUB!");
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/hidden-api/src/main/java/android/os/PersistableBundleHidden.java:
--------------------------------------------------------------------------------
1 | package android.os;
2 |
3 | import org.xmlpull.v1.XmlPullParser;
4 | import org.xmlpull.v1.XmlPullParserException;
5 |
6 | import java.io.IOException;
7 |
8 | import dev.rikka.tools.refine.RefineAs;
9 |
10 | @RefineAs(PersistableBundle.class)
11 | public class PersistableBundleHidden {
12 | public static PersistableBundle restoreFromXml(XmlPullParser in) throws IOException,
13 | XmlPullParserException {
14 | throw new RuntimeException("");
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/hidden-api/src/main/java/android/os/ServiceManager.java:
--------------------------------------------------------------------------------
1 | package android.os;
2 |
3 | public class ServiceManager {
4 | public static IBinder getService(String name) {
5 | throw new UnsupportedOperationException("STUB!");
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/hidden-api/src/main/java/android/os/UserHandleHidden.java:
--------------------------------------------------------------------------------
1 | package android.os;
2 |
3 | import dev.rikka.tools.refine.RefineAs;
4 |
5 | @RefineAs(UserHandle.class)
6 | public class UserHandleHidden {
7 | public static int myUserId() {
8 | throw new RuntimeException("");
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/hidden-api/src/main/java/android/util/TypedXmlPullParser.java:
--------------------------------------------------------------------------------
1 | package android.util;
2 |
3 | import android.os.Build;
4 |
5 | import androidx.annotation.RequiresApi;
6 |
7 | import org.xmlpull.v1.XmlPullParser;
8 |
9 | @RequiresApi(Build.VERSION_CODES.S)
10 | public interface TypedXmlPullParser extends XmlPullParser {
11 | }
12 |
--------------------------------------------------------------------------------
/hidden-api/src/main/java/android/util/XmlHidden.java:
--------------------------------------------------------------------------------
1 | package android.util;
2 |
3 | import android.os.Build;
4 |
5 | import androidx.annotation.NonNull;
6 | import androidx.annotation.RequiresApi;
7 |
8 | import java.io.IOException;
9 | import java.io.InputStream;
10 |
11 | import dev.rikka.tools.refine.RefineAs;
12 |
13 | @RefineAs(Xml.class)
14 | public class XmlHidden {
15 | @RequiresApi(Build.VERSION_CODES.S)
16 | public static @NonNull TypedXmlPullParser resolvePullParser(@NonNull InputStream in)
17 | throws IOException {
18 | throw new RuntimeException("");
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/hidden-api/src/main/java/com/android/internal/infra/AndroidFuture.java:
--------------------------------------------------------------------------------
1 | package com.android.internal.infra;
2 |
3 | import java.util.concurrent.ExecutionException;
4 | import java.util.concurrent.Future;
5 | import java.util.concurrent.TimeUnit;
6 | import java.util.concurrent.TimeoutException;
7 |
8 | public class AndroidFuture implements Future {
9 | @Override
10 | public boolean cancel(boolean b) {
11 | return false;
12 | }
13 |
14 | @Override
15 | public boolean isCancelled() {
16 | return false;
17 | }
18 |
19 | @Override
20 | public boolean isDone() {
21 | return false;
22 | }
23 |
24 | @Override
25 | public T get() throws ExecutionException, InterruptedException {
26 | return null;
27 | }
28 |
29 | @Override
30 | public T get(long l, TimeUnit timeUnit) throws ExecutionException, InterruptedException, TimeoutException {
31 | return null;
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/img/1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/5ec1cff/AndroidShortcutHelper/a9a3e9babe33805a447aee36985b24cc7b62cd55/img/1.png
--------------------------------------------------------------------------------
/settings.gradle.kts:
--------------------------------------------------------------------------------
1 | pluginManagement {
2 | repositories {
3 | gradlePluginPortal()
4 | google()
5 | mavenCentral()
6 | }
7 | val hiddenApiRefineVersion: String by settings
8 |
9 | plugins {
10 | id("com.android.application") version "7.2.2"
11 | id("com.android.library") version "7.2.2"
12 | id("org.jetbrains.kotlin.android") version "1.7.10"
13 | id("dev.rikka.tools.refine") version hiddenApiRefineVersion
14 | }
15 | }
16 | dependencyResolutionManagement {
17 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
18 | repositories {
19 | google()
20 | mavenCentral()
21 | maven("https://jitpack.io")
22 | }
23 | }
24 | rootProject.name = "Shortcut"
25 | include(":app")
26 | include(":hidden-api")
27 |
--------------------------------------------------------------------------------