Refactor preferences - #691
Conversation
sdsantos
left a comment
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
Why a Flow instead of a plain suspend method?
There was a problem hiding this comment.
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
There was a problem hiding this comment.
Regarding the white preview - it's not a regression, it's pre-existing. Will be fixed during UX/UI refresh.
There was a problem hiding this comment.
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
| run { | ||
| seedCommonDefaults() | ||
| readCommonSettings() | ||
| }, |
There was a problem hiding this comment.
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, |
There was a problem hiding this comment.
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> |
There was a problem hiding this comment.
Every call site unwraps this with runBlocking { .collect() }, and forgetting .collect() silently drops the write. Would plain fun setX(value) work here?
There was a problem hiding this comment.
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)
| 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/ |
There was a problem hiding this comment.
Should we be expecting this to be updated by agents as it goes in general?
| 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) | ||
| } | ||
| } |
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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
| @AndroidEntryPoint | ||
| class InAppGallery : AppCompatActivity() { |
There was a problem hiding this comment.
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
| override fun onCreate(savedInstanceState: Bundle?) { | ||
| isSecureMode = intent.getBooleanExtra(INTENT_KEY_SECURE_MODE, false) |
There was a problem hiding this comment.
Secure mode appears to have two representations:
SecureActivityis a type-level marker used by dependency injection to select secure/ephemeral repositoriesINTENT_KEY_SECURE_MODEis runtime state used byInAppGalleryto 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
Depends on #687
Closes #686
runBlockingand limited DI are intentional, as rewrite is phased.