feat: auth writeSecureSettings

This commit is contained in:
lisonge 2024-09-12 00:17:01 +08:00
parent f859fa1d15
commit 2ce08c81a0
9 changed files with 409 additions and 57 deletions

View File

@ -11,7 +11,12 @@ import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge import androidx.activity.enableEdgeToEdge
import androidx.activity.viewModels import androidx.activity.viewModels
import androidx.compose.animation.core.AnimationConstants 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.CompositionLocalProvider
import androidx.compose.runtime.collectAsState
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.core.view.ViewCompat import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat 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.ui.theme.AppTheme
import li.songe.gkd.util.LocalNavController import li.songe.gkd.util.LocalNavController
import li.songe.gkd.util.UpgradeDialog import li.songe.gkd.util.UpgradeDialog
import li.songe.gkd.util.appInfoCacheFlow
import li.songe.gkd.util.initFolder import li.songe.gkd.util.initFolder
import li.songe.gkd.util.launchTry import li.songe.gkd.util.launchTry
import li.songe.gkd.util.map import li.songe.gkd.util.map
import li.songe.gkd.util.openApp
import li.songe.gkd.util.openUri
import li.songe.gkd.util.storeFlow import li.songe.gkd.util.storeFlow
class MainActivity : ComponentActivity() { class MainActivity : ComponentActivity() {
@ -81,6 +89,7 @@ class MainActivity : ComponentActivity() {
navGraph = NavGraphs.root, navGraph = NavGraphs.root,
navController = navController navController = navController
) )
ShizukuErrorDialog(mainVm.shizukuErrorFlow)
AuthDialog(mainVm.authReasonFlow) AuthDialog(mainVm.authReasonFlow)
BuildDialog(mainVm.dialogFlow) BuildDialog(mainVm.dialogFlow)
if (META.updateEnabled) { if (META.updateEnabled) {
@ -183,3 +192,48 @@ private fun Activity.fixTopPadding() {
ViewCompat.onApplyWindowInsets(view, windowInsets) ViewCompat.onApplyWindowInsets(view, windowInsets)
} }
} }
@Composable
private fun ShizukuErrorDialog(stateFlow: MutableStateFlow<Boolean>) {
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 = "我知道了")
}
}
)
}
}

View File

@ -41,6 +41,8 @@ class MainViewModel : ViewModel() {
val updateStatus = UpdateStatus() val updateStatus = UpdateStatus()
val shizukuErrorFlow = MutableStateFlow(false)
init { init {
viewModelScope.launchTry(Dispatchers.IO) { viewModelScope.launchTry(Dispatchers.IO) {
val subsItems = DbSet.subsItemDao.queryAll() val subsItems = DbSet.subsItemDao.queryAll()

View File

@ -5,6 +5,7 @@ import android.app.ActivityManager
import android.app.IActivityTaskManager import android.app.IActivityTaskManager
import android.content.ComponentName import android.content.ComponentName
import android.content.ServiceConnection import android.content.ServiceConnection
import android.content.pm.IPackageManager
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.os.IBinder import android.os.IBinder
import android.view.Display import android.view.Display
@ -56,6 +57,15 @@ fun newActivityTaskManager(): IActivityTaskManager? {
return service.let(::ShizukuBinderWrapper).let(IActivityTaskManager.Stub::asInterface) 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: invalid fc
* 1: (int) -> List<Task> * 1: (int) -> List<Task>

View File

@ -62,10 +62,8 @@ import com.ramcosta.composedestinations.annotation.Destination
import com.ramcosta.composedestinations.annotation.RootNavGraph import com.ramcosta.composedestinations.annotation.RootNavGraph
import com.ramcosta.composedestinations.navigation.navigate import com.ramcosta.composedestinations.navigation.navigate
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import li.songe.gkd.MainActivity import li.songe.gkd.MainActivity
import li.songe.gkd.app
import li.songe.gkd.appScope import li.songe.gkd.appScope
import li.songe.gkd.debug.FloatingService import li.songe.gkd.debug.FloatingService
import li.songe.gkd.debug.HttpService 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.ui.style.titleItemPadding
import li.songe.gkd.util.LocalNavController import li.songe.gkd.util.LocalNavController
import li.songe.gkd.util.ProfileTransitions import li.songe.gkd.util.ProfileTransitions
import li.songe.gkd.util.appInfoCacheFlow
import li.songe.gkd.util.buildLogFile import li.songe.gkd.util.buildLogFile
import li.songe.gkd.util.json import li.songe.gkd.util.json
import li.songe.gkd.util.launchAsFn import li.songe.gkd.util.launchAsFn
import li.songe.gkd.util.launchTry import li.songe.gkd.util.launchTry
import li.songe.gkd.util.openApp
import li.songe.gkd.util.openUri import li.songe.gkd.util.openUri
import li.songe.gkd.util.privacyStoreFlow import li.songe.gkd.util.privacyStoreFlow
import li.songe.gkd.util.saveFileToDownloads import li.songe.gkd.util.saveFileToDownloads
@ -114,7 +110,6 @@ fun AdvancedPage() {
val store by storeFlow.collectAsState() val store by storeFlow.collectAsState()
val snapshotCount by vm.snapshotCountFlow.collectAsState() val snapshotCount by vm.snapshotCountFlow.collectAsState()
ShizukuErrorDialog(vm.shizukuErrorFlow)
vm.uploadOptions.ShowDialog() vm.uploadOptions.ShowDialog()
var showEditPortDlg by remember { var showEditPortDlg by remember {
@ -321,7 +316,7 @@ fun AdvancedPage() {
Shizuku.requestPermission(Activity.RESULT_OK) Shizuku.requestPermission(Activity.RESULT_OK)
} catch (e: Exception) { } catch (e: Exception) {
LogUtils.d("Shizuku授权错误", e.message) LogUtils.d("Shizuku授权错误", e.message)
vm.shizukuErrorFlow.value = true context.mainVm.shizukuErrorFlow.value = true
} }
}) })
ShizukuFragment(false) ShizukuFragment(false)
@ -666,48 +661,3 @@ private fun ShizukuFragment(enabled: Boolean = true) {
}) })
} }
@Composable
private fun ShizukuErrorDialog(stateFlow: MutableStateFlow<Boolean>) {
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 = "我知道了")
}
}
)
}
}

