Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
e07f57c
Fix missing deps verification hashes
RankoR Aug 7, 2026
c3a43b1
Merge branch 'GrapheneOS:compose-prototype' into compose-prototype
m4pl Sep 8, 2026
9d18818
Merge branch 'GrapheneOS:compose-prototype' into compose-prototype
m4pl Sep 9, 2026
1660af7
Cover bind time video quality decisions
m4pl Sep 5, 2026
0431203
Move extension availability verdicts
m4pl Sep 5, 2026
9156ce3
Move camera provider outside CamConfig
m4pl Sep 5, 2026
afe2efe
Move camera session outside CamConfig
m4pl Sep 5, 2026
bca02d6
Move camera mode list outside CamConfig
m4pl Sep 5, 2026
c5747d5
Move view manipulation outside CamConfig
m4pl Sep 5, 2026
85d7a87
Collapse repeated settings properties
m4pl Sep 5, 2026
e004071
Build CamConfig through factory
m4pl Sep 5, 2026
6a447e9
Inject camera entry point
m4pl Sep 5, 2026
d8047c1
Remove MainActivity from CamConfig
m4pl Sep 5, 2026
db42a79
Move settings dialog setup into dialog
m4pl Sep 5, 2026
a7443d2
Move camera session out of CamConfig
m4pl Sep 5, 2026
c3e81a1
Move gestures and orientation out of MainActivity
m4pl Sep 6, 2026
1bdab84
Clean up after decomposing CamConfig
m4pl Sep 7, 2026
6a49434
Reach camera session directly
m4pl Sep 7, 2026
96b80a0
Make CamConfig listener on camera session
m4pl Sep 8, 2026
ba8f08a
Turn CamConfig into viewfinder controller
m4pl Sep 8, 2026
83ef58b
Point guidelines at decomposed viewfinder
m4pl Sep 8, 2026
6c4764f
Run tests on test dispatcher
m4pl Sep 9, 2026
4b5881d
Attach viewfinder to its Activity
m4pl Sep 9, 2026
785cdc8
Read setting as soon as another screen writes it
m4pl Sep 9, 2026
9595580
Put camera session behind interface
m4pl Sep 9, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 13 additions & 10 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -141,12 +141,14 @@ extension bind `UnsupportedOperationException`s, gallery NPEs.

### Legacy

The pre-migration code has no DI, no ViewModels, no coroutines in the camera path (raw
`thread {}`, `Executors`, `Handler`).
`CamConfig` holds `private val mActivity: MainActivity` and some of its properties read the View
tree directly (e.g. `requireLocation`'s getter returns
`mActivity.settingsDialog.locToggle.isChecked`). This coupling is the thing the migration exists to
undo — do not add to it.
The pre-migration code has no ViewModels and no coroutines in the camera path (raw `thread {}`,
`Executors`, `Handler`). `MainActivity` still owns the viewfinder's View tree, and the classes
around it — `SettingsDialog`, `ImageCapturer`, `VideoCapturer`, `QRAnalyzer`, the custom Views —
hold a `MainActivity` and reach into it. Reading state back out of that tree is the coupling the
migration exists to undo (`CamConfig` used to answer `requireLocation` with
`settingsDialog.locToggle.isChecked`); `ViewfinderController` now holds the state and pushes it out
through `ViewfinderChrome`/`ViewfinderEffects`. Do not add a new read of the View tree from below
the UI.

### Target

Expand Down Expand Up @@ -252,11 +254,11 @@ not otherwise changing — whitespace churn buries the diff and makes the migrat
- **No expression-body functions.** Always a block body with an explicit return type:
```kotlin
// WRONG
fun currentMode() = camConfig.currentMode
fun currentMode() = viewfinder.currentMode

// CORRECT
fun currentMode(): CameraMode {
return camConfig.currentMode
return viewfinder.currentMode
}
```
Return type is omitted for functions returning `Unit`; write `fun bind() {`, not
Expand Down Expand Up @@ -446,5 +448,6 @@ migrating the UI is exactly when they stop being reachable, and left behind they
- **Never add a commit co-author unless the user explicitly asks.**
- Commit messages: imperative mood, describing the behavior change rather than the mechanism —
match the existing log ("Don't initialize the camera while its permission is not granted").
- Test-facing seams in `CamConfig` (`mPlayer`, `photoQuality`, `camera`, `switchMode`) are written
to by the instrumented suite. They stay writable until the screen that owns them is migrated.
- Test-facing seams in `ViewfinderController` (`mPlayer`, `photoQuality`, `switchMode`) and
`CameraSession` (`camera`) are written to by the instrumented suite. They stay writable until the
screen that owns them is migrated.
2 changes: 2 additions & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -182,9 +182,11 @@ dependencies {
testImplementation(libs.junit4)
testImplementation(libs.robolectric)
testImplementation(libs.androidx.test.core.ktx)
testImplementation(libs.kotlinx.coroutines.test)

androidTestImplementation(libs.androidx.test.core.ktx)
androidTestImplementation(libs.androidx.test.ext.junit.ktx)
androidTestImplementation(libs.androidx.test.rules)
androidTestImplementation(libs.androidx.test.runner)
androidTestImplementation(libs.kotlinx.coroutines.test)
}
522 changes: 154 additions & 368 deletions app/config/ktlint/baseline.xml

