Skip to content

Commit 4be4fee

Browse files
committed
Add missing translations and update related UI components
1 parent 1078576 commit 4be4fee

25 files changed

Lines changed: 912 additions & 30 deletions

File tree

app/src/main/java/app/pwhs/universalinstaller/presentation/install/ApkInfoContent.kt

Lines changed: 41 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -343,6 +343,9 @@ internal fun ApkInfoContent(
343343
if (isExpanded) {
344344
HorizontalDivider(thickness = 0.5.dp, color = MaterialTheme.colorScheme.outlineVariant)
345345
}
346+
val isScanCompleted = apkInfo.vtResult?.status in setOf(VtStatus.CLEAN, VtStatus.MALICIOUS, VtStatus.SUSPICIOUS)
347+
val isScanning = apkInfo.vtResult?.status in setOf(VtStatus.SCANNING, VtStatus.UPLOADING, VtStatus.QUEUED, VtStatus.ANALYZING)
348+
346349
Column(
347350
modifier = Modifier
348351
.fillMaxWidth()
@@ -369,17 +372,46 @@ internal fun ApkInfoContent(
369372
) {
370373
Text(cancelText ?: if (isExpanded && startCompact) stringResource(R.string.dialog_back_btn) else stringResource(R.string.cancel))
371374
}
372-
Button(
375+
376+
if (isScanCompleted) {
377+
Button(
378+
onClick = onInstall,
379+
modifier = Modifier.weight(1f),
380+
shape = MaterialTheme.shapes.medium,
381+
colors = if (isDowngrade) ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.error) else ButtonDefaults.buttonColors()
382+
) {
383+
if (confirmText == null) {
384+
Icon(Icons.Rounded.InstallMobile, null, modifier = Modifier.size(18.dp))
385+
Spacer(Modifier.width(8.dp))
386+
}
387+
Text(confirmText ?: if (isDowngrade) stringResource(R.string.dialog_downgrade_btn) else stringResource(R.string.txt_install))
388+
}
389+
} else {
390+
Button(
391+
onClick = onCheckVirusTotal,
392+
modifier = Modifier.weight(1f),
393+
shape = MaterialTheme.shapes.medium,
394+
enabled = !isScanning
395+
) {
396+
if (isScanning) {
397+
CircularProgressIndicator(modifier = Modifier.size(18.dp), color = MaterialTheme.colorScheme.onPrimary, strokeWidth = 2.dp)
398+
Spacer(Modifier.width(8.dp))
399+
Text(stringResource(R.string.scanning_progress))
400+
} else {
401+
Icon(Icons.Rounded.Security, null, modifier = Modifier.size(18.dp))
402+
Spacer(Modifier.width(8.dp))
403+
Text(stringResource(R.string.scan_virustotal_btn))
404+
}
405+
}
406+
}
407+
}
408+
409+
if (!isScanCompleted) {
410+
TextButton(
373411
onClick = onInstall,
374-
modifier = Modifier.weight(1f),
375-
shape = MaterialTheme.shapes.medium,
376-
colors = if (isDowngrade) ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.error) else ButtonDefaults.buttonColors()
412+
modifier = Modifier.fillMaxWidth()
377413
) {
378-
if (confirmText == null) {
379-
Icon(Icons.Rounded.InstallMobile, null, modifier = Modifier.size(18.dp))
380-
Spacer(Modifier.width(8.dp))
381-
}
382-
Text(confirmText ?: if (isDowngrade) stringResource(R.string.dialog_downgrade_btn) else stringResource(R.string.txt_install))
414+
Text(stringResource(R.string.skip_and_install_btn))
383415
}
384416
}
385417
}
@@ -533,11 +565,6 @@ private fun VirusTotalCard(vt: VtResult?, fileSizeBytes: Long, sha256: String =
533565
Spacer(Modifier.height(12.dp))
534566
VtBreakdownSection(vt = vt, warningColor = extendedColors.warning)
535567
}
536-
TextButton(onClick = onCheck, modifier = Modifier.fillMaxWidth()) {
537-
Icon(Icons.Rounded.CloudUpload, null, modifier = Modifier.size(16.dp))
538-
Spacer(Modifier.width(6.dp))
539-
Text(if (status == null) stringResource(R.string.apk_info_vt_check_button) else stringResource(R.string.apk_info_vt_rescan_button))
540-
}
541568
}
542569
}
543570
}

app/src/main/java/app/pwhs/universalinstaller/presentation/install/DialogInstallActivity.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,7 @@ class DialogInstallActivity : ComponentActivity() {
217217
val prefs by context.dataStore.data.collectAsState(initial = null)
218218
val autoOpenAfterInstall = prefs?.get(PreferencesKeys.AUTO_OPEN_AFTER_INSTALL) ?: false
219219
val autoConfirmExternalInstall = prefs?.get(PreferencesKeys.AUTO_CONFIRM_EXTERNAL_INSTALL) ?: false
220+
val strictVirusTotalCheck = prefs?.get(PreferencesKeys.STRICT_VIRUSTOTAL_CHECK) ?: false
220221

221222
// Tracks whether we've actually observed the captured session in the repository.
222223
// The session is added inside controller.install() AFTER createSession() suspends,
@@ -259,7 +260,7 @@ class DialogInstallActivity : ComponentActivity() {
259260
}
260261
val handleInstallTap = {
261262
val info = uiState.pendingApkInfo
262-
val risks = if (info != null) detectInstallRisks(info) else emptyList()
263+
val risks = if (info != null) detectInstallRisks(info, strictVirusTotalCheck) else emptyList()
263264
if (risks.isNotEmpty()) {
264265
pendingRisks = risks
265266
} else {

app/src/main/java/app/pwhs/universalinstaller/presentation/install/InstallScreen.kt

Lines changed: 35 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -91,24 +91,31 @@ fun InstallScreen(
9191
}
9292
}.collectAsState(initial = true)
9393

94+
val strictVirusTotalCheck by remember(context) {
95+
context.dataStore.data.map {
96+
it[PreferencesKeys.STRICT_VIRUSTOTAL_CHECK] ?: false
97+
}
98+
}.collectAsState(initial = false)
99+
94100
// Risk consent gate — populated when the user taps Install on a downgrade or
95101
// a VirusTotal-flagged APK. We render an AlertDialog over the rest of the screen
96102
// and only proceed to the biometric gate + confirmInstall after they acknowledge.
97103
var pendingRisks by remember { mutableStateOf<List<app.pwhs.universalinstaller.presentation.install.dialog.InstallRisk>>(emptyList()) }
104+
var pendingAction by remember { mutableStateOf<(() -> Unit)?>(null) }
98105

99-
val proceedWithBiometric: () -> Unit = {
106+
val startBiometricFlow: (onSuccess: () -> Unit) -> Unit = { onSuccess ->
100107
val activity = context as? androidx.fragment.app.FragmentActivity
101108
if (activity != null) {
102109
BiometricGate.authenticate(
103110
activity = activity,
104111
enabled = installGateEnabled,
105112
title = resource.getString(R.string.biometric_install_title),
106113
subtitle = resource.getString(R.string.biometric_install_sub),
107-
onSuccess = viewModel::confirmInstall,
114+
onSuccess = onSuccess,
108115
onCancel = viewModel::dismissPendingInstall,
109116
)
110117
} else {
111-
viewModel.confirmInstall()
118+
onSuccess()
112119
}
113120
}
114121

@@ -117,10 +124,15 @@ fun InstallScreen(
117124
risks = pendingRisks,
118125
onConfirm = {
119126
pendingRisks = emptyList()
120-
proceedWithBiometric()
127+
val action = pendingAction
128+
pendingAction = null
129+
if (action != null) {
130+
startBiometricFlow(action)
131+
}
121132
},
122133
onCancel = {
123134
pendingRisks = emptyList()
135+
pendingAction = null
124136
viewModel.dismissPendingInstall()
125137
},
126138
)
@@ -143,12 +155,13 @@ fun InstallScreen(
143155
// rather than a surprise after fingerprint auth completes.
144156
val info = uiState.pendingApkInfo
145157
val risks = if (info != null) {
146-
app.pwhs.universalinstaller.presentation.install.dialog.detectInstallRisks(info)
158+
app.pwhs.universalinstaller.presentation.install.dialog.detectInstallRisks(info, strictVirusTotalCheck)
147159
} else emptyList()
148160
if (risks.isNotEmpty()) {
149161
pendingRisks = risks
162+
pendingAction = viewModel::confirmInstall
150163
} else {
151-
proceedWithBiometric()
164+
startBiometricFlow(viewModel::confirmInstall)
152165
}
153166
},
154167
onDismissPreview = viewModel::dismissPendingInstall,
@@ -172,7 +185,22 @@ fun InstallScreen(
172185
onBatchPicked = { uris -> viewModel.parseBatch(context, uris) },
173186
onBatchToggleEntry = viewModel::toggleBatchSelection,
174187
onBatchToggleAll = viewModel::setBatchAllSelected,
175-
onBatchConfirm = viewModel::confirmBatchInstall,
188+
onBatchConfirm = {
189+
val ready = uiState.batchState as? BatchInstallState.Ready
190+
if (ready != null) {
191+
val picked = ready.entries.filter { it.selected && it.splitUris.isNotEmpty() }
192+
val risks = picked.flatMap {
193+
app.pwhs.universalinstaller.presentation.install.dialog.detectInstallRisks(it.apkInfo, strictVirusTotalCheck)
194+
}.distinct()
195+
196+
if (risks.isNotEmpty()) {
197+
pendingRisks = risks
198+
pendingAction = viewModel::confirmBatchInstall
199+
} else {
200+
viewModel.confirmBatchInstall()
201+
}
202+
}
203+
},
176204
onBatchDismiss = viewModel::dismissBatchInstall,
177205
onSkipBatchParse = viewModel::skipBatchParseAndInstall,
178206
onSkipParseSingle = viewModel::skipParseAndInstallSingle,

app/src/main/java/app/pwhs/universalinstaller/presentation/install/dialog/RiskConfirmDialog.kt

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,9 +45,12 @@ sealed interface InstallRisk {
4545

4646
/** VirusTotal flagged the APK as suspicious. */
4747
data class VtSuspicious(val engineCount: Int) : InstallRisk
48+
49+
/** Strict mode: APK was not scanned by VirusTotal. */
50+
data object VtUnscanned : InstallRisk
4851
}
4952

50-
fun detectInstallRisks(apkInfo: ApkInfo): List<InstallRisk> {
53+
fun detectInstallRisks(apkInfo: ApkInfo, strictVirusTotal: Boolean = false): List<InstallRisk> {
5154
val risks = mutableListOf<InstallRisk>()
5255
val installedCode = apkInfo.installedVersionCode
5356
if (installedCode != null && installedCode > 0 && apkInfo.versionCode < installedCode) {
@@ -56,10 +59,12 @@ fun detectInstallRisks(apkInfo: ApkInfo): List<InstallRisk> {
5659
newVersionName = apkInfo.versionName.ifBlank { "?" },
5760
)
5861
}
59-
when (apkInfo.vtResult?.status) {
62+
when (val status = apkInfo.vtResult?.status) {
6063
VtStatus.MALICIOUS -> risks += InstallRisk.VtMalicious(apkInfo.vtResult.malicious)
6164
VtStatus.SUSPICIOUS -> risks += InstallRisk.VtSuspicious(apkInfo.vtResult.suspicious)
62-
else -> Unit
65+
VtStatus.CLEAN -> Unit
66+
null -> if (strictVirusTotal) risks += InstallRisk.VtUnscanned
67+
else -> if (strictVirusTotal) risks += InstallRisk.VtUnscanned
6368
}
6469
return risks
6570
}
@@ -128,6 +133,8 @@ private fun RiskRow(risk: InstallRisk) {
128133
stringResource(R.string.dialog_risk_vt_malicious, risk.engineCount)
129134
is InstallRisk.VtSuspicious -> Icons.Rounded.Warning to
130135
stringResource(R.string.dialog_risk_vt_suspicious, risk.engineCount)
136+
is InstallRisk.VtUnscanned -> Icons.Rounded.Warning to
137+
stringResource(R.string.dialog_risk_vt_unscanned)
131138
}
132139
Row(verticalAlignment = Alignment.Top, modifier = Modifier.fillMaxWidth()) {
133140
Icon(

app/src/main/java/app/pwhs/universalinstaller/presentation/setting/SettingScreen.kt

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@ fun SettingScreen(
9696
uiState = uiState,
9797
onInstallModeChanged = viewModel::setInstallMode,
9898
onVirusTotalKeyChanged = viewModel::setVirusTotalApiKey,
99+
onStrictVirusTotalCheckChanged = viewModel::setStrictVirusTotalCheck,
99100
onShizukuOptionChanged = viewModel::setShizukuOption,
100101
onShizukuInstallerChanged = viewModel::setShizukuInstallerPackageName,
101102
onDeleteApkChanged = viewModel::setDeleteApkAfterInstall,
@@ -127,6 +128,7 @@ private fun SettingUi(
127128
uiState: SettingUiState = SettingUiState(),
128129
onInstallModeChanged: (InstallMode) -> Unit = {},
129130
onVirusTotalKeyChanged: (String) -> Unit = {},
131+
onStrictVirusTotalCheckChanged: (Boolean) -> Unit = {},
130132
onShizukuOptionChanged: (Preferences.Key<Boolean>, Boolean) -> Unit = { _, _ -> },
131133
onShizukuInstallerChanged: (String) -> Unit = {},
132134
onDeleteApkChanged: (Boolean) -> Unit = {},
@@ -653,6 +655,12 @@ private fun SettingUi(
653655
placeholder = { Text(stringResource(R.string.setting_vt_api_key_placeholder)) },
654656
singleLine = true,
655657
)
658+
SwitchPreference(
659+
title = stringResource(R.string.setting_vt_strict_check_title),
660+
subtitle = stringResource(R.string.setting_vt_strict_check_summary),
661+
checked = uiState.strictVirusTotalCheck,
662+
onCheckedChange = onStrictVirusTotalCheckChanged
663+
)
656664
}
657665
}
658666

app/src/main/java/app/pwhs/universalinstaller/presentation/setting/SettingViewModel.kt

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ object PreferencesKeys {
5656
val USE_ROOT = booleanPreferencesKey("use_root")
5757
val INSTALL_USER_ID = intPreferencesKey("install_user_id")
5858
val VIRUSTOTAL_API_KEY = stringPreferencesKey("virustotal_api_key")
59+
val STRICT_VIRUSTOTAL_CHECK = booleanPreferencesKey("strict_virustotal_check")
5960
val DELETE_APK_AFTER_INSTALL = booleanPreferencesKey("delete_apk_after_install")
6061

6162
/** Open the app automatically after a successful install (with a 3-second cancellable countdown). */
@@ -207,6 +208,7 @@ data class SettingUiState(
207208
val useShizuku: Boolean = false,
208209
val useRoot: Boolean = false,
209210
val virusTotalApiKey: String = "",
211+
val strictVirusTotalCheck: Boolean = false,
210212
val deleteApkAfterInstall: Boolean = false,
211213
val autoOpenAfterInstall: Boolean = false,
212214
val shizukuState: ShizukuState = ShizukuState.NOT_INSTALLED,
@@ -425,6 +427,12 @@ class SettingViewModel(
425427
}
426428
}
427429

430+
fun setStrictVirusTotalCheck(enabled: Boolean) {
431+
viewModelScope.launch {
432+
dataStore.edit { prefs -> prefs[PreferencesKeys.STRICT_VIRUSTOTAL_CHECK] = enabled }
433+
}
434+
}
435+
428436
fun setDeleteApkAfterInstall(enabled: Boolean) {
429437
viewModelScope.launch {
430438
dataStore.edit { prefs -> prefs[PreferencesKeys.DELETE_APK_AFTER_INSTALL] = enabled }
@@ -731,12 +739,13 @@ class SettingViewModel(
731739
(prefs[PreferencesKeys.BIOMETRIC_LOCK_UNINSTALL] ?: false)
732740
},
733741
dataStore.data.map { prefs ->
734-
// Flags: dialog-mode, auto-open, auto-confirm, and show-download-tab
742+
// Flags: dialog-mode, auto-open, auto-confirm, and show-download-tab, strict-vt
735743
listOf(
736744
prefs[PreferencesKeys.DIALOG_INSTALL_MODE] ?: true,
737745
prefs[PreferencesKeys.AUTO_OPEN_AFTER_INSTALL] ?: false,
738746
prefs[PreferencesKeys.AUTO_CONFIRM_EXTERNAL_INSTALL] ?: false,
739-
prefs[PreferencesKeys.SHOW_DOWNLOAD_TAB] ?: true
747+
prefs[PreferencesKeys.SHOW_DOWNLOAD_TAB] ?: true,
748+
prefs[PreferencesKeys.STRICT_VIRUSTOTAL_CHECK] ?: false
740749
)
741750
},
742751
dataStore.data.map { prefs ->
@@ -769,6 +778,7 @@ class SettingViewModel(
769778
val autoOpen = interfaceFlags[1]
770779
val autoConfirm = interfaceFlags[2]
771780
val showDownload = interfaceFlags[3]
781+
val strictVirusTotal = interfaceFlags[4]
772782
@Suppress("UNCHECKED_CAST")
773783
val extractorAndProfiles = flows[12] as List<String>
774784
val extractorPath = extractorAndProfiles[0]
@@ -797,6 +807,7 @@ class SettingViewModel(
797807
useShizuku = useShizuku,
798808
useRoot = useRoot && (rootState == RootState.READY || rootState == RootState.UNKNOWN),
799809
virusTotalApiKey = vtKey,
810+
strictVirusTotalCheck = strictVirusTotal,
800811
deleteApkAfterInstall = deleteApk,
801812
autoOpenAfterInstall = autoOpen,
802813
shizukuState = shizukuState,

app/src/main/res/values-ar/strings.xml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -709,4 +709,10 @@
709709
<string name="manage_action_installer_source_sub">تحميل جانبي / مصدر غير معروف</string>
710710
<string name="installer_engine_root_request">اضغط لمنح صلاحيات الروت</string>
711711
<string name="dialog_failed_fallback_install">التثبيت عبر مثبت النظام</string>
712+
<string name="dialog_risk_vt_unscanned">فحص VirusTotal لم يكتمل</string>
713+
<string name="scan_virustotal_btn">فحص باستخدام VirusTotal</string>
714+
<string name="skip_and_install_btn">تخطي وتثبيت</string>
715+
<string name="scanning_progress">جارٍ الفحص…</string>
716+
<string name="setting_vt_strict_check_title">فحص VirusTotal صارم</string>
717+
<string name="setting_vt_strict_check_summary">التحذير قبل التثبيت إذا لم يتم فحص الملف</string>
712718
</resources>

app/src/main/res/values-de/strings.xml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -709,4 +709,10 @@
709709
<string name="manage_action_installer_source_sub">Sideloaded / Unbekannte Quelle</string>
710710
<string name="installer_engine_root_request">Tippen, um Root-Zugriff zu gewähren</string>
711711
<string name="dialog_failed_fallback_install">Über System-Installationsprogramm installieren</string>
712+
<string name="dialog_risk_vt_unscanned">VirusTotal-Scan nicht abgeschlossen</string>
713+
<string name="scan_virustotal_btn">Mit VirusTotal scannen</string>
714+
<string name="skip_and_install_btn">Überspringen &amp; Installieren</string>
715+
<string name="scanning_progress">Wird gescannt…</string>
716+
<string name="setting_vt_strict_check_title">Strenger VirusTotal-Check</string>
717+
<string name="setting_vt_strict_check_summary">Vor der Installation warnen, wenn die Datei nicht gescannt wurde</string>
712718
</resources>

app/src/main/res/values-el/strings.xml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -709,4 +709,10 @@
709709
<string name="manage_action_installer_source_sub">Sideloaded / Άγνωστη πηγή</string>
710710
<string name="installer_engine_root_request">Πατήστε για εκχώρηση πρόσβασης root</string>
711711
<string name="dialog_failed_fallback_install">Εγκατάσταση μέσω προγράμματος εγκατάστασης συστήματος</string>
712+
<string name="dialog_risk_vt_unscanned">Η σάρωση VirusTotal δεν ολοκληρώθηκε</string>
713+
<string name="scan_virustotal_btn">Σάρωση VirusTotal</string>
714+
<string name="skip_and_install_btn">Παράλειψη και Εγκατάσταση</string>
715+
<string name="scanning_progress">Γίνεται σάρωση…</string>
716+
<string name="setting_vt_strict_check_title">Αυστηρός έλεγχος VirusTotal</string>
717+
<string name="setting_vt_strict_check_summary">Προειδοποίηση πριν την εγκατάσταση εάν το αρχείο δεν έχει σαρωθεί</string>
712718
</resources>

app/src/main/res/values-es/strings.xml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -709,4 +709,10 @@
709709
<string name="manage_action_installer_source_sub">Carga lateral / Fuente desconocida</string>
710710
<string name="installer_engine_root_request">Toca para conceder acceso root</string>
711711
<string name="dialog_failed_fallback_install">Instalar mediante el instalador del sistema</string>
712+
<string name="dialog_risk_vt_unscanned">Análisis de VirusTotal no completado</string>
713+
<string name="scan_virustotal_btn">Analizar con VirusTotal</string>
714+
<string name="skip_and_install_btn">Omitir e instalar</string>
715+
<string name="scanning_progress">Analizando…</string>
716+
<string name="setting_vt_strict_check_title">Comprobación estricta de VirusTotal</string>
717+
<string name="setting_vt_strict_check_summary">Avisar antes de instalar si el archivo no ha sido analizado</string>
712718
</resources>

0 commit comments

Comments
 (0)