View File

@ -2,7 +2,6 @@ package li.songe.gkd.ui
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.stateIn
import li.songe.gkd.db.DbSet import li.songe.gkd.db.DbSet
@ -12,7 +11,5 @@ class AdvancedVm : ViewModel() {
val snapshotCountFlow = val snapshotCountFlow =
DbSet.snapshotDao.count().stateIn(viewModelScope, SharingStarted.Eagerly, 0) DbSet.snapshotDao.count().stateIn(viewModelScope, SharingStarted.Eagerly, 0)
val shizukuErrorFlow = MutableStateFlow(false)
val uploadOptions = UploadOptions(viewModelScope) val uploadOptions = UploadOptions(viewModelScope)
} }

View File

@ -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<AuthA11yVm>()
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()
}
}

View File

@ -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)
}

View File

@ -39,6 +39,7 @@ import li.songe.gkd.ui.component.AuthCard
import li.songe.gkd.ui.component.SettingItem import li.songe.gkd.ui.component.SettingItem
import li.songe.gkd.ui.component.TextSwitch import li.songe.gkd.ui.component.TextSwitch
import li.songe.gkd.ui.destinations.ActivityLogPageDestination 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.ClickLogPageDestination
import li.songe.gkd.ui.destinations.SlowGroupPageDestination import li.songe.gkd.ui.destinations.SlowGroupPageDestination
import li.songe.gkd.ui.style.EmptyHeight 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.HOME_PAGE_URL
import li.songe.gkd.util.LocalNavController import li.songe.gkd.util.LocalNavController
import li.songe.gkd.util.launchAsFn import li.songe.gkd.util.launchAsFn
import li.songe.gkd.util.openA11ySettings
import li.songe.gkd.util.openUri import li.songe.gkd.util.openUri
import li.songe.gkd.util.ruleSummaryFlow import li.songe.gkd.util.ruleSummaryFlow
import li.songe.gkd.util.storeFlow import li.songe.gkd.util.storeFlow
@ -110,8 +110,7 @@ fun useControlPage(): ScaffoldExt {
title = "无障碍授权", title = "无障碍授权",
desc = if (a11yBroken) "服务故障,请重新授权" else "授权使无障碍服务运行", desc = if (a11yBroken) "服务故障,请重新授权" else "授权使无障碍服务运行",
onAuthClick = { onAuthClick = {
openA11ySettings() navController.navigate(AuthA11yPageDestination)
// TODO context.mainVm.showA11yAuthDlgFlow.value = true
}) })
} }

View File

@ -16,6 +16,8 @@ public interface IPackageManager extends IInterface {
PackageInfo getPackageInfo(String packageName, long flags, int userId); PackageInfo getPackageInfo(String packageName, long flags, int userId);
void grantRuntimePermission(String packageName, String permName, int user);
abstract class Stub { abstract class Stub {
public static IPackageManager asInterface(IBinder obj) { public static IPackageManager asInterface(IBinder obj) {