perf: check shizuku state

This commit is contained in:
二刺螈
2025-07-17 19:46:51 +08:00
parent bc15ccafa5
commit 3b78f002e0
11 changed files with 170 additions and 105 deletions

View File

@@ -32,6 +32,7 @@ import li.songe.gkd.util.initSubsState
import li.songe.gkd.util.json
import li.songe.gkd.util.launchTry
import li.songe.gkd.util.setReactiveToastStyle
import li.songe.gkd.util.toast
import li.songe.json5.encodeToJson5String
import org.lsposed.hiddenapibypass.HiddenApiBypass
import rikka.shizuku.Shizuku
@@ -123,12 +124,16 @@ class App : Application() {
}
)
Shizuku.addBinderReceivedListener {
LogUtils.d("Shizuku.addBinderReceivedListener")
appScope.launchTry(Dispatchers.IO) {
shizukuOkState.updateAndGet()
}
}
Shizuku.addBinderDeadListener {
LogUtils.d("Shizuku.addBinderDeadListener")
shizukuOkState.stateFlow.value = false
val prefix = if (isActivityVisible()) "" else "${META.appName}: "
toast("${prefix}已断开 Shizuku 服务")
}
appScope.launchTry(Dispatchers.IO) {
initStore()

View File

@@ -10,7 +10,24 @@ import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.activity.viewModels
import androidx.compose.animation.core.AnimationConstants
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.text.selection.SelectionContainer
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.ContentCopy
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
@@ -18,10 +35,14 @@ import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.unit.dp
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.lifecycle.lifecycleScope
import androidx.navigation.compose.rememberNavController
import com.blankj.utilcode.util.LogUtils
import com.dylanc.activityresult.launcher.PickContentLauncher
import com.dylanc.activityresult.launcher.StartActivityLauncher
import com.ramcosta.composedestinations.DestinationsNavHost
@@ -29,25 +50,21 @@ import com.ramcosta.composedestinations.generated.NavGraphs
import com.ramcosta.composedestinations.generated.destinations.AuthA11YPageDestination
import com.ramcosta.composedestinations.utils.currentDestinationAsState
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.yield
import li.songe.gkd.debug.FloatingService
import li.songe.gkd.debug.HttpService
import li.songe.gkd.debug.ScreenshotService
import li.songe.gkd.permission.AuthDialog
import li.songe.gkd.permission.shizukuOkState
import li.songe.gkd.permission.updatePermissionState
import li.songe.gkd.service.A11yService
import li.songe.gkd.service.ManageService
import li.songe.gkd.service.fixRestartService
import li.songe.gkd.service.updateDefaultInputAppId
import li.songe.gkd.service.updateLauncherAppId
import li.songe.gkd.shizuku.execCommandForResult
import li.songe.gkd.store.storeFlow
import li.songe.gkd.ui.component.BuildDialog
import li.songe.gkd.ui.component.ShareDataDialog
@@ -61,15 +78,15 @@ import li.songe.gkd.util.EditGithubCookieDlg
import li.songe.gkd.util.ShortUrlSet
import li.songe.gkd.util.appInfoCacheFlow
import li.songe.gkd.util.componentName
import li.songe.gkd.util.copyText
import li.songe.gkd.util.fixSomeProblems
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.shizukuAppId
import li.songe.gkd.util.throttle
import li.songe.gkd.util.toast
import rikka.shizuku.Shizuku
import kotlin.coroutines.coroutineContext
import kotlin.reflect.KClass
import kotlin.reflect.jvm.jvmName
@@ -162,14 +179,6 @@ class MainActivity : ComponentActivity() {
super.onBackPressed()
}
}
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<out String?>,
grantResults: IntArray,
deviceId: Int
) {
}
}
private val activityVisibleFlow by lazy { MutableStateFlow(0) }
@@ -227,34 +236,70 @@ fun syncFixState() {
}
@Composable
private fun ShizukuErrorDialog(stateFlow: MutableStateFlow<Boolean>) {
private fun ShizukuErrorDialog(stateFlow: MutableStateFlow<Throwable?>) {
val state = stateFlow.collectAsState().value
if (state) {
if (state != null) {
val errorText = remember { state.stackTraceToString() }
val appInfoCache = appInfoCacheFlow.collectAsState().value
val installed = appInfoCache.contains(shizukuAppId)
AlertDialog(
onDismissRequest = { stateFlow.value = false },
onDismissRequest = { stateFlow.value = null },
title = { Text(text = "授权错误") },
text = {
Text(
text = if (installed) {
"Shizuku 授权失败, 请检查是否运行"
} else {
"Shizuku 授权失败, 检测到 Shizuku 未安装, 请先下载后安装, 如果你是通过其它方式授权, 请忽略此提示自行查找原因"
Column {
Text(
text = if (installed) {
"Shizuku 授权失败, 请检查是否运行"
} else {
"Shizuku 授权失败, 检测到 Shizuku 未安装, 请先下载后安装, 如果你是通过其它方式授权, 请忽略此提示自行查找原因"
}
)
Spacer(modifier = Modifier.height(8.dp))
Box(
modifier = Modifier.fillMaxWidth()
) {
SelectionContainer(
modifier = Modifier
.align(Alignment.TopStart)
.fillMaxWidth()
) {
Text(
text = errorText,
modifier = Modifier
.clip(MaterialTheme.shapes.extraSmall)
.background(MaterialTheme.colorScheme.secondaryContainer)
.padding(8.dp)
.heightIn(max = 400.dp)
.verticalScroll(rememberScrollState()),
style = MaterialTheme.typography.bodySmall,
)
}
Icon(
modifier = Modifier
.align(Alignment.TopEnd)
.clickable(onClick = throttle {
copyText(errorText)
})
.padding(4.dp)
.size(20.dp),
imageVector = Icons.Outlined.ContentCopy,
contentDescription = null,
tint = MaterialTheme.colorScheme.tertiary.copy(alpha = 0.75f),
)
}
)
}
},
confirmButton = {
if (installed) {
TextButton(onClick = {
stateFlow.value = false
stateFlow.value = null
openApp(shizukuAppId)
}) {
Text(text = "打开 Shizuku")
}
} else {
TextButton(onClick = {
stateFlow.value = false
stateFlow.value = null
openUri(ShortUrlSet.URL4)
}) {
Text(text = "去下载")
@@ -262,7 +307,7 @@ private fun ShizukuErrorDialog(stateFlow: MutableStateFlow<Boolean>) {
}
},
dismissButton = {
TextButton(onClick = { stateFlow.value = false }) {
TextButton(onClick = { stateFlow.value = null }) {
Text(text = "我知道了")
}
}
@@ -321,24 +366,3 @@ fun AccessRestrictedSettingsDlg() {
)
}
}
suspend fun MainActivity.grantPermissionByShizuku(command: String) {
if (shizukuOkState.stateFlow.value) {
try {
execCommandForResult(command)
return
} 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
}
}
coroutineContext[Job]?.cancel()
yield()
}

View File

@@ -1,5 +1,6 @@
package li.songe.gkd
import android.app.Activity
import android.content.ComponentName
import android.content.Intent
import android.net.Uri
@@ -34,7 +35,9 @@ import li.songe.gkd.debug.FloatingTileService
import li.songe.gkd.debug.HttpTileService
import li.songe.gkd.debug.SnapshotTileService
import li.songe.gkd.permission.AuthReason
import li.songe.gkd.permission.shizukuOkState
import li.songe.gkd.service.MatchTileService
import li.songe.gkd.shizuku.execCommandForResult
import li.songe.gkd.store.createTextFlow
import li.songe.gkd.store.storeFlow
import li.songe.gkd.ui.component.AlertDialogOptions
@@ -53,11 +56,13 @@ import li.songe.gkd.util.componentName
import li.songe.gkd.util.launchTry
import li.songe.gkd.util.openUri
import li.songe.gkd.util.openWeChatScaner
import li.songe.gkd.util.stopCoroutine
import li.songe.gkd.util.subsFolder
import li.songe.gkd.util.subsItemsFlow
import li.songe.gkd.util.toast
import li.songe.gkd.util.updateSubsMutex
import li.songe.gkd.util.updateSubscription
import rikka.shizuku.Shizuku
import java.lang.ref.WeakReference
private var tempTermsAccepted = false
@@ -87,7 +92,7 @@ class MainViewModel : ViewModel() {
val updateStatus = if (META.updateEnabled) UpdateStatus(viewModelScope) else null
val shizukuErrorFlow = MutableStateFlow(false)
val shizukuErrorFlow = MutableStateFlow<Throwable?>(null)
val uploadOptions = UploadOptions(this)
@@ -270,6 +275,26 @@ class MainViewModel : ViewModel() {
)
}
suspend fun grantPermissionByShizuku(command: String) {
if (shizukuOkState.stateFlow.value) {
try {
execCommandForResult(command)
return
} catch (e: Exception) {
toast("运行失败:${e.message}")
LogUtils.d(e)
}
} else {
try {
Shizuku.requestPermission(Activity.RESULT_OK)
} catch (e: Throwable) {
LogUtils.d("Shizuku授权错误", e.message)
shizukuErrorFlow.value = e
}
}
stopCoroutine()
}
init {
viewModelScope.launchTry(Dispatchers.IO) {
val subsItems = DbSet.subsItemDao.queryAll()

View File

@@ -42,6 +42,8 @@ class PermissionState(
val reason: AuthReason? = null,
) {
val stateFlow = MutableStateFlow(false)
val value: Boolean
get() = stateFlow.value
fun updateAndGet(): Boolean {
return stateFlow.updateAndGet { check() }
}

View File

@@ -126,7 +126,9 @@ val activityTaskManagerFlow by lazy<StateFlow<SafeActivityTaskManager?>> {
val stateFlow = MutableStateFlow<SafeActivityTaskManager?>(null)
appScope.launchTry(Dispatchers.IO) {
shizukuActivityUsedFlow.collect {
stateFlow.value?.unregisterTaskStackListener(taskListener)
if (shizukuOkState.value) {
stateFlow.value?.unregisterTaskStackListener(taskListener)
}
stateFlow.value = if (it) newActivityTaskManager() else null
stateFlow.value?.registerTaskStackListener(taskListener)
}

View File

@@ -22,8 +22,7 @@ import rikka.shizuku.Shizuku
fun shizukuCheckGranted(): Boolean {
val granted = try {
Shizuku.checkSelfPermission() == PackageManager.PERMISSION_GRANTED
} catch (e: Exception) {
e.printStackTrace()
} catch (_: Exception) {
false
}
if (!granted) return false

View File

@@ -116,14 +116,13 @@ private fun IUserService.execCommandForResult(command: String): Boolean? {
private fun unbindUserService(serviceArgs: Shizuku.UserServiceArgs, connection: ServiceConnection) {
if (!shizukuOkState.stateFlow.value) return
LogUtils.d("unbindUserService", serviceArgs)
// https://github.com/RikkaApps/Shizuku-API/blob/master/server-shared/src/main/java/rikka/shizuku/server/UserServiceManager.java#L62
try {
Shizuku.unbindUserService(serviceArgs, connection, false)
Shizuku.unbindUserService(serviceArgs, connection, true)
} catch (e: Exception) {
// binder haven't been received
e.printStackTrace()
LogUtils.d(e)
}
}
@@ -144,6 +143,10 @@ data class UserServiceWrapper(
private val bindServiceMutex by lazy { Mutex() }
suspend fun buildServiceWrapper(): UserServiceWrapper? {
if (bindServiceMutex.isLocked) {
toast("正在获取 Shizuku 服务,请稍后再试")
return null
}
val serviceArgs = Shizuku
.UserServiceArgs(UserService::class.componentName)
.daemon(false)
@@ -183,7 +186,7 @@ suspend fun buildServiceWrapper(): UserServiceWrapper? {
}
}.apply {
if (this == null) {
toast("Shizuku获取绑定服务超时失败")
toast("获取 Shizuku 服务超时失败")
unbindUserService(serviceArgs, connection)
}
}
@@ -201,9 +204,9 @@ val serviceWrapperFlow by lazy {
appScope.launch(Dispatchers.IO) {
shizukuServiceUsedFlow.collect {
if (it) {
stateFlow.update { it ?: buildServiceWrapper() }
stateFlow.update { s -> s ?: buildServiceWrapper() }
} else {
stateFlow.update { it?.destroy(); null }
stateFlow.update { s -> s?.destroy(); null }
}
}
}

View File

@@ -63,8 +63,8 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withTimeoutOrNull
import li.songe.gkd.MainActivity
import li.songe.gkd.appScope
import li.songe.gkd.debug.FloatingService
import li.songe.gkd.debug.HttpService
import li.songe.gkd.debug.ScreenshotService
@@ -94,6 +94,7 @@ import li.songe.gkd.util.appInfoCacheFlow
import li.songe.gkd.util.launchAsFn
import li.songe.gkd.util.shizukuAppId
import li.songe.gkd.util.shizukuMiniVersionCode
import li.songe.gkd.util.stopCoroutine
import li.songe.gkd.util.throttle
import li.songe.gkd.util.toast
import rikka.shizuku.Shizuku
@@ -209,13 +210,13 @@ fun AdvancedPage() {
onAuthClick = {
try {
Shizuku.requestPermission(Activity.RESULT_OK)
} catch (e: Exception) {
} catch (e: Throwable) {
LogUtils.d("Shizuku授权错误", e.message)
mainVm.shizukuErrorFlow.value = true
mainVm.shizukuErrorFlow.value = e
}
})
}
ShizukuFragment(shizukuOk)
ShizukuFragment(vm, shizukuOk)
val server by HttpService.httpServerFlow.collectAsState()
val httpServerRunning = server != null
@@ -474,8 +475,30 @@ fun AdvancedPage() {
private val checkShizukuMutex by lazy { Mutex() }
private suspend fun checkShizukuFeat(block: suspend () -> Boolean) {
if (checkShizukuMutex.isLocked) {
toast("正在检测中, 请稍后再试")
stopCoroutine()
}
checkShizukuMutex.withLock {
toast("检测中")
val r = withTimeoutOrNull(3000) {
block()
}
if (r == null) {
toast("检测超时,请重试")
stopCoroutine()
}
if (!r) {
toast("检测失败,无法使用")
stopCoroutine()
}
toast("已启用")
}
}
@Composable
private fun ShizukuFragment(enabled: Boolean = true) {
private fun ShizukuFragment(vm: AdvancedVm, enabled: Boolean = true) {
val shizukuStore by shizukuStoreFlow.collectAsState()
val mainVm = LocalMainViewModel.current
TextSwitch(
@@ -487,18 +510,11 @@ private fun ShizukuFragment(enabled: Boolean = true) {
},
checked = shizukuStore.enableActivity,
enabled = enabled,
onCheckedChange = appScope.launchAsFn<Boolean>(Dispatchers.IO) {
checkShizukuMutex.withLock {
if (it) {
toast("检测中")
if (!shizukuCheckActivity()) {
toast("检测失败,无法使用")
return@launchAsFn
}
toast("已启用")
}
shizukuStoreFlow.update { s -> s.copy(enableActivity = it) }
onCheckedChange = vm.viewModelScope.launchAsFn<Boolean>(Dispatchers.IO) {
if (it) {
checkShizukuFeat { shizukuCheckActivity() }
}
shizukuStoreFlow.update { s -> s.copy(enableActivity = it) }
})
TextSwitch(
@@ -510,18 +526,11 @@ private fun ShizukuFragment(enabled: Boolean = true) {
},
checked = shizukuStore.enableTapClick,
enabled = enabled,
onCheckedChange = appScope.launchAsFn<Boolean>(Dispatchers.IO) {
checkShizukuMutex.withLock {
if (it) {
toast("检测中")
if (!shizukuCheckUserService()) {
toast("检测失败,无法使用")
return@launchAsFn
}
toast("已启用")
}
shizukuStoreFlow.update { s -> s.copy(enableTapClick = it) }
onCheckedChange = vm.viewModelScope.launchAsFn<Boolean>(Dispatchers.IO) {
if (it) {
checkShizukuFeat { shizukuCheckUserService() }
}
shizukuStoreFlow.update { s -> s.copy(enableTapClick = it) }
})
@@ -534,18 +543,11 @@ private fun ShizukuFragment(enabled: Boolean = true) {
},
checked = shizukuStore.enableWorkProfile,
enabled = enabled,
onCheckedChange = appScope.launchAsFn<Boolean>(Dispatchers.IO) {
checkShizukuMutex.withLock {
if (it) {
toast("检测中")
if (!shizukuCheckWorkProfile()) {
toast("检测失败,无法使用")
return@launchAsFn
}
toast("已启用")
}
shizukuStoreFlow.update { s -> s.copy(enableWorkProfile = it) }
onCheckedChange = vm.viewModelScope.launchAsFn<Boolean>(Dispatchers.IO) {
if (it) {
checkShizukuFeat { shizukuCheckWorkProfile() }
}
shizukuStoreFlow.update { s -> s.copy(enableWorkProfile = it) }
})
}

View File

@@ -1,6 +1,5 @@
package li.songe.gkd.ui
import androidx.activity.compose.LocalActivity
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
@@ -32,12 +31,11 @@ import com.ramcosta.composedestinations.annotation.Destination
import com.ramcosta.composedestinations.annotation.RootGraph
import kotlinx.coroutines.Dispatchers
import li.songe.gkd.META
import li.songe.gkd.MainActivity
import li.songe.gkd.grantPermissionByShizuku
import li.songe.gkd.permission.foregroundServiceSpecialUseState
import li.songe.gkd.ui.component.AuthButtonGroup
import li.songe.gkd.ui.component.EmptyText
import li.songe.gkd.ui.component.ManualAuthDialog
import li.songe.gkd.ui.local.LocalMainViewModel
import li.songe.gkd.ui.local.LocalNavController
import li.songe.gkd.ui.style.EmptyHeight
import li.songe.gkd.ui.style.ProfileTransitions
@@ -51,7 +49,7 @@ import li.songe.gkd.util.toast
@Destination<RootGraph>(style = ProfileTransitions::class)
@Composable
fun AppOpsAllowPage() {
val context = LocalActivity.current as MainActivity
val mainVm = LocalMainViewModel.current
val navController = LocalNavController.current
val vm = viewModel<AppOpsAllowVm>()
val scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior()
@@ -99,7 +97,7 @@ fun AppOpsAllowPage() {
)
AuthButtonGroup(
onClickShizuku = vm.viewModelScope.launchAsFn(Dispatchers.IO) {
context.grantPermissionByShizuku(appOpsCommand)
mainVm.grantPermissionByShizuku(appOpsCommand)
toast("授权成功")
},
onClickManual = {

View File

@@ -1,7 +1,6 @@
package li.songe.gkd.ui
import android.os.Build
import androidx.activity.compose.LocalActivity
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
@@ -36,8 +35,6 @@ import com.ramcosta.composedestinations.generated.destinations.WebViewPageDestin
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.update
import li.songe.gkd.META
import li.songe.gkd.MainActivity
import li.songe.gkd.grantPermissionByShizuku
import li.songe.gkd.permission.writeSecureSettingsState
import li.songe.gkd.service.A11yService
import li.songe.gkd.service.fixRestartService
@@ -270,11 +267,11 @@ private fun successAuthExec() {
@Composable
private fun A11yAuthButtonGroup() {
val context = LocalActivity.current as MainActivity
val mainVm = LocalMainViewModel.current
val vm = viewModel<AuthA11yVm>()
AuthButtonGroup(
onClickShizuku = vm.viewModelScope.launchAsFn(Dispatchers.IO) {
context.grantPermissionByShizuku(a11yCommandText)
mainVm.grantPermissionByShizuku(a11yCommandText)
successAuthExec()
},
onClickManual = {

View File

@@ -5,9 +5,12 @@ import com.blankj.utilcode.util.LogUtils
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.CoroutineStart
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import kotlinx.coroutines.yield
import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.EmptyCoroutineContext
import kotlin.coroutines.coroutineContext
fun CoroutineScope.launchTry(
context: CoroutineContext = EmptyCoroutineContext,
@@ -65,3 +68,8 @@ fun <T> CoroutineScope.launchAsFn(
}
}
suspend fun stopCoroutine(): Nothing {
coroutineContext[Job]?.cancel()
yield()
throw CancellationException("Coroutine stopped")
}