feat: 增加已修補列表自動刷新 & 優化安裝完成後的跳轉邏輯
1. AppManageViewModel: - 實現已修補應用列表的自動刷新機制(每 90 秒輪詢一次)。 - 引入 `silent` 參數區分自動刷新與手動刷新,避免自動刷新時 UI 出現加載轉圈,提升體驗。 2. NewPatchPage: - 重構安裝成功的判定邏輯:不再僅依賴 PackageInstaller 的回調。 - 新增 `BroadcastReceiver` 監聽系統 `PACKAGE_ADDED` 和 `PACKAGE_REPLACED` 廣播。 - 確保系統真正註冊完新包後再執行導航返回 (navigateUp),解決安裝後立即返回導致列表尚未更新或狀態不同步的問題。
This commit is contained in:
parent
5aa809dc5a
commit
af548bb25d
|
|
@ -55,7 +55,7 @@ val coreVerName by extra(coreLatestTag)
|
||||||
val androidMinSdkVersion by extra(27)
|
val androidMinSdkVersion by extra(27)
|
||||||
val androidTargetSdkVersion by extra(36)
|
val androidTargetSdkVersion by extra(36)
|
||||||
val androidCompileSdkVersion 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 androidBuildToolsVersion by extra("36.0.0")
|
||||||
val androidSourceCompatibility by extra(JavaVersion.VERSION_21)
|
val androidSourceCompatibility by extra(JavaVersion.VERSION_21)
|
||||||
val androidTargetCompatibility by extra(JavaVersion.VERSION_21)
|
val androidTargetCompatibility by extra(JavaVersion.VERSION_21)
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,12 @@
|
||||||
package org.lsposed.npatch.ui.page
|
package org.lsposed.npatch.ui.page
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
|
import android.content.BroadcastReceiver
|
||||||
import android.content.ClipData
|
import android.content.ClipData
|
||||||
import android.content.ClipboardManager
|
import android.content.ClipboardManager
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Context.RECEIVER_NOT_EXPORTED
|
import android.content.Context.RECEIVER_NOT_EXPORTED
|
||||||
|
import android.content.Intent
|
||||||
import android.content.IntentFilter
|
import android.content.IntentFilter
|
||||||
import android.content.pm.PackageInstaller
|
import android.content.pm.PackageInstaller
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
|
|
@ -406,6 +408,35 @@ private fun DoPatchBody(modifier: Modifier, navigator: DestinationsNavigator) {
|
||||||
val viewModel = viewModel<NewPatchViewModel>()
|
val viewModel = viewModel<NewPatchViewModel>()
|
||||||
val snackbarHost = LocalSnackbarHost.current
|
val snackbarHost = LocalSnackbarHost.current
|
||||||
val scope = rememberCoroutineScope()
|
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) {
|
LaunchedEffect(Unit) {
|
||||||
if (viewModel.logs.isEmpty()) {
|
if (viewModel.logs.isEmpty()) {
|
||||||
|
|
@ -454,16 +485,20 @@ private fun DoPatchBody(modifier: Modifier, navigator: DestinationsNavigator) {
|
||||||
when (viewModel.patchState) {
|
when (viewModel.patchState) {
|
||||||
PatchState.PATCHING -> BackHandler {}
|
PatchState.PATCHING -> BackHandler {}
|
||||||
PatchState.FINISHED -> {
|
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 installFailed = stringResource(R.string.patch_install_failed)
|
||||||
val copyError = stringResource(R.string.copy_error)
|
val copyError = stringResource(R.string.copy_error)
|
||||||
var installation by remember { mutableStateOf<NewPatchViewModel.InstallMethod?>(null) }
|
var installation by remember { mutableStateOf<NewPatchViewModel.InstallMethod?>(null) }
|
||||||
|
|
||||||
val onFinish: (Int, String?) -> Unit = { status, message ->
|
val onFinish: (Int, String?) -> Unit = { status, message ->
|
||||||
scope.launch {
|
scope.launch {
|
||||||
if (status == PackageInstaller.STATUS_SUCCESS) {
|
if (status == PackageInstaller.STATUS_SUCCESS) {
|
||||||
snackbarHost.showSnackbar(installSuccessfully)
|
// Shizuku 安装成功的情况
|
||||||
navigator.navigateUp()
|
// 此处不执行 navigateUp,等待 BroadcastReceiver 收到系统广播后再统一跳转
|
||||||
|
// 这样可以保证应用确实已被系统注册
|
||||||
|
Log.i(TAG, "Install reported success, waiting for broadcast to navigate.")
|
||||||
} else if (status != NPackageManager.STATUS_USER_CANCELLED) {
|
} else if (status != NPackageManager.STATUS_USER_CANCELLED) {
|
||||||
|
// 安装失败处理
|
||||||
val result = snackbarHost.showSnackbar(installFailed, copyError)
|
val result = snackbarHost.showSnackbar(installFailed, copyError)
|
||||||
if (result == SnackbarResult.ActionPerformed) {
|
if (result == SnackbarResult.ActionPerformed) {
|
||||||
val cm = lspApp.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
val cm = lspApp.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
||||||
|
|
@ -565,7 +600,7 @@ private fun InstallDialog(patchApp: AppInfo, onFinish: (Int, String?) -> Unit) {
|
||||||
}
|
}
|
||||||
|
|
||||||
LaunchedEffect(uninstallFirst) {
|
LaunchedEffect(uninstallFirst) {
|
||||||
if (!uninstallFirst && installing == 0) {
|
if (!uninstallFirst && installing == 0) {
|
||||||
onFinish(NPackageManager.STATUS_USER_CANCELLED, "User cancelled")
|
onFinish(NPackageManager.STATUS_USER_CANCELLED, "User cancelled")
|
||||||
doInstall()
|
doInstall()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,14 +3,17 @@ package org.lsposed.npatch.ui.viewmodel.manage
|
||||||
import android.content.pm.PackageInstaller
|
import android.content.pm.PackageInstaller
|
||||||
import android.util.Base64
|
import android.util.Base64
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.compose.runtime.derivedStateOf
|
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.runtime.snapshotFlow
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import com.google.gson.Gson
|
import com.google.gson.Gson
|
||||||
import kotlinx.coroutines.Dispatchers
|
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.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import org.lsposed.npatch.Patcher
|
import org.lsposed.npatch.Patcher
|
||||||
|
|
@ -31,6 +34,7 @@ class AppManageViewModel : ViewModel() {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val TAG = "ManageViewModel"
|
private const val TAG = "ManageViewModel"
|
||||||
|
private const val AUTO_REFRESH_INTERVAL = 90_114L
|
||||||
}
|
}
|
||||||
|
|
||||||
sealed class ViewAction {
|
sealed class ViewAction {
|
||||||
|
|
@ -38,22 +42,15 @@ class AppManageViewModel : ViewModel() {
|
||||||
object ClearUpdateLoaderResult : ViewAction()
|
object ClearUpdateLoaderResult : ViewAction()
|
||||||
data class PerformOptimize(val appInfo: AppInfo) : ViewAction()
|
data class PerformOptimize(val appInfo: AppInfo) : ViewAction()
|
||||||
object ClearOptimizeResult : ViewAction()
|
object ClearOptimizeResult : ViewAction()
|
||||||
|
object RefreshList : ViewAction()
|
||||||
}
|
}
|
||||||
|
|
||||||
val appList: List<Pair<AppInfo, PatchConfig>> by derivedStateOf {
|
// 手動管理狀態,避免實時響應系統廣播導致列表跳動
|
||||||
NPackageManager.appList.mapNotNull { appInfo ->
|
var appList: List<Pair<AppInfo, PatchConfig>> by mutableStateOf(emptyList())
|
||||||
runCatching {
|
private set
|
||||||
appInfo.app.metaData?.getString("npatch")?.let {
|
|
||||||
val json = Base64.decode(it, Base64.DEFAULT).toString(Charsets.UTF_8)
|
var isRefreshing by mutableStateOf(false)
|
||||||
Log.d(TAG, "Read patched config: $json")
|
private set
|
||||||
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 updateLoaderState: ProcessingState<Result<Unit>> by mutableStateOf(ProcessingState.Idle)
|
var updateLoaderState: ProcessingState<Result<Unit>> by mutableStateOf(ProcessingState.Idle)
|
||||||
private set
|
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) {
|
fun dispatch(action: ViewAction) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
when (action) {
|
when (action) {
|
||||||
|
|
@ -82,10 +98,30 @@ class AppManageViewModel : ViewModel() {
|
||||||
is ViewAction.ClearUpdateLoaderResult -> updateLoaderState = ProcessingState.Idle
|
is ViewAction.ClearUpdateLoaderResult -> updateLoaderState = ProcessingState.Idle
|
||||||
is ViewAction.PerformOptimize -> performOptimize(action.appInfo)
|
is ViewAction.PerformOptimize -> performOptimize(action.appInfo)
|
||||||
is ViewAction.ClearOptimizeResult -> optimizeState = ProcessingState.Idle
|
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) {
|
private suspend fun updateLoader(appInfo: AppInfo, config: PatchConfig) {
|
||||||
Log.i(TAG, "Update loader for ${appInfo.app.packageName}")
|
Log.i(TAG, "Update loader for ${appInfo.app.packageName}")
|
||||||
updateLoaderState = ProcessingState.Processing
|
updateLoaderState = ProcessingState.Processing
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue