Skip to content

Commit 18ba2c5

Browse files
nqmgamingclaude
andcommitted
feat(settings): give the install UI its own screen
Settings > Installation had grown to ten controls in four idioms — segmented buttons, install mode, switches, radios, thumbnails — in one scrolling run, and two of them contradicted each other in plain sight. The appearance questions now live on their own screen, reached by one row, the same way the theme settings already work. What moved: how you are asked when another app opens an APK, where the install card appears, auto-confirm, and the Download tab toggle. What stayed: install mode, Dhizuku, delete-after-install, open-after-install, default installer — engine and behaviour, not appearance. Two things the split fixes rather than relocates: "Don't ask, just install" and the auto-confirm switch used to sit next to each other saying the same sentence, with no way to tell from the screen which won. Auto-confirm is now nested under "Ask on screen", where it means something the mode does not: the card still appears, it just does not wait for you. Under the other two modes it is gone. The card position had the same problem in reverse — it only applies when a card is drawn, but stayed lit and tappable under the notification modes. It is inside the same nested group now. The screen opens with a phone frame showing what the current choice actually produces: the card centered, the card at the bottom, or a notification. That is the part the old section could not do at any length of prose. Trade-off: Settings' search no longer finds the individual options, since they are on another screen. The entry row carries their keywords so the way in is still findable; making search reach across screens is a bigger change. Verified on the emulator: section slimmed, screen opens, switching to "Ask in a notification" swaps the preview and hides the card group, and the choice persists to DataStore. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent 133d60a commit 18ba2c5

5 files changed

Lines changed: 530 additions & 200 deletions

File tree

app/src/main/AndroidManifest.xml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -268,6 +268,11 @@
268268
android:exported="false"
269269
android:launchMode="singleTop" />
270270

271+
<activity
272+
android:name=".presentation.setting.installui.InstallUiActivity"
273+
android:exported="false"
274+
android:launchMode="singleTop" />
275+
271276
<!-- Answers the Cancel action on an install-prompt notification without opening a
272277
window. Reachable only through our own PendingIntents. -->
273278
<receiver

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

Lines changed: 24 additions & 200 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ import androidx.compose.material.icons.rounded.Search
4444
import androidx.compose.material.icons.rounded.SearchOff
4545
import androidx.compose.material.icons.rounded.SettingsApplications
4646
import androidx.compose.material.icons.rounded.Terminal
47+
import androidx.compose.material.icons.rounded.Wallpaper
4748
import androidx.compose.material.icons.rounded.WifiTethering
4849
import androidx.compose.material3.ExperimentalMaterial3Api
4950
import androidx.compose.material3.HorizontalDivider
@@ -417,46 +418,32 @@ private fun SettingUi(
417418
)
418419
}
419420

