Skip to content

Tag captured photos and videos with device location - #588

Open
temcguir wants to merge 28 commits into
temcguir/location-locationmanager-providerfrom
temcguir/location-pipeline-plumbing
Open

temcguir wants to merge 28 commits into
temcguir/location-locationmanager-providerfrom
temcguir/location-pipeline-plumbing

Conversation

@temcguir

@temcguir temcguir commented Oct 2, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

Wires the location stack into capture. Photos and videos are tagged with the device location when the user has enabled location saving and granted location permission.

This is Part 3 of the location series, stacked on #587.

How it works

While the preview is visible, PreviewViewModel runs the location provider from #587, which keeps an in-memory cached location (see the diagram in #587 for how location is acquired). On shutter press, the capture is tagged with the cached location without waiting on location hardware.

Key Changes

  • :core:camera:
    • Adds an optional location: Location? parameter to CameraSystem.takePicture and CameraSystem.startVideoRecording (default null).
    • Image capture sets the location on ImageCapture.Metadata. Out-of-range coordinates are dropped instead of failing the capture.
    • Video recording sets the location on the CameraX output options (FileOutputOptions, FileDescriptorOutputOptions, MediaStoreOutputOptions).
  • :ui:controller:impl:
    • CaptureControllerImpl accepts an optional LocationProvider and reads getCurrentLocation() at capture time for both images and videos.
  • :feature:preview:
    • PreviewViewModel injects Optional<LocationProvider>.
    • Runs LocationProvider.runLocationUpdates() while the preview is visible (startLocationUpdates() / stopLocationUpdates() from PreviewScreen's LifecycleStartEffect).
    • Gates both location updates and capture-time location on the locationEnabled setting.
    • Pauses location updates while video recording is starting or active, and resumes them when recording stops if the preview is still visible.
  • :core:location:location-manager-di (new):
    • Binds LocationManagerLocationProvider as the concrete LocationProvider. The optional contract stays in :core:location:location-di, following the low-light-playservices-di pattern, so host applications opt in explicitly and can substitute their own implementation.
  • :app:
    • Depends on :core:location:location-manager-di.
    • Declares ACCESS_FINE_LOCATION and ACCESS_COARSE_LOCATION, and the location hardware features as required="false".
    • Adds location to the startup permission list, which enables the onboarding location page and the Settings "Save Location" row added in Add location permission UX to onboarding and settings #586.
  • Testing:
    • PreviewViewModelTest: 9 tests covering location update start/stop, setting toggles, absent provider, video recording pause/resume, and capture with the setting disabled.
    • CaptureControllerImplTest: 4 tests covering location passed to image and video capture, and omitted when disabled.
    • PermissionsTest (instrumented): location permission page granted and denied paths.
    • FakeCameraSystem records the last location passed to capture calls.

Plumbs an optional Location through CameraSystem.takePicture and
startVideoRecording into CameraX ImageCapture metadata and video output
options. CaptureControllerImpl reads the current location from an
optional LocationProvider at capture time.

PreviewViewModel runs location warmup while the preview is visible,
gates it on the location setting, and pauses it during video recording.
The app module binds LocationManagerLocationProvider and declares the
location permissions.
…r-provider' into temcguir/location-pipeline-plumbing
…r-provider' into temcguir/location-pipeline-plumbing
…anager-di

The LocationManager-backed LocationProvider binding previously lived in
the app module's shared di package. Host applications that reuse that
package would receive the hardware-backed binding unconditionally and
could not substitute a fake or an alternative implementation.

Mirror the low-light-playservices-di pattern: keep the optional contract
in :core:location:location-di and move the concrete binding into a
dedicated module that :app opts into explicitly.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Code Review

This pull request implements geographic location tagging (geotagging) for both image and video captures. It introduces location permissions, a new Hilt module for location provider dependency injection, location hardware warmup logic in the preview lifecycle, and updates to the camera session to attach location metadata to output files. The review feedback focuses on improving robustness by sanitizing coordinates to prevent out-of-bounds crashes, adopting idiomatic Kotlin patterns (such as using apply instead of complex .let blocks and smart-casting Optional values), and restricting dependency injection provider visibility to internal.

Comment thread core/camera/src/main/java/com/google/jetpackcamera/core/camera/CameraSession.kt Outdated
Comment thread core/camera/src/main/java/com/google/jetpackcamera/core/camera/CameraSession.kt Outdated
Comment thread core/camera/src/main/java/com/google/jetpackcamera/core/camera/CameraSession.kt Outdated
CameraX video OutputOptions.Builder.setLocation throws
IllegalArgumentException when latitude is outside [-90, 90] or longitude
is outside [-180, 180]. Drop out-of-range locations in
CameraXCameraSystem before they reach image metadata or video output
options, using a shared Location.takeIfInBounds() helper. This replaces
the try/catch around ImageCapture.Metadata.setLocation, which does not
validate and could not throw.

Also pass the nullable location to OutputOptions builders directly and
resolve the Optional LocationProvider once in PreviewViewModel.
…ir/location-pipeline-plumbing

# Conflicts:
#	feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/PreviewViewModel.kt
#	ui/controller/impl/src/main/java/com/google/jetpackcamera/ui/controller/impl/CaptureControllerImpl.kt
#	ui/controller/impl/src/test/java/com/google/jetpackcamera/ui/controller/impl/CaptureControllerImplTest.kt
startLocationWarmup()/stopLocationWarmup() become startLocationUpdates()/
stopLocationUpdates(), and updateLocationWarmup() becomes
syncLocationUpdates(), matching LocationProvider.runLocationUpdates().
Test names updated to match. No behavior change.
- Treat an exception from getCurrentLocation() as no location so capture proceeds.
- Catch failures from runLocationUpdates() and restart the updates job if it is no longer active.
- Apply the initial location setting before the first settings emission.
- Document on LocationProvider that implementations should not throw.
…r-provider' into temcguir/location-pipeline-plumbing
PreviewViewModel combines preview visibility, the location setting, and
recording state into one flow and runs location updates with
collectLatest, replacing the @volatile flag and manual job tracking.
FakeCameraSystem stores capture locations in plain properties since it
is only accessed from the test thread.
Assign the unused result to satisfy the CheckResult lint check.
…r-provider' into temcguir/location-pipeline-plumbing
Calling startLocationUpdates while a session is running keeps the
current session instead of restarting it.
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.

2 participants