aboutsummaryrefslogtreecommitdiffstats
path: root/docs/superpowers/specs/2026-11-05-git-dialogs-compose-design.md
blob: 3ccf5fb176ee1b1d68cb84d5a1d4189f20f15a2a (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
# Git dialogs to Compose, Material and AppCompat out

Last `TODO.md` item. Moves the remaining View dialogs and the snackbar helper to Compose, removes
Material Components, raises minSdk to 31 and takes the app's own code off AppCompat.

## Goals

- `com.google.android.material:material` is gone from the dependency graph.
- No `androidx.appcompat` import and no direct `appcompat` dependency in the app.
- minSdk is 31 (Android 12) in every module.

## Non-goals

- Removing AppCompat from the APK. `zxing-android-embedded` still pulls it in: `CaptureManager`
  uses its `AlertDialog` and `zxing_CaptureTheme` has parent `Theme.AppCompat.NoActionBar`.
  Replacing the QR scanner is a new `TODO.md` item.
- Renaming or restructuring the git operations beyond what the dialogs need.

## Why minSdk 31

The project targets GrapheneOS, which runs current Android only. `androidx.biometric`'s
`FingerprintDialogFragment` builds an AppCompat `AlertDialog`, used on old Android only, so moving
themes off AppCompat at minSdk 26 risks a crash on Android 8. At 31 that path is unused,
`UiModeManager.setApplicationNightMode` exists, and dynamic colors are always available. Devices
below Android 12 stay on 0.2.0; the next release notes must say so.

## Design

### Prompts

`Prompts` is a ViewModel scoped to the activity, reached with the extension `activity.prompts`
(`ViewModelProvider(activity)`), so no interface is needed. It offers:

- `suspend fun ask(title, message, confirm, cancel = null, other = null): Choice`, which suspends
  until a button is pressed or the dialog is dismissed. Dismissing answers `Choice.Cancel`, so a
  tap outside the missing SSH key dialog can never mean "generate".
- `askCredential(...)` for the credential dialog, delivering the entered text and the remember
  choice, or null on cancel.
- `snackbar(message, length)`, with `length` short, long or indefinite, returning a handle to
  dismiss it.

`PromptHost()` is a composable placed inside `APSTheme` at the root of `MainActivity` and
`AutofillSaveActivity`, the only two activities that show these. It draws the current dialog and a
`SnackbarHost` at the bottom of the window, above the navigation bar and keyboard.

The state lives in the ViewModel. Git operations run in the activity's `lifecycleScope`, so a theme
or locale recreation cancels them: `ask` removes its dialog when cancelled. The credential dialog
blocks an SSH thread that cancellation cannot reach, so it stays and is redrawn by the new activity
instead of leaving that thread waiting forever. When the activity finishes for good, `onCleared`
answers every pending prompt with its cancelling answer.

### Dialogs

These all move to `ask`:

- the git error dialog in `BaseGitActivity.promptOnErrorHandler`, whose unused `onPromptDone`
  parameter goes
- the missing SSH key dialog in `GitOperation` (import, generate, cancel)
- `BreakOutOfDetached`'s "not needed" notice
- `moveFile`'s error and `confirm` in `StoreActions`

`GitOperation.preExecute` becomes suspend so the notice can wait for its answer.

The credential dialog in `CredentialFinder` becomes a Compose `AlertDialog`: password field with
the mode's hint, remember checkbox, retry error that clears on typing, keyboard open on show, and
`SecureFlagPolicy.SecureOn` (the View dialog had no `FLAG_SECURE`). Storing a remembered credential
stays in `CredentialFinder`. `InteractivePasswordFinder`'s blocking wait on the main dispatcher
does not change. `git_credential_layout.xml` is deleted.

### Snackbar

`FragmentActivity.snackbar` keeps its name and now goes through `Prompts`, so call sites change
only their `length` argument. A new snackbar replaces the current one, as the View snackbar did,
rather than queueing behind it as `SnackbarHostState` does by default. `GitCommandExecutor` keeps
"operation running" up until the operation ends, then dismisses it.

### minSdk

`AndroidCommon.kt` sets 31. Branches that become dead are deleted, including the below-Android-11
snackbar in `copyTextToClipboard` and its parameters, and `Api26AutofillResponseBuilder`. Lint runs
with `warningsAsErrors`, so `ObsoleteSdkInt` lists whatever is missed. `StrictDomainRegexTest`'s
`@Config(sdk = [23])` goes, being below minSdk.

### AppCompat out

- `BaseGitActivity`, `LaunchActivity` and the autofill activities extend `FragmentActivity`, which
  `BiometricPrompt` needs. `GitOperation` subclasses and `SshAuthMethod` take `FragmentActivity`.
- `supportActionBar?.hide()` goes: the themes have no action bar.
- Window themes derive from `android:Theme.Material.Light.NoActionBar`, and
  `android:Theme.Material.NoActionBar` in `values-night`. AppCompat-only theme items are removed
  or switched to their `android:` form. `zxing_CaptureTheme` is untouched.
- `fragment-ktx` stays as the provider of `FragmentActivity`; its comment is updated.

### Night mode

`Application.setNightMode` calls `UiModeManager.setApplicationNightMode`: `light` is
`MODE_NIGHT_NO`, `dark` is `MODE_NIGHT_YES`, everything else is `MODE_NIGHT_AUTO`.

`values-v29` already selects `follow_system` and hides "Set by Battery Saver" on Android 10 and
later, so nothing changes for users: its `arrays.xml` and `prefs.xml` replace the base ones, the
folder goes, and the battery saver string leaves every locale. A stored `battery_saver` value could
only come from a device that ran the app on Android 9 or older; it still maps to `MODE_NIGHT_AUTO`.

### System bars

`DynamicColors` tints the bars today through the window theme. Without it, `APSTheme` sets the
status and navigation bar colors from `colorScheme.background` in its existing `SideEffect`,
skipping windows whose theme has transparent bars (the autofill decrypt window). The deprecation
on these setters at compileSdk 35 is suppressed at that line, since `CI=1` makes warnings errors.

### Dependencies and resources

`androidx-material` and `androidx-appcompat` leave `app/build.gradle.kts` and the catalog.
`DynamicColors.applyToActivitiesIfAvailable` goes from `Application`. Color resources that lint
reports as unused after this (`md_theme_*`) are deleted.

## Testing

- JVM test for `Prompts`: an answer resumes `ask`, dismissing gives the cancelling answer, a new
  snackbar replaces the current one.
- Full command from `AGENTS.md`: `CI=1 ./gradlew spotlessCheck test -PslimTests :app:assembleDebug
  :app:assembleRelease lint metalavaCheckCompatibilityRelease`.
- Emulator, on the e2e store: credential dialog (wrong passphrase retry, remember, cancel), missing
  SSH key dialog, git error dialog, conflict recovery, move error, delete and move confirmations,
  every theme setting, bar colors in light and dark, QR scanner
  opens, fingerprint unlock, a theme change while a dialog is open.
- Real device with Firefox: autofill save, since its window now hosts `PromptHost`, and the
  autofill windows' transparent bars.

## Docs

- `AGENTS.md`: the Material Components gotcha, the window theme parent, the autofill activities'
  base class.
- `TODO.md`: remove this item, add replacing the QR scanner so AppCompat can leave the APK.
- The website docs, if they state a minimum Android version. That lives in the site repo and
  deploys through production.

## Commit order

1. minSdk 31 and dead branches.
2. `Prompts`, `PromptHost` and all call sites.
3. AppCompat out: activities, themes, night mode, `DynamicColors`, system bars.
4. Dependencies and unused resources out, docs.

AppCompat goes before the Material dependency so the window themes change only once.