Skip to content

Refactor preferences - #691

Open
RankoR wants to merge 12 commits into
GrapheneOS:composefrom
RankoR-GOS:prefs-settings-repo
Open

Refactor preferences#691
RankoR wants to merge 12 commits into
GrapheneOS:composefrom
RankoR-GOS:prefs-settings-repo

Conversation

@RankoR

@RankoR RankoR commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Depends on #687
Closes #686

runBlocking and limited DI are intentional, as rewrite is phased.

@RankoR
RankoR requested review from inthewaves, m4pl and sdsantos August 11, 2026 22:16

@sdsantos sdsantos left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code looks good.

But I found a bug on the last photo preview. It shows empty (white), and when I tap it, it shows an error message. This persists until I take a photo, then everything works fine. If I open the production Camera app, the last photo preview is fine. Video:

camera_gallery_tap_bug.mp4

update: (CameraSettings) -> CameraSettings,
persist: () -> Unit,
): Flow<Unit> {
return unitFlow {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why a Flow instead of a plain suspend method?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I personally prefer Flows because IMO chaining them looks more elegant, and things like catching exceptions / switching dispatchers are easier to read compared to suspend functions

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Regarding the white preview - it's not a regression, it's pre-existing. Will be fixed during UX/UI refresh.

@inthewaves inthewaves Aug 13, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think Flows work when there's a stream of events, but for one-shot updates it puts the onus on the caller to call .collect() to ensure the write is done before continuing execution. I'm not sure if there's a use-case for chaining these set* calls for a one-shot Flow<Unit> vs a sequence of suspend functions

Comment on lines +89 to +92
run {
seedCommonDefaults()
readCommonSettings()
},

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The seed and read run in a field initializer, above slotted. An init {} below the properties makes that safe by construction and drops the run {}.

): SharedPreferences {
return commonPreferences(
context = context,
ephemeral = context is SecureActivity,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nothing checks that lockscreen activities actually carry the SecureActivity marker. Add a test so it can't be lost?


fun refresh()

fun setAspectRatio(value: Int): Flow<Unit>

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Every call site unwraps this with runBlocking { .collect() }, and forgetting .collect() silently drops the write. Would plain fun setX(value) work here?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

runBlocking is a temp solution to reduce per-PR changes, later it will look better. We should probably add a hard linter rule for that (Android Studio already highlights such things, but it should be a CI failure)

Comment thread AGENTS.md
Comment on lines 39 to 57
app/src/main/java/app/grapheneos/camera/
data/
core/
model/ types more than one feature stores, e.g. CameraMode
store/ the preferences files themselves, and the keys features share
settings/
model/ CameraSettings, per-mode setting values
repository/ SettingsRepository (entry-mode-scoped, never application-scoped)
store/ prefs-backed stores, EphemeralSharedPrefs namespace
camera/
model/ CameraCapabilities, lens/extension descriptors
repository/ CameraProviderSource
store/ ExtensionAvailabilityStore
media/
model/ CapturedItem and friends
repository/ CapturedItemStore
store/ MediaStoreDataSource, SafDataSource
repository/ CapturedItemRepository
store/ CapturedItemStore, MediaStoreDataSource, SafDataSource
location/
repository/ LocationRepository
domain/

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we be expecting this to be updated by agents as it goes in general?

Comment on lines +79 to +93
override fun trackSafTree(treeUri: Uri) {
val trees = previousSafTrees().toMutableList()

trees.remove(treeUri)
trees.add(0, treeUri)

while (trees.size > CapturedItems.MAX_NUMBER_OF_TRACKED_PREVIOUS_SAF_TREES) {
// trees.removeLast() requires API level 35 now due to Java adding it
trees.removeAt(trees.lastIndex)
}

commons.edit {
putPreviousSafTrees(trees, this)
}
}

@inthewaves inthewaves Aug 13, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe a sequence would be cleaner, e.g. it would only need to take MAX_NUMBER_OF_TRACKED_PREVIOUS_SAF_TREES elements instead of having to remove n - MAX_NUMBER_OF_TRACKED_PREVIOUS_SAF_TREES elements from the removeAt loop.

  val previousTrees = previousSafTrees()
      .asSequence()
      .filterNot { it == treeUri }

  val trees = sequenceOf(treeUri)
      .plus(previousTrees)
      .take(CapturedItems.MAX_NUMBER_OF_TRACKED_PREVIOUS_SAF_TREES)

putPreviousSafTrees doesn't seem to inherently need a list anyway, since it just does trees.joinToString

Although MAX_NUMBER_OF_TRACKED_PREVIOUS_SAF_TREES is pretty small anyway, so this is more of a nit

update: (CameraSettings) -> CameraSettings,
persist: () -> Unit,
): Flow<Unit> {
return unitFlow {

@inthewaves inthewaves Aug 13, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think Flows work when there's a stream of events, but for one-shot updates it puts the onus on the caller to call .collect() to ensure the write is done before continuing execution. I'm not sure if there's a use-case for chaining these set* calls for a one-shot Flow<Unit> vs a sequence of suspend functions

Comment on lines +66 to 67
@AndroidEntryPoint
class InAppGallery : AppCompatActivity() {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Secure gallery mode is selected through the isSecureMode intent extra, but repository DI determines whether to use LockscreenCapturedItemRepository by checking whether the activity implements SecureActivity. Since InAppGallery does not implement that marker, a secure gallery launch receives the ordinary repository.

This does not cause a current behavior bug: the gallery only calls capturedItems(), which the lockscreen wrapper delegates unchanged. However, the injected interface also exposes persistent mutation and SAF grant-release operations. If the marker is intended to enforce the lockscreen dependency boundary, consider injecting a narrower read-only gallery repository or using a distinct secure gallery activity. Otherwise, documenting this as an intentional exception would make the split between isSecureMode and SecureActivity clear

Comment on lines 609 to 610
override fun onCreate(savedInstanceState: Bundle?) {
isSecureMode = intent.getBooleanExtra(INTENT_KEY_SECURE_MODE, false)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Secure mode appears to have two representations:

  • SecureActivity is a type-level marker used by dependency injection to select secure/ephemeral repositories
  • INTENT_KEY_SECURE_MODE is runtime state used by InAppGallery to restrict UI behavior, filter media, and manage the lockscreen lifecycle

InAppGallery can therefore have isSecureMode == true without being a SecureActivity. Its UI behaves securely, but DI still supplies the ordinary CapturedItemRepository.

See the other review comment

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants