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 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)
|
||||
|
|
|
|||
|
|
@ -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<NewPatchViewModel>()
|
||||
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<NewPatchViewModel.InstallMethod?>(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
|
||||
|
|
|
|||
|
|
@ -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<Pair<AppInfo, PatchConfig>> 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<Pair<AppInfo, PatchConfig>> by mutableStateOf(emptyList())
|
||||
private set
|
||||
|
||||
var isRefreshing by mutableStateOf(false)
|
||||
private set
|
||||
|
||||
var updateLoaderState: ProcessingState<Result<Unit>> 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
|
||||
|
|
|
|||
Loading…
Reference in New Issue