diff --git a/build.gradle.kts b/build.gradle.kts index 2d58fb4..09acfe9 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -55,7 +55,7 @@ val coreVerName by extra(coreLatestTag) val androidMinSdkVersion by extra(27) val androidTargetSdkVersion by extra(36) val androidCompileSdkVersion by extra(36) -val androidCompileNdkVersion by extra("29.0.13113456") +val androidCompileNdkVersion by extra("29.0.13599879") val androidBuildToolsVersion by extra("36.0.0") val androidSourceCompatibility by extra(JavaVersion.VERSION_21) val androidTargetCompatibility by extra(JavaVersion.VERSION_21) diff --git a/manager/src/main/java/org/lsposed/npatch/ui/page/NewPatchScreen.kt b/manager/src/main/java/org/lsposed/npatch/ui/page/NewPatchScreen.kt index 6c4fd9a..e5867c5 100644 --- a/manager/src/main/java/org/lsposed/npatch/ui/page/NewPatchScreen.kt +++ b/manager/src/main/java/org/lsposed/npatch/ui/page/NewPatchScreen.kt @@ -1,10 +1,12 @@ package org.lsposed.npatch.ui.page import android.annotation.SuppressLint +import android.content.BroadcastReceiver import android.content.ClipData import android.content.ClipboardManager import android.content.Context import android.content.Context.RECEIVER_NOT_EXPORTED +import android.content.Intent import android.content.IntentFilter import android.content.pm.PackageInstaller import android.net.Uri @@ -406,6 +408,35 @@ private fun DoPatchBody(modifier: Modifier, navigator: DestinationsNavigator) { val viewModel = viewModel() val snackbarHost = LocalSnackbarHost.current val scope = rememberCoroutineScope() + val context = LocalContext.current + + // 监听应用安装广播 + DisposableEffect(viewModel.patchApp.app.packageName) { + val receiver = object : BroadcastReceiver() { + override fun onReceive(context: Context, intent: Intent) { + val action = intent.action + val data = intent.data + val pkgName = data?.schemeSpecificPart + + // 只有当包名匹配,且动作是 添加 或 替换 时才认为是安装成功 + if (pkgName == viewModel.patchApp.app.packageName) { + if (action == Intent.ACTION_PACKAGE_ADDED || action == Intent.ACTION_PACKAGE_REPLACED) { + scope.launch { + snackbarHost.showSnackbar(context.getString(R.string.patch_install_successfully)) + navigator.navigateUp() + } + } + } + } + } + val filter = IntentFilter().apply { + addAction(Intent.ACTION_PACKAGE_ADDED) + addAction(Intent.ACTION_PACKAGE_REPLACED) + addDataScheme("package") + } + context.registerReceiver(receiver, filter) + onDispose { context.unregisterReceiver(receiver) } + } LaunchedEffect(Unit) { if (viewModel.logs.isEmpty()) { @@ -454,16 +485,20 @@ private fun DoPatchBody(modifier: Modifier, navigator: DestinationsNavigator) { when (viewModel.patchState) { PatchState.PATCHING -> BackHandler {} PatchState.FINISHED -> { - val installSuccessfully = stringResource(R.string.patch_install_successfully) + // val installSuccessfully = stringResource(R.string.patch_install_successfully) // 移交给 BroadcastReceiver 处理 val installFailed = stringResource(R.string.patch_install_failed) val copyError = stringResource(R.string.copy_error) var installation by remember { mutableStateOf(null) } + val onFinish: (Int, String?) -> Unit = { status, message -> scope.launch { if (status == PackageInstaller.STATUS_SUCCESS) { - snackbarHost.showSnackbar(installSuccessfully) - navigator.navigateUp() + // Shizuku 安装成功的情况 + // 此处不执行 navigateUp,等待 BroadcastReceiver 收到系统广播后再统一跳转 + // 这样可以保证应用确实已被系统注册 + Log.i(TAG, "Install reported success, waiting for broadcast to navigate.") } else if (status != NPackageManager.STATUS_USER_CANCELLED) { + // 安装失败处理 val result = snackbarHost.showSnackbar(installFailed, copyError) if (result == SnackbarResult.ActionPerformed) { val cm = lspApp.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager @@ -565,7 +600,7 @@ private fun InstallDialog(patchApp: AppInfo, onFinish: (Int, String?) -> Unit) { } LaunchedEffect(uninstallFirst) { - if (!uninstallFirst && installing == 0) { + if (!uninstallFirst && installing == 0) { onFinish(NPackageManager.STATUS_USER_CANCELLED, "User cancelled") doInstall() } diff --git a/manager/src/main/java/org/lsposed/npatch/ui/viewmodel/manage/AppManageViewModel.kt b/manager/src/main/java/org/lsposed/npatch/ui/viewmodel/manage/AppManageViewModel.kt index 3ac4388..990cce8 100644 --- a/manager/src/main/java/org/lsposed/npatch/ui/viewmodel/manage/AppManageViewModel.kt +++ b/manager/src/main/java/org/lsposed/npatch/ui/viewmodel/manage/AppManageViewModel.kt @@ -3,14 +3,17 @@ package org.lsposed.npatch.ui.viewmodel.manage import android.content.pm.PackageInstaller import android.util.Base64 import android.util.Log -import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue +import androidx.compose.runtime.snapshotFlow import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.google.gson.Gson import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.lsposed.npatch.Patcher @@ -31,6 +34,7 @@ class AppManageViewModel : ViewModel() { companion object { private const val TAG = "ManageViewModel" + private const val AUTO_REFRESH_INTERVAL = 90_114L } sealed class ViewAction { @@ -38,22 +42,15 @@ class AppManageViewModel : ViewModel() { object ClearUpdateLoaderResult : ViewAction() data class PerformOptimize(val appInfo: AppInfo) : ViewAction() object ClearOptimizeResult : ViewAction() + object RefreshList : ViewAction() } - val appList: List> by derivedStateOf { - NPackageManager.appList.mapNotNull { appInfo -> - runCatching { - appInfo.app.metaData?.getString("npatch")?.let { - val json = Base64.decode(it, Base64.DEFAULT).toString(Charsets.UTF_8) - Log.d(TAG, "Read patched config: $json") - val config = Gson().fromJson(json, PatchConfig::class.java) - if (config?.lspConfig == null) null else appInfo to config - } - }.getOrNull() - }.also { - Log.d(TAG, "Loaded ${it.size} patched apps") - } - } + // 手動管理狀態,避免實時響應系統廣播導致列表跳動 + var appList: List> by mutableStateOf(emptyList()) + private set + + var isRefreshing by mutableStateOf(false) + private set var updateLoaderState: ProcessingState> by mutableStateOf(ProcessingState.Idle) private set @@ -75,6 +72,25 @@ class AppManageViewModel : ViewModel() { } } + init { + viewModelScope.launch { + snapshotFlow { NPackageManager.appList } + .filter { it.isNotEmpty() } + .first() + Log.d(TAG, "Initial data ready, starting auto-refresh loop") + // 啓動立即加载 + loadData() + + while (true) { + delay(AUTO_REFRESH_INTERVAL) + Log.d(TAG, "Auto refreshing app list (90s timer)") + if (!isRefreshing) { + loadData(silent = true) + } + } + } + } + fun dispatch(action: ViewAction) { viewModelScope.launch { when (action) { @@ -82,10 +98,30 @@ class AppManageViewModel : ViewModel() { is ViewAction.ClearUpdateLoaderResult -> updateLoaderState = ProcessingState.Idle is ViewAction.PerformOptimize -> performOptimize(action.appInfo) is ViewAction.ClearOptimizeResult -> optimizeState = ProcessingState.Idle + is ViewAction.RefreshList -> loadData(silent = false) } } } + // silent 参数用于区分是否显示 loading 状态 + private fun loadData(silent: Boolean = false) { + if (!silent) isRefreshing = true + val currentList = NPackageManager.appList.mapNotNull { appInfo -> + runCatching { + appInfo.app.metaData?.getString("npatch")?.let { + val json = Base64.decode(it, Base64.DEFAULT).toString(Charsets.UTF_8) + val config = Gson().fromJson(json, PatchConfig::class.java) + if (config?.lspConfig == null) null else appInfo to config + } + }.getOrNull() + } + + Log.d(TAG, "Loaded ${currentList.size} patched apps") + appList = currentList + + if (!silent) isRefreshing = false + } + private suspend fun updateLoader(appInfo: AppInfo, config: PatchConfig) { Log.i(TAG, "Update loader for ${appInfo.app.packageName}") updateLoaderState = ProcessingState.Processing