420-
// Auto-confirm moved up next to the external-open modes: it only applies
421-
// to APKs opened from another app, so it belongs to that question rather
422-
// than sitting among the after-install toggles it used to follow.
423-
if (q.isBlank()) OptionGroupHeader(stringResource(R.string.setting_external_open_title))
424-
SearchableItem(q, stringResource(R.string.setting_external_open_title), stringResource(R.string.setting_external_open_notification_sub)) {
425-
ExternalOpenModeSelector(
426-
current = externalOpenMode,
427-
onChange = onExternalOpenModeChanged,
428-
)
429-
}
430-
SearchableItem(q, stringResource(R.string.setting_auto_confirm_title), stringResource(R.string.setting_auto_confirm_subtitle)) {
431-
SwitchPreference(
432-
title = stringResource(R.string.setting_auto_confirm_title),
433-
subtitle = stringResource(R.string.setting_auto_confirm_subtitle),
434-
checked = uiState.autoConfirmExternalInstall,
435-
onCheckedChange = onAutoConfirmExternalInstallChanged,
436-
)
437-
}
438-
439-
if (q.isBlank()) OptionGroupHeader(stringResource(R.string.setting_install_ui_style_title))
440-
SearchableItem(q, stringResource(R.string.setting_install_ui_style_title), stringResource(R.string.setting_install_ui_style_sub)) {
441-
InstallUiStyleSelector(
442-
current = installUiStyle,
443-
onChange = onInstallUiStyleChanged,
444-
)
445-
}
446-
447-
// Divider only makes sense when the full (unfiltered) list shows.
448-
// Below it sit the two settings that belong to no group above — they
449-
// change what the rest of the system sees, not how a single install runs.
421+
// Everything about how the installer looks — the external-open modes, the
422+
// card position, auto-confirm, the Download tab — now lives on its own
423+
// screen. The keyword list keeps this row findable by what moved.
450424
if (q.isBlank()) {
451425
HorizontalDivider(modifier = Modifier.padding(horizontal = 16.dp), thickness = 0.5.dp)
452426
}
453-
454-
SearchableItem(q, stringResource(R.string.setting_show_download_tab_title), stringResource(R.string.setting_show_download_tab_subtitle)) {
455-
SwitchPreference(
456-
title = stringResource(R.string.setting_show_download_tab_title),
457-
subtitle = stringResource(R.string.setting_show_download_tab_subtitle),
458-
checked = uiState.showDownloadTab,
459-
onCheckedChange = onShowDownloadTabChanged,
427+
SearchableItem(
428+
q,
429+
stringResource(R.string.install_ui_screen_title),
430+
"dialog notification bottom sheet position card download tab appearance",
431+
) {
432+
ListItem(
433+
headlineContent = { Text(stringResource(R.string.install_ui_screen_title)) },
434+
supportingContent = { Text(stringResource(R.string.install_ui_entry_subtitle)) },
435+
leadingContent = {
436+
Icon(Icons.Rounded.Wallpaper, null, tint = MaterialTheme.colorScheme.primary)
437+
},
438+
modifier = Modifier.clickable {
439+
context.startActivity(
440+
android.content.Intent(
441+
context,
442+
app.pwhs.universalinstaller.presentation.setting.installui.InstallUiActivity::class.java,
443+
)
444+
)
445+
},
446+
colors = ListItemDefaults.colors(containerColor = Color.Transparent),
460447
)
461448
}
462449
SearchableItem(q, stringResource(R.string.setting_default_installer_title), stringResource(R.string.setting_default_installer_subtitle)) {
@@ -1097,170 +1084,7 @@ private fun SecurityLevelSelector(
10971084
}
10981085

10991086

1100-
/**
1101-
* Dialog / Notification / Notification-and-install, for packages opened by another app.
1102-
*
1103-
* A list of radio rows rather than the segmented control used elsewhere: each option needs a line
1104-
* of explanation, and "installs without asking" is not a choice to make from a two-word label.
1105-
*/
1106-
@Composable
1107-
private fun ExternalOpenModeSelector(
1108-
current: ExternalOpenMode,
1109-
onChange: (ExternalOpenMode) -> Unit,
1110-
) {
1111-
// No title of its own: the group header above already reads "When another app opens an APK",
1112-
// and repeating it here was the second of the two headings this control used to carry.
1113-
Column(modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp, vertical = 8.dp)) {
1114-
listOf(
1115-
Triple(ExternalOpenMode.Dialog, R.string.setting_external_open_dialog, R.string.setting_external_open_dialog_sub),
1116-
Triple(ExternalOpenMode.Notification, R.string.setting_external_open_notification, R.string.setting_external_open_notification_sub),
1117-
Triple(ExternalOpenMode.AutoNotification, R.string.setting_external_open_auto_notification, R.string.setting_external_open_auto_notification_sub),
1118-
).forEach { (mode, titleRes, subRes) ->
1119-
Row(
1120-
modifier = Modifier
1121-
.fillMaxWidth()
1122-
.clickable { if (mode != current) onChange(mode) }
1123-
.padding(vertical = 8.dp),
1124-
verticalAlignment = Alignment.Top,
1125-
) {
1126-
RadioButton(
1127-
selected = mode == current,
1128-
onClick = { if (mode != current) onChange(mode) },
1129-
)
1130-
Spacer(Modifier.width(8.dp))
1131-
Column(modifier = Modifier.weight(1f)) {
1132-
Text(stringResource(titleRes), style = MaterialTheme.typography.bodyLarge)
1133-
Text(
1134-
text = stringResource(subRes),
1135-
style = MaterialTheme.typography.bodySmall,
1136-
color = MaterialTheme.colorScheme.onSurfaceVariant,
1137-
)
1138-
}
1139-
}
1140-
}
1141-
}
1142-
}
11431087

11441088

1145-
/**
1146-
* Where the install card sits: centered, or anchored at the bottom.
1147-
*
1148-
* Two thumbnails rather than the segmented control this used to be. The setting decides what the
1149-
* install UI *looks like*, and "Dialog | Bottom sheet" asked the user to picture two Material
1150-
* component names — the only way to find out was to pick one and install something. A drawn
1151-
* mock-up answers the question at a glance, and drops the paragraph of prose that stood in for it.
1152-
*
1153-
* The card idiom (fixed-size tile, primary border and a check when selected) is the one
1154-
* [app.pwhs.universalinstaller.presentation.setting.theme.ThemeScreen] already uses for its theme
1155-
* presets, so the two visual pickers in Settings look like the same control.
1156-
*/
1157-
@Composable
1158-
private fun InstallUiStyleSelector(
1159-
current: InstallUiStyle,
1160-
onChange: (InstallUiStyle) -> Unit,
1161-
) {
1162-
Row(
1163-
modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp, vertical = 8.dp),
1164-
horizontalArrangement = Arrangement.spacedBy(12.dp),
1165-
) {
1166-
listOf(
1167-
InstallUiStyle.Dialog to R.string.setting_install_ui_style_dialog,
1168-
InstallUiStyle.Sheet to R.string.setting_install_ui_style_sheet,
1169-
).forEach { (style, labelRes) ->
1170-
InstallUiStyleCard(
1171-
style = style,
1172-
label = stringResource(labelRes),
1173-
selected = style == current,
1174-
onClick = { if (style != current) onChange(style) },
1175-
)
1176-
}
1177-
}
1178-
}
11791089

1180-
/**
1181-
* One thumbnail: a phone-shaped frame with a miniature install card drawn where the real one lands.
1182-
*/
1183-
@Composable
1184-
private fun InstallUiStyleCard(
1185-
style: InstallUiStyle,
1186-
label: String,
1187-
selected: Boolean,
1188-
onClick: () -> Unit,
1189-
) {
1190-
val accent = MaterialTheme.colorScheme.primary
1191-
Column(horizontalAlignment = Alignment.CenterHorizontally) {
1192-
Surface(
1193-
// selectable(), not clickable(): this is one of a set of mutually exclusive choices,
1194-
// and TalkBack should announce it as such rather than as a plain button.
1195-
modifier = Modifier
1196-
.size(width = 92.dp, height = 124.dp)
1197-
.selectable(selected = selected, role = Role.RadioButton, onClick = onClick),
1198-
shape = RoundedCornerShape(12.dp),
1199-
color = MaterialTheme.colorScheme.surfaceVariant,
1200-
border = BorderStroke(
1201-
width = if (selected) 2.dp else 1.dp,
1202-
color = if (selected) accent else MaterialTheme.colorScheme.outlineVariant,
1203-
),
1204-
) {
1205-
Box(
1206-
modifier = Modifier.fillMaxSize().padding(10.dp),
1207-
// The whole point of the thumbnail: the mini card sits where the real one will.
1208-
contentAlignment = if (style == InstallUiStyle.Sheet) {
1209-
Alignment.BottomCenter
1210-
} else {
1211-
Alignment.Center
1212-
},
1213-
) {
1214-
Column(
1215-
modifier = Modifier
1216-
// A sheet spans the width; a centered card is inset on both sides.
1217-
.fillMaxWidth(if (style == InstallUiStyle.Sheet) 1f else 0.82f)
1218-
.clip(RoundedCornerShape(6.dp))
1219-
.background(MaterialTheme.colorScheme.surface)
1220-
.padding(horizontal = 6.dp, vertical = 7.dp),
1221-
verticalArrangement = Arrangement.spacedBy(4.dp),
1222-
) {
1223-
MiniCardLine(0.7f, MaterialTheme.colorScheme.onSurface.copy(alpha = 0.55f))
1224-
MiniCardLine(1f, MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.35f))
1225-
Spacer(Modifier.height(1.dp))
1226-
// Stands in for the Install button, in the accent color so the mock-up reads
1227-
// as this app's card rather than a generic grey box.
1228-
MiniCardLine(0.45f, accent, height = 6.dp)
1229-
}
1230-
}
1231-
}
1232-
Spacer(Modifier.height(6.dp))
1233-
Row(verticalAlignment = Alignment.CenterVertically) {
1234-
if (selected) {
1235-
Icon(
1236-
imageVector = Icons.Rounded.CheckCircle,
1237-
contentDescription = null,
1238-
tint = accent,
1239-
modifier = Modifier.size(14.dp),
1240-
)
1241-
Spacer(Modifier.width(4.dp))
1242-
}
1243-
Text(
1244-
text = label,
1245-
style = MaterialTheme.typography.bodyMedium,
1246-
color = if (selected) accent else MaterialTheme.colorScheme.onSurfaceVariant,
1247-
)
1248-
}
1249-
}
1250-
}
12511090

