diff --git a/app/src/main/kotlin/li/songe/gkd/MainActivity.kt b/app/src/main/kotlin/li/songe/gkd/MainActivity.kt index cd22561..494555e 100644 --- a/app/src/main/kotlin/li/songe/gkd/MainActivity.kt +++ b/app/src/main/kotlin/li/songe/gkd/MainActivity.kt @@ -11,7 +11,12 @@ import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge import androidx.activity.viewModels import androidx.compose.animation.core.AnimationConstants +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.collectAsState import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen import androidx.core.view.ViewCompat import androidx.core.view.WindowInsetsCompat @@ -40,9 +45,12 @@ import li.songe.gkd.ui.component.BuildDialog import li.songe.gkd.ui.theme.AppTheme import li.songe.gkd.util.LocalNavController import li.songe.gkd.util.UpgradeDialog +import li.songe.gkd.util.appInfoCacheFlow import li.songe.gkd.util.initFolder import li.songe.gkd.util.launchTry import li.songe.gkd.util.map +import li.songe.gkd.util.openApp +import li.songe.gkd.util.openUri import li.songe.gkd.util.storeFlow class MainActivity : ComponentActivity() { @@ -81,6 +89,7 @@ class MainActivity : ComponentActivity() { navGraph = NavGraphs.root, navController = navController ) + ShizukuErrorDialog(mainVm.shizukuErrorFlow) AuthDialog(mainVm.authReasonFlow) BuildDialog(mainVm.dialogFlow) if (META.updateEnabled) { @@ -183,3 +192,48 @@ private fun Activity.fixTopPadding() { ViewCompat.onApplyWindowInsets(view, windowInsets) } } + +@Composable +private fun ShizukuErrorDialog(stateFlow: MutableStateFlow) { + val state = stateFlow.collectAsState() + if (state.value) { + val appId = "moe.shizuku.privileged.api" + val appInfoCache = appInfoCacheFlow.collectAsState() + val installed = appInfoCache.value.contains(appId) + AlertDialog( + onDismissRequest = { stateFlow.value = false }, + title = { Text(text = "授权错误") }, + text = { + Text( + text = if (installed) { + "Shizuku 授权失败, 请检查是否运行" + } else { + "Shizuku 未安装, 请先下载后安装" + } + ) + }, + confirmButton = { + if (installed) { + TextButton(onClick = { + stateFlow.value = false + app.openApp(appId) + }) { + Text(text = "打开 Shizuku") + } + } else { + TextButton(onClick = { + stateFlow.value = false + app.openUri("https://shizuku.rikka.app/") + }) { + Text(text = "去下载") + } + } + }, + dismissButton = { + TextButton(onClick = { stateFlow.value = false }) { + Text(text = "我知道了") + } + } + ) + } +} diff --git a/app/src/main/kotlin/li/songe/gkd/MainViewModel.kt b/app/src/main/kotlin/li/songe/gkd/MainViewModel.kt index 8bd37da..c8c3b42 100644 --- a/app/src/main/kotlin/li/songe/gkd/MainViewModel.kt +++ b/app/src/main/kotlin/li/songe/gkd/MainViewModel.kt @@ -41,6 +41,8 @@ class MainViewModel : ViewModel() { val updateStatus = UpdateStatus() + val shizukuErrorFlow = MutableStateFlow(false) + init { viewModelScope.launchTry(Dispatchers.IO) { val subsItems = DbSet.subsItemDao.queryAll() diff --git a/app/src/main/kotlin/li/songe/gkd/shizuku/ShizukuApi.kt b/app/src/main/kotlin/li/songe/gkd/shizuku/ShizukuApi.kt index a3267c9..62b63ce 100644 --- a/app/src/main/kotlin/li/songe/gkd/shizuku/ShizukuApi.kt +++ b/app/src/main/kotlin/li/songe/gkd/shizuku/ShizukuApi.kt @@ -5,6 +5,7 @@ import android.app.ActivityManager import android.app.IActivityTaskManager import android.content.ComponentName import android.content.ServiceConnection +import android.content.pm.IPackageManager import android.content.pm.PackageManager import android.os.IBinder import android.view.Display @@ -56,6 +57,15 @@ fun newActivityTaskManager(): IActivityTaskManager? { return service.let(::ShizukuBinderWrapper).let(IActivityTaskManager.Stub::asInterface) } +fun newPackageManager(): IPackageManager? { + val service = SystemServiceHelper.getSystemService("package") + if (service == null) { + LogUtils.d("shizuku 无法获取 package") + return null + } + return service.let(::ShizukuBinderWrapper).let(IPackageManager.Stub::asInterface) +} + /** * -1: invalid fc * 1: (int) -> List diff --git a/app/src/main/kotlin/li/songe/gkd/ui/AdvancedPage.kt b/app/src/main/kotlin/li/songe/gkd/ui/AdvancedPage.kt index 3f5747e..25bfbc0 100644 --- a/app/src/main/kotlin/li/songe/gkd/ui/AdvancedPage.kt +++ b/app/src/main/kotlin/li/songe/gkd/ui/AdvancedPage.kt @@ -62,10 +62,8 @@ import com.ramcosta.composedestinations.annotation.Destination import com.ramcosta.composedestinations.annotation.RootNavGraph import com.ramcosta.composedestinations.navigation.navigate import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.update import li.songe.gkd.MainActivity -import li.songe.gkd.app import li.songe.gkd.appScope import li.songe.gkd.debug.FloatingService import li.songe.gkd.debug.HttpService @@ -89,12 +87,10 @@ import li.songe.gkd.ui.style.itemPadding import li.songe.gkd.ui.style.titleItemPadding import li.songe.gkd.util.LocalNavController import li.songe.gkd.util.ProfileTransitions -import li.songe.gkd.util.appInfoCacheFlow import li.songe.gkd.util.buildLogFile import li.songe.gkd.util.json import li.songe.gkd.util.launchAsFn import li.songe.gkd.util.launchTry -import li.songe.gkd.util.openApp import li.songe.gkd.util.openUri import li.songe.gkd.util.privacyStoreFlow import li.songe.gkd.util.saveFileToDownloads @@ -114,7 +110,6 @@ fun AdvancedPage() { val store by storeFlow.collectAsState() val snapshotCount by vm.snapshotCountFlow.collectAsState() - ShizukuErrorDialog(vm.shizukuErrorFlow) vm.uploadOptions.ShowDialog() var showEditPortDlg by remember { @@ -321,7 +316,7 @@ fun AdvancedPage() { Shizuku.requestPermission(Activity.RESULT_OK) } catch (e: Exception) { LogUtils.d("Shizuku授权错误", e.message) - vm.shizukuErrorFlow.value = true + context.mainVm.shizukuErrorFlow.value = true } }) ShizukuFragment(false) @@ -666,48 +661,3 @@ private fun ShizukuFragment(enabled: Boolean = true) { }) } - -@Composable -private fun ShizukuErrorDialog(stateFlow: MutableStateFlow) { - val state = stateFlow.collectAsState() - if (state.value) { - val appId = "moe.shizuku.privileged.api" - val appInfoCache = appInfoCacheFlow.collectAsState() - val installed = appInfoCache.value.contains(appId) - AlertDialog( - onDismissRequest = { stateFlow.value = false }, - title = { Text(text = "授权错误") }, - text = { - Text( - text = if (installed) { - "Shizuku 授权失败, 请检查是否运行" - } else { - "Shizuku 未安装, 请先下载后安装" - } - ) - }, - confirmButton = { - if (installed) { - TextButton(onClick = { - stateFlow.value = false - app.openApp(appId) - }) { - Text(text = "打开 Shizuku") - } - } else { - TextButton(onClick = { - stateFlow.value = false - app.openUri("https://shizuku.rikka.app/") - }) { - Text(text = "去下载") - } - } - }, - dismissButton = { - TextButton(onClick = { stateFlow.value = false }) { - Text(text = "我知道了") - } - } - ) - } -} diff --git a/app/src/main/kotlin/li/songe/gkd/ui/AdvancedVm.kt b/app/src/main/kotlin/li/songe/gkd/ui/AdvancedVm.kt index dc88865..efe01f0 100644 --- a/app/src/main/kotlin/li/songe/gkd/ui/AdvancedVm.kt +++ b/app/src/main/kotlin/li/songe/gkd/ui/AdvancedVm.kt @@ -2,7 +2,6 @@ package li.songe.gkd.ui import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.stateIn import li.songe.gkd.db.DbSet @@ -12,7 +11,5 @@ class AdvancedVm : ViewModel() { val snapshotCountFlow = DbSet.snapshotDao.count().stateIn(viewModelScope, SharingStarted.Eagerly, 0) - val shizukuErrorFlow = MutableStateFlow(false) - val uploadOptions = UploadOptions(viewModelScope) } \ No newline at end of file diff --git a/app/src/main/kotlin/li/songe/gkd/ui/AuthA11yPage.kt b/app/src/main/kotlin/li/songe/gkd/ui/AuthA11yPage.kt new file mode 100644 index 0000000..4d0059e --- /dev/null +++ b/app/src/main/kotlin/li/songe/gkd/ui/AuthA11yPage.kt @@ -0,0 +1,314 @@ +package li.songe.gkd.ui + +import android.app.Activity +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.RadioButton +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.material3.TopAppBar +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.lifecycle.viewModelScope +import androidx.lifecycle.viewmodel.compose.viewModel +import com.blankj.utilcode.util.ClipboardUtils +import com.blankj.utilcode.util.LogUtils +import com.ramcosta.composedestinations.annotation.Destination +import com.ramcosta.composedestinations.annotation.RootNavGraph +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.delay +import li.songe.gkd.META +import li.songe.gkd.MainActivity +import li.songe.gkd.permission.shizukuOkState +import li.songe.gkd.permission.writeSecureSettingsState +import li.songe.gkd.service.fixRestartService +import li.songe.gkd.shizuku.newPackageManager +import li.songe.gkd.util.LocalNavController +import li.songe.gkd.util.ProfileTransitions +import li.songe.gkd.util.launchAsFn +import li.songe.gkd.util.openA11ySettings +import li.songe.gkd.util.throttle +import li.songe.gkd.util.toast +import rikka.shizuku.Shizuku +import java.io.DataOutputStream + +@RootNavGraph +@Destination(style = ProfileTransitions::class) +@Composable +fun AuthA11yPage() { + val context = LocalContext.current as MainActivity + val navController = LocalNavController.current + + val vm = viewModel() + val showCopyDlg by vm.showCopyDlgFlow.collectAsState() + val writeSecureSettings by writeSecureSettingsState.stateFlow.collectAsState() + var mode by remember { mutableIntStateOf(-1) } + + val scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior() + Scaffold(modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection), topBar = { + TopAppBar(scrollBehavior = scrollBehavior, navigationIcon = { + IconButton(onClick = { + navController.popBackStack() + }) { + Icon( + imageVector = Icons.AutoMirrored.Filled.ArrowBack, + contentDescription = null, + ) + } + }, title = { + Text(text = "授权说明") + }, actions = {}) + }) { contentPadding -> + Column( + modifier = Modifier.padding(contentPadding) + ) { + if (writeSecureSettings) { + Spacer(modifier = Modifier.height(40.dp)) + Text( + text = "授权成功,请关闭此页面", + style = MaterialTheme.typography.bodyLarge, + modifier = Modifier.fillMaxWidth(), + textAlign = TextAlign.Center + ) + } else { + Text( + text = "选择一个授权模式", + style = MaterialTheme.typography.bodyLarge, + modifier = Modifier.padding(16.dp) + ) + Card( + modifier = Modifier + .padding(16.dp, 0.dp) + .fillMaxWidth(), + colors = CardDefaults.cardColors( + containerColor = if (mode == 0) { + MaterialTheme.colorScheme.primaryContainer + } else { + Color.Unspecified + } + ), + onClick = { mode = 0 } + ) { + Row( + verticalAlignment = Alignment.CenterVertically + ) { + RadioButton( + selected = mode == 0, + onClick = { mode = 0 } + ) + Text( + text = "普通授权(简单)", + style = MaterialTheme.typography.bodyLarge, + ) + } + Text( + modifier = Modifier.padding(16.dp, 0.dp), + style = MaterialTheme.typography.bodyMedium, + text = "1. 授予[无障碍权限]" + ) + Text( + modifier = Modifier.padding(16.dp, 0.dp), + style = MaterialTheme.typography.bodyMedium, + text = "2. 无障碍服务关闭后需重新授权" + ) + Row( + modifier = Modifier + .padding(16.dp, 0.dp) + .fillMaxWidth(), + ) { + TextButton(onClick = throttle { openA11ySettings() }) { + Text( + text = "手动授权", + style = MaterialTheme.typography.bodyLarge, + ) + } + } + Spacer(modifier = Modifier.height(12.dp)) + } + Spacer(modifier = Modifier.height(16.dp)) + Card( + modifier = Modifier + .padding(16.dp, 0.dp) + .fillMaxWidth(), + colors = CardDefaults.cardColors( + containerColor = if (mode == 1) { + MaterialTheme.colorScheme.primaryContainer + } else { + Color.Unspecified + } + ), + onClick = { mode = 1 } + ) { + Row( + verticalAlignment = Alignment.CenterVertically + ) { + RadioButton( + selected = mode == 1, + onClick = { mode = 1 } + ) + Text(text = "高级授权(推荐)") + } + Text( + modifier = Modifier.padding(16.dp, 0.dp), + style = MaterialTheme.typography.bodyMedium, + text = "1. 授予[写入安全设置权限]" + ) + Text( + modifier = Modifier.padding(16.dp, 0.dp), + style = MaterialTheme.typography.bodyMedium, + text = "2. 授权永久有效, 可自动重启无障碍服务" + ) + Text( + modifier = Modifier.padding(16.dp, 0.dp), + style = MaterialTheme.typography.bodyMedium, + text = "3. 搭配通知栏快捷图标可实现无感重启, 无限保活" + ) + Row( + modifier = Modifier + .padding(16.dp, 0.dp) + .fillMaxWidth(), + ) { + TextButton(onClick = throttle(fn = vm.viewModelScope.launchAsFn(Dispatchers.IO) { + context.grantPermissionByShizuku() + })) { + Text( + text = "Shizuku授权", + style = MaterialTheme.typography.bodyLarge, + ) + } + TextButton(onClick = throttle(fn = vm.viewModelScope.launchAsFn(Dispatchers.IO) { + grantPermissionByRoot() + })) { + Text( + text = "ROOT授权", + style = MaterialTheme.typography.bodyLarge, + ) + } + TextButton(onClick = { + vm.showCopyDlgFlow.value = true + }) { + Text( + text = "手动授权", + style = MaterialTheme.typography.bodyLarge, + ) + } + } + Spacer(modifier = Modifier.height(12.dp)) + } + } + } + } + + if (showCopyDlg) { + AlertDialog( + onDismissRequest = { vm.showCopyDlgFlow.value = false }, + title = { Text(text = "手动授权") }, + text = { + Column(modifier = Modifier.fillMaxWidth()) { + Text(text = "1. 有一台安装了 adb 的电脑\n\n2.手机开启调试模式后连接电脑授权调试\n\n3. 在电脑 cmd/pwsh 中运行如下命令") + Spacer(modifier = Modifier.height(4.dp)) + Text( + text = commandText, + modifier = Modifier + .clip(MaterialTheme.shapes.extraSmall) + .background(MaterialTheme.colorScheme.secondaryContainer) + .padding(4.dp) + ) + } + }, + confirmButton = { + TextButton(onClick = { + vm.showCopyDlgFlow.value = false + ClipboardUtils.copyText(commandText) + toast("复制成功") + }) { + Text(text = "复制并关闭") + } + }, + dismissButton = { + TextButton(onClick = { vm.showCopyDlgFlow.value = false }) { + Text(text = "关闭") + } + } + ) + } +} + +private val commandText by lazy { "adb pm grant ${META.appId} android.permission.WRITE_SECURE_SETTINGS" } + +private suspend fun MainActivity.grantPermissionByShizuku() { + if (shizukuOkState.stateFlow.value) { + try { + val service = newPackageManager() + if (service != null) { + service.grantRuntimePermission( + META.appId, + "android.permission.WRITE_SECURE_SETTINGS", + 0, // maybe others + ) + delay(500) + if (writeSecureSettingsState.updateAndGet()) { + toast("授权成功") + fixRestartService() + } + } + } catch (e: Exception) { + toast("授权失败:${e.message}") + LogUtils.d(e) + } + } else { + try { + Shizuku.requestPermission(Activity.RESULT_OK) + } catch (e: Exception) { + LogUtils.d("Shizuku授权错误", e.message) + mainVm.shizukuErrorFlow.value = true + } + } +} + +private fun grantPermissionByRoot() { + var p: Process? = null + try { + p = Runtime.getRuntime().exec("su") + val o = DataOutputStream(p.outputStream) + o.writeBytes("pm grant ${META.appId} android.permission.WRITE_SECURE_SETTINGS\nexit\n") + o.flush() + o.close() + p.waitFor() + if (p.exitValue() == 0) { + toast("授权成功") + } + } catch (e: Exception) { + toast("授权失败:${e.message}") + LogUtils.d(e) + } finally { + p?.destroy() + } +} diff --git a/app/src/main/kotlin/li/songe/gkd/ui/AuthA11yVm.kt b/app/src/main/kotlin/li/songe/gkd/ui/AuthA11yVm.kt new file mode 100644 index 0000000..4771d77 --- /dev/null +++ b/app/src/main/kotlin/li/songe/gkd/ui/AuthA11yVm.kt @@ -0,0 +1,24 @@ +package li.songe.gkd.ui + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.isActive +import kotlinx.coroutines.launch +import li.songe.gkd.permission.writeSecureSettingsState + +class AuthA11yVm : ViewModel() { + init { + viewModelScope.launch { + while (isActive) { + if (writeSecureSettingsState.updateAndGet()) { + break + } + delay(1000) + } + } + } + + val showCopyDlgFlow = MutableStateFlow(false) +} \ No newline at end of file diff --git a/app/src/main/kotlin/li/songe/gkd/ui/home/ControlPage.kt b/app/src/main/kotlin/li/songe/gkd/ui/home/ControlPage.kt index a239bed..3cb0220 100644 --- a/app/src/main/kotlin/li/songe/gkd/ui/home/ControlPage.kt +++ b/app/src/main/kotlin/li/songe/gkd/ui/home/ControlPage.kt @@ -39,6 +39,7 @@ import li.songe.gkd.ui.component.AuthCard import li.songe.gkd.ui.component.SettingItem import li.songe.gkd.ui.component.TextSwitch import li.songe.gkd.ui.destinations.ActivityLogPageDestination +import li.songe.gkd.ui.destinations.AuthA11yPageDestination import li.songe.gkd.ui.destinations.ClickLogPageDestination import li.songe.gkd.ui.destinations.SlowGroupPageDestination import li.songe.gkd.ui.style.EmptyHeight @@ -46,7 +47,6 @@ import li.songe.gkd.ui.style.itemPadding import li.songe.gkd.util.HOME_PAGE_URL import li.songe.gkd.util.LocalNavController import li.songe.gkd.util.launchAsFn -import li.songe.gkd.util.openA11ySettings import li.songe.gkd.util.openUri import li.songe.gkd.util.ruleSummaryFlow import li.songe.gkd.util.storeFlow @@ -110,8 +110,7 @@ fun useControlPage(): ScaffoldExt { title = "无障碍授权", desc = if (a11yBroken) "服务故障,请重新授权" else "授权使无障碍服务运行", onAuthClick = { - openA11ySettings() - // TODO context.mainVm.showA11yAuthDlgFlow.value = true + navController.navigate(AuthA11yPageDestination) }) } diff --git a/hidden_api/src/main/java/android/content/pm/IPackageManager.java b/hidden_api/src/main/java/android/content/pm/IPackageManager.java index dea8e1c..c641688 100644 --- a/hidden_api/src/main/java/android/content/pm/IPackageManager.java +++ b/hidden_api/src/main/java/android/content/pm/IPackageManager.java @@ -16,6 +16,8 @@ public interface IPackageManager extends IInterface { PackageInfo getPackageInfo(String packageName, long flags, int userId); + void grantRuntimePermission(String packageName, String permName, int user); + abstract class Stub { public static IPackageManager asInterface(IBinder obj) {