Large diffs are not rendered by default.

75 changes: 34 additions & 41 deletions app/detekt-baseline-debug.xml

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ class BottomTabLayoutRegressionTest {

var travel = 0
scenario.onActivity { activity ->
activity.camConfig.switchMode(activity.tabLayout.getTabAt(0)!!.tag as CameraMode)
activity.viewfinder.switchMode(activity.tabLayout.getTabAt(0)!!.tag as CameraMode)
}
waitUntil(scenario, "the strip is centred on the first tab") {
it.tabLayout.scrollX == centreOf(it.tabLayout, 0)
Expand Down Expand Up @@ -127,7 +127,7 @@ class BottomTabLayoutRegressionTest {
dragStrip(scenario, scrollPx = travel)

waitUntil(scenario, "the camera switched to $targetMode") {
it.camConfig.currentMode == targetMode
it.viewfinder.currentMode == targetMode
}
waitUntil(scenario, "the strip settled on tab $target") {
it.tabLayout.scrollX == centreOf(it.tabLayout, target)
Expand Down Expand Up @@ -176,7 +176,7 @@ class BottomTabLayoutRegressionTest {

assertNotEquals(selectedAtStart, target)
waitUntil(scenario, "the camera switched to the tab the drag ended on") {
it.camConfig.currentMode == it.tabLayout.getTabAt(target)!!.tag
it.viewfinder.currentMode == it.tabLayout.getTabAt(target)!!.tag
}
}
}
Expand All @@ -199,7 +199,7 @@ class BottomTabLayoutRegressionTest {
var travel = 0
scenario.onActivity { activity ->
val tabs = activity.tabLayout
startMode = activity.camConfig.currentMode
startMode = activity.viewfinder.currentMode
target = nextTo(tabs.selectedTabPosition, tabs.tabCount)
// Two thirds of the way over: enough to commit the mode, and far enough short of
// the centre that the strip has a settle worth measuring left to run.
Expand All @@ -215,12 +215,12 @@ class BottomTabLayoutRegressionTest {
assertEquals(
"the camera rebound while the strip was still moving",
startMode,
activity.camConfig.currentMode,
activity.viewfinder.currentMode,
)
}

waitUntil(scenario, "the camera switched once the strip had settled") {
it.camConfig.currentMode == it.tabLayout.getTabAt(target)!!.tag
it.viewfinder.currentMode == it.tabLayout.getTabAt(target)!!.tag
}
}
}
Expand Down Expand Up @@ -254,7 +254,7 @@ class BottomTabLayoutRegressionTest {
assertEquals(
"the mode the strip was settling into never reached the camera",
tabs.getTabAt(target)!!.tag,
activity.camConfig.currentMode,
activity.viewfinder.currentMode,
)
}
}
Expand All @@ -278,7 +278,7 @@ class BottomTabLayoutRegressionTest {
var travel = 0
scenario.onActivity { activity ->
val tabs = activity.tabLayout
startMode = activity.camConfig.currentMode
startMode = activity.viewfinder.currentMode
target = nextTo(tabs.selectedTabPosition, tabs.tabCount)
travel = (centreOf(tabs, target) - tabs.scrollX) * 2 / 3
}
Expand All @@ -288,14 +288,14 @@ class BottomTabLayoutRegressionTest {
assertEquals(
"the camera switched before there was anything to check",
startMode,
activity.camConfig.currentMode,
activity.viewfinder.currentMode,
)
prefetched = activity.lastFrame
assertNotNull("the drag started no copy of the preview", prefetched)
}

waitUntil(scenario, "the camera switched to tab $target") {
it.camConfig.currentMode == it.tabLayout.getTabAt(target)!!.tag
it.viewfinder.currentMode == it.tabLayout.getTabAt(target)!!.tag
}
scenario.onActivity {
assertSame(
Expand Down Expand Up @@ -325,7 +325,7 @@ class BottomTabLayoutRegressionTest {
var travel = 0
scenario.onActivity { activity ->
val tabs = activity.tabLayout
startMode = activity.camConfig.currentMode
startMode = activity.viewfinder.currentMode
target = nextTo(tabs.selectedTabPosition, tabs.tabCount)
travel = (centreOf(tabs, target) - tabs.scrollX) * 2 / 3
}
Expand All @@ -334,7 +334,7 @@ class BottomTabLayoutRegressionTest {
assertEquals(
"the camera rebound before there was anything to check",
startMode,
activity.camConfig.currentMode,
activity.viewfinder.currentMode,
)
assertEquals(
"the preview was left live over the freeze the switch is about to cost",
Expand All @@ -360,7 +360,7 @@ class BottomTabLayoutRegressionTest {
val tabs = activity.tabLayout
// Started against a surface the rebind has just emptied, so the copy comes back
// with nothing and schedules the retry this is about.
activity.camConfig.switchMode(
activity.viewfinder.switchMode(
tabs.getTabAt(nextTo(tabs.selectedTabPosition, tabs.tabCount))!!.tag
as CameraMode
)
Expand All @@ -371,7 +371,7 @@ class BottomTabLayoutRegressionTest {
Thread.sleep(RETRY_WINDOW_MS)

scenario.moveToState(Lifecycle.State.RESUMED)
waitUntil(scenario, "the camera came back") { it.camConfig.camera != null }
waitUntil(scenario, "the camera came back") { it.session.camera != null }
}
}

Expand All @@ -392,7 +392,7 @@ class BottomTabLayoutRegressionTest {
// The rebind that applies the new ratio runs whole inside one message, so a poll
// that sees the mode has landed sees a layout the resize has already asked for.
waitUntil(scenario, "the preview took $mode's aspect ratio") {
it.camConfig.currentMode == mode && !it.previewView.isLayoutRequested
it.viewfinder.currentMode == mode && !it.previewView.isLayoutRequested
}

scenario.onActivity { activity ->
Expand Down Expand Up @@ -431,14 +431,14 @@ class BottomTabLayoutRegressionTest {
var startMode: CameraMode? = null
scenario.onActivity { activity ->
val tabs = activity.tabLayout
startMode = activity.camConfig.currentMode
startMode = activity.viewfinder.currentMode

activity.finalizeMode(tabs.getTabAt(nextTo(tabs.selectedTabPosition, tabs.tabCount)))

assertEquals(
"the switch reached the camera before it could be taken back",
startMode,
activity.camConfig.currentMode,
activity.viewfinder.currentMode,
)
assertEquals(
"the switch put up no transition to strand",
Expand All @@ -458,7 +458,7 @@ class BottomTabLayoutRegressionTest {
assertEquals(
"the camera left the mode the strip came back to",
startMode,
it.camConfig.currentMode,
it.viewfinder.currentMode,
)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,10 +42,10 @@ class CameraModeTabsRegressionTest {
awaitModeTabs(scenario)

scenario.onActivity { activity ->
activity.camConfig.switchMode(CameraMode.VIDEO)
activity.viewfinder.switchMode(CameraMode.VIDEO)
assertEquals(CameraMode.VIDEO, activity.tabLayout.selectedTab?.tag)

activity.camConfig.switchMode(CameraMode.CAMERA)
activity.viewfinder.switchMode(CameraMode.CAMERA)
assertEquals(CameraMode.CAMERA, activity.tabLayout.selectedTab?.tag)
}
}
Expand All @@ -60,7 +60,7 @@ class CameraModeTabsRegressionTest {
ActivityScenario.launch(MainActivity::class.java).use { scenario ->
awaitModeTabs(scenario)

scenario.onActivity { it.camConfig.switchMode(CameraMode.VIDEO) }
scenario.onActivity { it.viewfinder.switchMode(CameraMode.VIDEO) }

// Counting passes rather than reading isLayoutRequested: a request issued from inside a
// layout pass is parked for the next traversal and the flag is cleared on the way, so it
Expand Down Expand Up @@ -116,7 +116,7 @@ class CameraModeTabsRegressionTest {
scenario.onActivity { activity ->
val tabs = activity.tabLayout
val next = tabs.getTabAt(tabs.selectedTabPosition + 1)
assertNotNull("no mode to the left of ${activity.camConfig.currentMode}", next)
assertNotNull("no mode to the left of ${activity.viewfinder.currentMode}", next)
nextMode = next!!.tag as CameraMode

flingLeft(activity)
Expand All @@ -125,26 +125,26 @@ class CameraModeTabsRegressionTest {
// The strip slides to the new mode before the camera rebinds, so the switch lands a
// few frames after the fling rather than inside it.
waitUntil(scenario, "the fling switched the mode to $nextMode") {
it.camConfig.currentMode == nextMode
it.viewfinder.currentMode == nextMode
}
}

launchCaptureSession(CaptureActivity::class.java, MediaStore.ACTION_IMAGE_CAPTURE)
.use { scenario ->
waitUntil(scenario, "camera is bound") { it.camConfig.camera != null }
waitUntil(scenario, "camera is bound") { it.session.camera != null }

// Flinging before the tabs were built would pass whether or not any get built
Thread.sleep(TAB_BUILD_DWELL_MS)

scenario.onActivity { activity ->
flingLeft(activity)
assertEquals(CameraMode.CAMERA, activity.camConfig.currentMode)
assertEquals(CameraMode.CAMERA, activity.viewfinder.currentMode)
}
}
}

private fun <A : MainActivity> assertNoModeTabs(scenario: ActivityScenario<A>) {
waitUntil(scenario, "camera is bound") { it.camConfig.camera != null }
waitUntil(scenario, "camera is bound") { it.session.camera != null }

// Asserting straight after the bind would also hold while a build was merely still
// pending, so outlast the extension probe round that used to precede one.
Expand Down
Loading