1252-
/** A single bar inside the thumbnail, standing in for a line of text or a button. */
1253-
@Composable
1254-
private fun MiniCardLine(
1255-
widthFraction: Float,
1256-
color: Color,
1257-
height: Dp = 4.dp,
1258-
) {
1259-
Box(
1260-
modifier = Modifier
1261-
.fillMaxWidth(widthFraction)
1262-
.height(height)
1263-
.clip(RoundedCornerShape(2.dp))
1264-
.background(color),
1265-
)
1266-
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package app.pwhs.universalinstaller.presentation.setting.installui
2+
3+
import android.os.Bundle
4+
import androidx.compose.foundation.layout.Box
5+
import androidx.compose.foundation.layout.fillMaxSize
6+
import androidx.compose.ui.Modifier
7+
import app.pwhs.universalinstaller.base.BaseActivity
8+
9+
/** Host for [InstallUiScreen]. Same shape as the theme screen's activity, deliberately. */
10+
class InstallUiActivity : BaseActivity() {
11+
override fun onCreate(savedInstanceState: Bundle?) {
12+
super.onCreate(savedInstanceState)
13+
setContentWithTheme {
14+
Box(modifier = Modifier.fillMaxSize()) {
15+
InstallUiScreen()
16+
}
17+
}
18+
}
19+
}

0 commit comments

Comments
 (0)