Skip to content
2 changes: 1 addition & 1 deletion app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ android {
// Please change the applicationId to one that does not conflict with any official release.
applicationId = "com.rosan.installer.x.revived"
namespace = "com.rosan.installer"
minSdk = 34
minSdk = 30
targetSdk = 35
versionCode = 33
versionName = "2.2"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,13 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.rosan.installer.data.app.model.entity.AppEntity
import com.rosan.installer.data.app.util.InstalledAppInfo
import com.rosan.installer.data.app.util.sortedBest
import com.rosan.installer.data.installer.model.entity.ProgressEntity
import com.rosan.installer.data.installer.repo.InstallerRepo
import com.rosan.installer.data.settings.model.room.entity.ConfigEntity
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
Expand All @@ -37,6 +38,11 @@ class DialogViewModel(
private val _currentPackageName = MutableStateFlow<String?>(null)
val currentPackageName: StateFlow<String?> = _currentPackageName.asStateFlow()

private var fetchPreInstallInfoJob: Job? = null
private var autoInstallJob: Job? = null
private var collectRepoJob: Job? = null


fun dispatch(action: DialogViewAction) {
when (action) {
is DialogViewAction.CollectRepo -> collectRepo(action.repo)
Expand All @@ -45,128 +51,200 @@ class DialogViewModel(
is DialogViewAction.InstallChoice -> installChoice()
is DialogViewAction.InstallPrepare -> installPrepare()
is DialogViewAction.Install -> {
viewModelScope.launch {
fetchAndStorePreInstallInfoSuspend()
install()
}
install()
}
is DialogViewAction.Background -> background()
}
}

private var collectRepoJob: Job? = null

private fun collectRepo(repo: InstallerRepo) {
this.repo = repo
_preInstallAppInfo.value = null
_currentPackageName.value = null
collectRepoJob?.cancel()
autoInstallJob?.cancel()
fetchPreInstallInfoJob?.cancel()

collectRepoJob = viewModelScope.launch {
repo.progress.collect { progress ->
val previousState = state
var newState = when (progress) {
is ProgressEntity.Ready -> DialogViewState.Ready
is ProgressEntity.Resolving -> DialogViewState.Resolving
is ProgressEntity.ResolvedFailed -> DialogViewState.ResolveFailed
is ProgressEntity.Analysing -> DialogViewState.Analysing
is ProgressEntity.AnalysedFailed -> DialogViewState.AnalyseFailed
var newState = state
var newPackageNameFromProgress: String? = _currentPackageName.value

when (progress) {
is ProgressEntity.Ready -> {
newState = DialogViewState.Ready
newPackageNameFromProgress = null
}
is ProgressEntity.Resolving -> newState = DialogViewState.Resolving
is ProgressEntity.ResolvedFailed -> newState = DialogViewState.ResolveFailed
is ProgressEntity.Analysing -> newState = DialogViewState.Analysing
is ProgressEntity.AnalysedFailed -> newState = DialogViewState.AnalyseFailed
is ProgressEntity.AnalysedSuccess -> {
if (repo.entities.filter { it.selected }
.groupBy { it.app.packageName }.size != 1) {
DialogViewState.InstallChoice
val selectedEntities = repo.entities.filter { it.selected }
val mappedApps = selectedEntities.map { it.app }
val uniquePackages = mappedApps.groupBy { appEntity: AppEntity -> appEntity.packageName }

if (uniquePackages.size != 1) {
newState = DialogViewState.InstallChoice
newPackageNameFromProgress = null
} else {
DialogViewState.InstallPrepare
newState = DialogViewState.InstallPrepare
newPackageNameFromProgress = selectedEntities.first().app.packageName
}
}
is ProgressEntity.Installing -> DialogViewState.Installing
is ProgressEntity.InstallFailed -> DialogViewState.InstallFailed
is ProgressEntity.InstallSuccess -> DialogViewState.InstallSuccess
else -> DialogViewState.Ready
is ProgressEntity.Installing -> {
newState = DialogViewState.Installing
autoInstallJob?.cancel()
if (newPackageNameFromProgress == null && repo.entities.isNotEmpty()) {
val selectedEntities = repo.entities.filter { it.selected }
val mappedApps = selectedEntities.map { it.app }
val uniquePackages = mappedApps.groupBy { appEntity: AppEntity -> appEntity.packageName }
if (uniquePackages.size == 1) {
newPackageNameFromProgress = selectedEntities.first().app.packageName
}
}
}
is ProgressEntity.InstallFailed -> {
newState = DialogViewState.InstallFailed
autoInstallJob?.cancel()
}
is ProgressEntity.InstallSuccess -> {
newState = DialogViewState.InstallSuccess
autoInstallJob?.cancel()
}
else -> newState = DialogViewState.Ready
}

if (newPackageNameFromProgress != null) {
if (_currentPackageName.value != newPackageNameFromProgress) {
_currentPackageName.value = newPackageNameFromProgress
_preInstallAppInfo.value = null
fetchPreInstallAppInfo(newPackageNameFromProgress)
} else if (_preInstallAppInfo.value == null) {
fetchPreInstallAppInfo(newPackageNameFromProgress)
}
} else {
if (_currentPackageName.value != null) _currentPackageName.value = null
if (_preInstallAppInfo.value != null) _preInstallAppInfo.value = null
}

if (newState is DialogViewState.Installing &&
previousState !is DialogViewState.InstallPrepare &&
previousState !is DialogViewState.Installing &&
_preInstallAppInfo.value == null) {
launch { fetchAndStorePreInstallInfoSuspend() }
if (newState !is DialogViewState.InstallPrepare && autoInstallJob?.isActive == true) {
autoInstallJob?.cancel()
}

if (newState is DialogViewState.InstallPrepare && previousState !is DialogViewState.InstallPrepare) {
if (_currentPackageName.value != null && (_preInstallAppInfo.value == null || _preInstallAppInfo.value?.packageName != _currentPackageName.value)) {
fetchPreInstallAppInfo(_currentPackageName.value!!)
}

if (repo.config.installMode == ConfigEntity.InstallMode.AutoDialog) {
dispatch(DialogViewAction.Install)
} else {
autoInstallJob?.cancel()
autoInstallJob = viewModelScope.launch {
delay(500)
if (state is DialogViewState.InstallPrepare && repo.config.installMode == ConfigEntity.InstallMode.AutoDialog) {
install()
}
}
}
}


if (newState != previousState) {
if (state != newState) {
state = newState
}
if (newState != state) {
state = newState
}
}
}
}

private suspend fun fetchAndStorePreInstallInfoSuspend() {
val entitiesToInstall = repo.entities.filter { it.selected }.map { it.app }.sortedBest()
val uniquePackages = entitiesToInstall.groupBy { it.packageName }

if (entitiesToInstall.isNotEmpty() && uniquePackages.size == 1) {
val entity = entitiesToInstall.first()
val packageName = entity.packageName
_currentPackageName.value = packageName
try {
val info = withContext(Dispatchers.IO) {
InstalledAppInfo.buildByPackageName(packageName)
}
_preInstallAppInfo.value = info
} catch (e: Exception) {
_currentPackageName.value = null
_preInstallAppInfo.value = null
}
} else {
_currentPackageName.value = null
private fun fetchPreInstallAppInfo(packageName: String) {
if (packageName.isBlank()) {
_preInstallAppInfo.value = null
return
}
if (fetchPreInstallInfoJob?.isActive == true && _currentPackageName.value == packageName) {
return
}
if (_preInstallAppInfo.value != null && _preInstallAppInfo.value?.packageName == packageName) {
return
}
}

fetchPreInstallInfoJob?.cancel()
fetchPreInstallInfoJob = viewModelScope.launch {
val packageNameAtFetchStart = packageName
val info = try {
withContext(Dispatchers.IO) {
InstalledAppInfo.buildByPackageName(packageNameAtFetchStart)
}
} catch (e: Exception) {
null
}

private fun toast(message: String) {
Toast.makeText(context, message, Toast.LENGTH_LONG).show()
// --- 关键修改:增加状态检查 ---
// 只在状态不是最终状态(成功/失败)时更新 preInstallAppInfo
// 并且包名仍然匹配
if (state !is DialogViewState.InstallSuccess &&
state !is DialogViewState.InstallFailed &&
_currentPackageName.value == packageNameAtFetchStart) {
_preInstallAppInfo.value = info
}
// --- 修改结束 ---
}
}

private fun toast(@StringRes resId: Int) {
Toast.makeText(context, resId, Toast.LENGTH_LONG).show()
}
private fun toast(message: String) { Toast.makeText(context, message, Toast.LENGTH_LONG).show() }
private fun toast(@StringRes resId: Int) { Toast.makeText(context, resId, Toast.LENGTH_LONG).show() }

private fun close() {
autoInstallJob?.cancel()
fetchPreInstallInfoJob?.cancel()
collectRepoJob?.cancel()
_preInstallAppInfo.value = null
_currentPackageName.value = null
repo.close()
state = DialogViewState.Ready
}

private fun analyse() {
repo.analyse()
}
private fun analyse() { repo.analyse() }

private fun installChoice() {
_preInstallAppInfo.value = null
_currentPackageName.value = null
autoInstallJob?.cancel()
fetchPreInstallInfoJob?.cancel()
if (_currentPackageName.value != null) _currentPackageName.value = null
if (_preInstallAppInfo.value != null) _preInstallAppInfo.value = null
state = DialogViewState.InstallChoice
}

private fun installPrepare() {
if(state !is DialogViewState.InstallPrepare) {
val targetStateIsPrepare = state is DialogViewState.InstallPrepare
val selectedEntities = repo.entities.filter { it.selected }
val mappedApps = selectedEntities.map { it.app }
val uniquePackages = mappedApps.groupBy { appEntity: AppEntity -> appEntity.packageName }
var targetPackageName: String? = null
if (uniquePackages.size == 1) {
targetPackageName = selectedEntities.first().app.packageName
}
if (targetPackageName != null) {
if (_currentPackageName.value != targetPackageName) {
_currentPackageName.value = targetPackageName
_preInstallAppInfo.value = null
}
// Fetch info if needed when entering or already in prepare state
if ((targetStateIsPrepare && _preInstallAppInfo.value == null) || !targetStateIsPrepare) {
fetchPreInstallAppInfo(targetPackageName)
}
} else {
if (_currentPackageName.value != null) _currentPackageName.value = null
if (_preInstallAppInfo.value != null) _preInstallAppInfo.value = null
}
if (!targetStateIsPrepare) {
state = DialogViewState.InstallPrepare
}
}

private fun install() {
autoInstallJob?.cancel()
repo.install()
}

private fun background() {
repo.background(true)
}
private fun background() { repo.background(true) }
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
package com.rosan.installer.ui.page.installer.dialog.inner

// 导入需要的库
// 可能需要导入 InstalledAppInfo
import android.content.Intent
import android.net.Uri
import android.provider.Settings
Expand All @@ -12,55 +10,51 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import com.rosan.installer.R
import com.rosan.installer.data.installer.repo.InstallerRepo
import com.rosan.installer.ui.page.installer.dialog.DialogInnerParams
import com.rosan.installer.ui.page.installer.dialog.DialogParams
import com.rosan.installer.ui.page.installer.dialog.DialogParamsType
import com.rosan.installer.ui.page.installer.dialog.DialogViewAction
import com.rosan.installer.ui.page.installer.dialog.DialogViewModel
import com.rosan.installer.ui.page.installer.dialog.*

// Assume errorText is accessible

@Composable
fun installFailedDialog(
fun installFailedDialog( // 小写开头
installer: InstallerRepo, viewModel: DialogViewModel
): DialogParams {
val context = LocalContext.current

// --- 开始: 从 ViewModel 收集状态 ---
val preInstallAppInfo by viewModel.preInstallAppInfo.collectAsState()
val currentPackageName by viewModel.currentPackageName.collectAsState()
val packageName = currentPackageName ?: installer.entities.filter { it.selected }.map { it.app }
.firstOrNull()?.packageName ?: ""
// --- 结束: 从 ViewModel 收集状态 ---

// --- 调用 InstallInfoDialog 时传入 preInstallAppInfo ---
return installInfoDialog(
// Call InstallInfoDialog for base structure
val baseParams = InstallInfoDialog(
installer = installer,
viewModel = viewModel,
preInstallAppInfo = preInstallAppInfo, // <-- 传递从 ViewModel 获取的值
onTitleExtraClick = { // 保持标题点击逻辑
if (packageName.isNotEmpty()) { // 安全检查
preInstallAppInfo = preInstallAppInfo,
onTitleExtraClick = {
if (packageName.isNotEmpty()) {
context.startActivity(
Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
.setData(Uri.fromParts("package", packageName, null))
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
)
}
// 失败时点击标题附加按钮后是否需要特殊操作?
// viewModel.dispatch(DialogViewAction.Background)
}
).copy( // 使用 copy 添加错误文本并替换按钮
text = DialogInnerParams( // 添加错误信息显示
DialogParamsType.InstallerInstallFailed.id, // 使用特定 ID
errorText(installer, viewModel) // 调用公共的错误显示 Composable
)

// Override text and buttons
return baseParams.copy(
text = DialogInnerParams(
DialogParamsType.InstallerInstallFailed.id,
errorText(installer, viewModel) // Assume errorText is accessible
),
buttons = DialogButtons( // 替换按钮
DialogParamsType.InstallerInstallFailed.id // 使用特定 ID
buttons = DialogButtons(
DialogParamsType.InstallerInstallFailed.id
) {
// 失败时通常提供 "重试/上一步" 和 "取消"
listOf(
DialogButton(stringResource(R.string.previous)) { // "上一步" 通常返回准备阶段
DialogButton(stringResource(R.string.previous)) {
viewModel.dispatch(DialogViewAction.InstallPrepare)
},
DialogButton(stringResource(R.string.cancel)) { // "取消" 关闭对话框
DialogButton(stringResource(R.string.cancel)) {
viewModel.dispatch(DialogViewAction.Close)
}
)
Expand Down
Loading