diff --git a/app/src/main/java/com/google/jetpackcamera/ui/JcaApp.kt b/app/src/main/java/com/google/jetpackcamera/ui/JcaApp.kt
index e66f51055..889771b14 100644
--- a/app/src/main/java/com/google/jetpackcamera/ui/JcaApp.kt
+++ b/app/src/main/java/com/google/jetpackcamera/ui/JcaApp.kt
@@ -16,7 +16,6 @@
package com.google.jetpackcamera.ui
import android.Manifest
-import android.net.Uri
import androidx.compose.animation.AnimatedContentTransitionScope
import androidx.compose.animation.core.EaseIn
import androidx.compose.animation.core.EaseOut
@@ -27,11 +26,9 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.Modifier
import androidx.navigation.NavHostController
-import androidx.navigation.NavType
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
-import androidx.navigation.navArgument
import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.google.accompanist.permissions.isGranted
import com.google.accompanist.permissions.rememberMultiplePermissionsState
@@ -118,11 +115,7 @@ private fun JetpackCameraNavHost(
}
PreviewScreen(
onNavigateToSettings = { navController.navigate(SETTINGS_ROUTE) },
- onNavigateToPostCapture = { imageUri ->
- navController.navigate(
- "$POST_CAPTURE_ROUTE?imageUri=${Uri.encode(imageUri.toString())}"
- )
- },
+ onNavigateToPostCapture = { navController.navigate(POST_CAPTURE_ROUTE) },
onRequestWindowColorMode = onRequestWindowColorMode,
onFirstFrameCaptureCompleted = onFirstFrameCaptureCompleted,
previewMode = previewMode,
@@ -156,25 +149,10 @@ private fun JetpackCameraNavHost(
}
composable(
- "$POST_CAPTURE_ROUTE?imageUri={imageUri}",
- arguments = listOf(
- navArgument("imageUri") {
- type = NavType.StringType
- defaultValue = ""
- }
- )
- ) { backStackEntry ->
- val imageUriString = backStackEntry.arguments?.getString("imageUri")
-
- val imageUri = if (!imageUriString.isNullOrEmpty()) {
- Uri.parse(
- imageUriString
- )
- } else {
- null
- }
+ POST_CAPTURE_ROUTE
+ ) {
PostCaptureScreen(
- imageUri = imageUri
+ onRequestWindowColorMode = onRequestWindowColorMode
)
}
}
diff --git a/core/common/src/main/java/com/google/jetpackcamera/core/common/MediaStoreUtils.kt b/core/common/src/main/java/com/google/jetpackcamera/core/common/MediaStoreUtils.kt
deleted file mode 100644
index f7604522f..000000000
--- a/core/common/src/main/java/com/google/jetpackcamera/core/common/MediaStoreUtils.kt
+++ /dev/null
@@ -1,100 +0,0 @@
-/*
- * Copyright (C) 2025 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.google.jetpackcamera.core.common
-
-import android.content.ContentUris
-import android.content.Context
-import android.graphics.Bitmap
-import android.graphics.ImageDecoder
-import android.graphics.Matrix
-import android.net.Uri
-import android.provider.MediaStore
-import java.io.File
-import java.io.FileNotFoundException
-
-/**
- * Retrieves the URI for the most recently added image whose filename starts with "JCA".
- *
- * @param context The application context.
- * @return The content URI of the matching image, or null if none is found.
- */
-fun getLastImageUri(context: Context): Uri? {
- val projection = arrayOf(
- MediaStore.Images.Media._ID,
- MediaStore.Images.Media.DATE_ADDED
- )
-
- // Filter by filenames starting with "JCA"
- val selection = "${MediaStore.Images.Media.DISPLAY_NAME} LIKE ?"
- val selectionArgs = arrayOf("JCA%")
-
- // Sort the results so that the most recently added image appears first.
- val sortOrder = "${MediaStore.Images.Media.DATE_ADDED} DESC"
-
- // Perform the query on the MediaStore.
- context.contentResolver.query(
- MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
- projection,
- selection,
- selectionArgs,
- sortOrder
- )?.use { cursor ->
- if (cursor.moveToFirst()) {
- val idColumn = cursor.getColumnIndexOrThrow(MediaStore.Images.Media._ID)
- val id = cursor.getLong(idColumn)
-
- return ContentUris.withAppendedId(
- MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
- id
- )
- }
- }
- return null
-}
-
-/**
- * Loads a Bitmap from a given URI and rotates it by the specified degrees.
- *
- * @param context The application context.
- * @param uri The URI of the image to load.
- * @param degrees The number of degrees to rotate the image by.
- */
-fun loadAndRotateBitmap(context: Context, uri: Uri?, degrees: Float): Bitmap? {
- uri ?: return null
-
- if (uri.scheme == "file") {
- val file = File(uri.path ?: "")
- if (!file.exists()) {
- return null
- }
- }
-
- return try {
- val bitmap = if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.P) {
- MediaStore.Images.Media.getBitmap(context.contentResolver, uri)
- } else {
- val imageDecoderSource = ImageDecoder.createSource(context.contentResolver, uri)
- ImageDecoder.decodeBitmap(imageDecoderSource)
- }
-
- bitmap?.let {
- val matrix = Matrix().apply { postRotate(degrees) }
- Bitmap.createBitmap(it, 0, 0, it.width, it.height, matrix, true)
- }
- } catch (e: FileNotFoundException) {
- null
- }
-}
diff --git a/data/media/.gitignore b/data/media/.gitignore
new file mode 100644
index 000000000..42afabfd2
--- /dev/null
+++ b/data/media/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/data/media/build.gradle.kts b/data/media/build.gradle.kts
new file mode 100644
index 000000000..2f382a2f4
--- /dev/null
+++ b/data/media/build.gradle.kts
@@ -0,0 +1,93 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+plugins {
+ alias(libs.plugins.android.library)
+ alias(libs.plugins.kotlin.android)
+ alias(libs.plugins.kotlin.kapt)
+ alias(libs.plugins.dagger.hilt.android)
+}
+
+android {
+ namespace = "com.google.jetpackcamera.data.media"
+ compileSdk = libs.versions.compileSdk.get().toInt()
+
+ defaultConfig {
+ minSdk = libs.versions.minSdk.get().toInt()
+ testOptions.targetSdk = libs.versions.targetSdk.get().toInt()
+ lint.targetSdk = libs.versions.targetSdk.get().toInt()
+
+ testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
+ consumerProguardFiles("consumer-rules.pro")
+ }
+
+ flavorDimensions += "flavor"
+ productFlavors {
+ create("stable") {
+ dimension = "flavor"
+ isDefault = true
+ }
+ }
+
+ compileOptions {
+ sourceCompatibility = JavaVersion.VERSION_17
+ targetCompatibility = JavaVersion.VERSION_17
+ }
+ kotlin {
+ jvmToolchain(17)
+ }
+
+ @Suppress("UnstableApiUsage")
+ testOptions {
+ managedDevices {
+ localDevices {
+ create("pixel2Api28") {
+ device = "Pixel 2"
+ apiLevel = 28
+ }
+ create("pixel8Api34") {
+ device = "Pixel 8"
+ apiLevel = 34
+ systemImageSource = "aosp_atd"
+ }
+ }
+ }
+ }
+}
+
+dependencies {
+ implementation(libs.kotlinx.coroutines.core)
+
+ // Hilt
+ implementation(libs.dagger.hilt.android)
+ kapt(libs.dagger.hilt.compiler)
+
+ // Testing
+ testImplementation(libs.junit)
+ testImplementation(libs.truth)
+ androidTestImplementation(libs.androidx.espresso.core)
+ androidTestImplementation(libs.androidx.junit)
+ androidTestImplementation(libs.truth)
+ androidTestImplementation(libs.kotlinx.coroutines.test)
+
+
+ // Project dependencies
+ implementation(project(":core:common"))
+}
+
+// Allow references to generated code
+kapt {
+ correctErrorTypes = true
+}
diff --git a/data/media/consumer-rules.pro b/data/media/consumer-rules.pro
new file mode 100644
index 000000000..e69de29bb
diff --git a/data/media/proguard-rules.pro b/data/media/proguard-rules.pro
new file mode 100644
index 000000000..481bb4348
--- /dev/null
+++ b/data/media/proguard-rules.pro
@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile
\ No newline at end of file
diff --git a/data/media/src/main/AndroidManifest.xml b/data/media/src/main/AndroidManifest.xml
new file mode 100644
index 000000000..1e7c244bc
--- /dev/null
+++ b/data/media/src/main/AndroidManifest.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/data/media/src/main/kotlin/com/google/jetpackcamera/data/media/FakeMediaRepository.kt b/data/media/src/main/kotlin/com/google/jetpackcamera/data/media/FakeMediaRepository.kt
new file mode 100644
index 000000000..590d10ca6
--- /dev/null
+++ b/data/media/src/main/kotlin/com/google/jetpackcamera/data/media/FakeMediaRepository.kt
@@ -0,0 +1,26 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.jetpackcamera.data.media
+
+object FakeMediaRepository : MediaRepository {
+ override suspend fun getLastCapturedMedia(): MediaDescriptor {
+ return MediaDescriptor.None
+ }
+
+ override suspend fun load(mediaDescriptor: MediaDescriptor): Media {
+ return Media.None
+ }
+}
diff --git a/data/media/src/main/kotlin/com/google/jetpackcamera/data/media/LocalMediaRepository.kt b/data/media/src/main/kotlin/com/google/jetpackcamera/data/media/LocalMediaRepository.kt
new file mode 100644
index 000000000..b38903fc7
--- /dev/null
+++ b/data/media/src/main/kotlin/com/google/jetpackcamera/data/media/LocalMediaRepository.kt
@@ -0,0 +1,167 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.jetpackcamera.data.media
+
+import android.content.ContentUris
+import android.content.Context
+import android.graphics.Bitmap
+import android.graphics.BitmapFactory
+import android.graphics.ImageDecoder
+import android.net.Uri
+import android.os.Build
+import android.provider.MediaStore
+import android.util.Size
+import com.google.jetpackcamera.core.common.IODispatcher
+import dagger.hilt.android.qualifiers.ApplicationContext
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.withContext
+
+class LocalMediaRepository
+@Inject constructor(
+ @ApplicationContext private val context: Context,
+ @IODispatcher private val iODispatcher: CoroutineDispatcher
+) : MediaRepository {
+
+ override suspend fun load(mediaDescriptor: MediaDescriptor): Media {
+ return when (mediaDescriptor) {
+ is MediaDescriptor.Image -> loadImage(mediaDescriptor.uri)
+ MediaDescriptor.None -> Media.None
+ is MediaDescriptor.Video -> Media.Video(mediaDescriptor.uri)
+ }
+ }
+
+ override suspend fun getLastCapturedMedia(): MediaDescriptor {
+ val imagePair =
+ getLastMediaUriWithDate(context, MediaStore.Images.Media.EXTERNAL_CONTENT_URI)
+ val videoPair =
+ getLastMediaUriWithDate(context, MediaStore.Video.Media.EXTERNAL_CONTENT_URI)
+
+ return when {
+ imagePair == null && videoPair == null -> MediaDescriptor.None
+ imagePair == null && videoPair != null -> getVideoMediaDescriptor(videoPair.first)
+ videoPair == null && imagePair != null -> getImageMediaDescriptor(imagePair.first)
+ imagePair != null && videoPair != null -> {
+ if (imagePair.second > videoPair.second) {
+ getImageMediaDescriptor(imagePair.first)
+ } else {
+ getVideoMediaDescriptor(videoPair.first)
+ }
+ }
+
+ else -> MediaDescriptor.None // Should not happen
+ }
+ }
+
+ private suspend fun loadImage(uri: Uri): Media = withContext(iODispatcher) {
+ try {
+ val loadedBitmap = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
+ // Android 10 (API 29) and above: Use ImageDecoder
+ val source = ImageDecoder.createSource(context.contentResolver, uri)
+ ImageDecoder.decodeBitmap(source)
+ } else {
+ // Android 9 (API 28) and below: Use BitmapFactory
+ context.contentResolver.openInputStream(uri)?.use { inputStream ->
+ BitmapFactory.decodeStream(inputStream)
+ }
+ }
+
+ return@withContext if (loadedBitmap != null) {
+ Media.Image(loadedBitmap)
+ } else {
+ Media.Error
+ }
+ } catch (e: Exception) {
+ e.printStackTrace()
+ return@withContext Media.Error
+ }
+ }
+
+ private suspend fun getVideoMediaDescriptor(uri: Uri): MediaDescriptor {
+ val thumbnail = getThumbnail(uri, MediaStore.Video.Media.EXTERNAL_CONTENT_URI)
+ return MediaDescriptor.Video(uri, thumbnail)
+ }
+
+ private suspend fun getImageMediaDescriptor(uri: Uri): MediaDescriptor {
+ val thumbnail = getThumbnail(uri, MediaStore.Images.Media.EXTERNAL_CONTENT_URI)
+ return MediaDescriptor.Image(uri, thumbnail)
+ }
+
+ private suspend fun getThumbnail(uri: Uri, collectionUri: Uri): Bitmap? =
+ withContext(iODispatcher) {
+ return@withContext try {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
+ context.contentResolver.loadThumbnail(uri, Size(640, 480), null)
+ } else {
+ if (collectionUri == MediaStore.Images.Media.EXTERNAL_CONTENT_URI) {
+ MediaStore.Images.Thumbnails.getThumbnail(
+ context.contentResolver,
+ ContentUris.parseId(uri),
+ MediaStore.Images.Thumbnails.MINI_KIND,
+ null
+ )
+ } else { // Video
+ MediaStore.Video.Thumbnails.getThumbnail(
+ context.contentResolver,
+ ContentUris.parseId(uri),
+ MediaStore.Video.Thumbnails.MINI_KIND,
+ null
+ )
+ }
+ }
+ } catch (e: Exception) {
+ e.printStackTrace()
+ null
+ }
+ }
+
+ private fun getLastMediaUriWithDate(context: Context, collectionUri: Uri): Pair? {
+ val projection = arrayOf(
+ MediaStore.MediaColumns._ID,
+ MediaStore.MediaColumns.DATE_ADDED
+ )
+
+ // Filter by filenames starting with "JCA"
+ val selection = "${MediaStore.MediaColumns.DISPLAY_NAME} LIKE ?"
+ val selectionArgs = arrayOf("JCA%")
+
+ // Sort the results so that the most recently added media appears first.
+ val sortOrder = "${MediaStore.MediaColumns.DATE_ADDED} DESC"
+
+ // Perform the query on the MediaStore.
+ context.contentResolver.query(
+ collectionUri,
+ projection,
+ selection,
+ selectionArgs,
+ sortOrder
+ )?.use { cursor ->
+ if (cursor.moveToFirst()) {
+ val idColumn = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns._ID)
+ val dateAddedColumn = cursor.getColumnIndexOrThrow(
+ MediaStore.MediaColumns.DATE_ADDED
+ )
+
+ val id = cursor.getLong(idColumn)
+ val dateAdded = cursor.getLong(dateAddedColumn)
+
+ val uri = ContentUris.withAppendedId(collectionUri, id)
+ return Pair(uri, dateAdded)
+ }
+ }
+ return null
+ }
+}
diff --git a/data/media/src/main/kotlin/com/google/jetpackcamera/data/media/MediaModule.kt b/data/media/src/main/kotlin/com/google/jetpackcamera/data/media/MediaModule.kt
new file mode 100644
index 000000000..a4ec2ddfc
--- /dev/null
+++ b/data/media/src/main/kotlin/com/google/jetpackcamera/data/media/MediaModule.kt
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.jetpackcamera.data.media
+
+import dagger.Binds
+import dagger.Module
+import dagger.hilt.InstallIn
+import dagger.hilt.components.SingletonComponent
+import javax.inject.Singleton
+
+/**
+ * Dagger [Module] for Media dependencies.
+ */
+@Module
+@InstallIn(SingletonComponent::class)
+interface MediaModule {
+
+ @Binds
+ @Singleton
+ fun bindsMediaRepository(localMediaRepository: LocalMediaRepository): MediaRepository
+}
diff --git a/data/media/src/main/kotlin/com/google/jetpackcamera/data/media/MediaRepository.kt b/data/media/src/main/kotlin/com/google/jetpackcamera/data/media/MediaRepository.kt
new file mode 100644
index 000000000..d7705bf0a
--- /dev/null
+++ b/data/media/src/main/kotlin/com/google/jetpackcamera/data/media/MediaRepository.kt
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.jetpackcamera.data.media
+
+import android.graphics.Bitmap
+import android.net.Uri
+
+/**
+ * Data layer for Media.
+ */
+interface MediaRepository {
+ suspend fun getLastCapturedMedia(): MediaDescriptor
+ suspend fun load(mediaDescriptor: MediaDescriptor): Media
+}
+
+/**
+ * Descriptors used for [Media].
+ *
+ * Media descriptors contain a reference to a [Media] item that's not yet loaded.
+ */
+sealed interface MediaDescriptor {
+ data object None : MediaDescriptor
+ class Image(val uri: Uri, val thumbnail: Bitmap?) : MediaDescriptor
+ class Video(val uri: Uri, val thumbnail: Bitmap?) : MediaDescriptor
+}
+
+/**
+ * Media items that are supported by [MediaRepository].
+ *
+ * [Image] will have the bitmap data loaded.
+ * [Video] is still a reference to the video file, will switch to a loaded version later on.
+ *
+ * TODO(yasith): Load the video data to the Video object.
+ */
+sealed interface Media {
+ data object None : Media
+ data object Error : Media
+ class Image(val bitmap: Bitmap) : Media
+ class Video(val uri: Uri) : Media
+}
diff --git a/feature/postcapture/build.gradle.kts b/feature/postcapture/build.gradle.kts
index f42133426..88f1bf042 100644
--- a/feature/postcapture/build.gradle.kts
+++ b/feature/postcapture/build.gradle.kts
@@ -16,6 +16,8 @@
plugins {
alias(libs.plugins.android.library)
alias(libs.plugins.kotlin.android)
+ alias(libs.plugins.kotlin.kapt)
+ alias(libs.plugins.dagger.hilt.android)
}
android {
@@ -111,6 +113,14 @@ dependencies {
testImplementation(libs.compose.test.manifest)
testImplementation(libs.compose.junit)
+ // Hilt
+ implementation(libs.dagger.hilt.android)
+ kapt(libs.dagger.hilt.compiler)
+
+ // Media3
+ implementation(libs.androidx.media3.exoplayer)
+ implementation(libs.androidx.media3.ui.compose)
+
// Testing
testImplementation(libs.junit)
testImplementation(libs.truth)
@@ -129,5 +139,11 @@ dependencies {
// Project dependencies
implementation(project(":core:common"))
+ implementation(project(":data:media"))
testImplementation(project(":core:common"))
-}
\ No newline at end of file
+}
+
+// Allow references to generated code
+kapt {
+ correctErrorTypes = true
+}
diff --git a/feature/postcapture/src/main/java/com/google/jetpackcamera/feature/postcapture/PostCaptureScreen.kt b/feature/postcapture/src/main/java/com/google/jetpackcamera/feature/postcapture/PostCaptureScreen.kt
index 854f64fb5..8cda26d5f 100644
--- a/feature/postcapture/src/main/java/com/google/jetpackcamera/feature/postcapture/PostCaptureScreen.kt
+++ b/feature/postcapture/src/main/java/com/google/jetpackcamera/feature/postcapture/PostCaptureScreen.kt
@@ -17,7 +17,12 @@ package com.google.jetpackcamera.feature.postcapture
import android.content.Context
import android.content.Intent
+import android.content.pm.ActivityInfo
import android.net.Uri
+import android.os.Build
+import android.util.Log
+import android.view.Display.HdrCapabilities.HDR_TYPE_HLG
+import androidx.annotation.OptIn
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
@@ -37,38 +42,43 @@ import androidx.compose.material3.IconButtonDefaults
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
-import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
-import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.shadow
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.drawscope.drawIntoCanvas
import androidx.compose.ui.graphics.nativeCanvas
+import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
-import com.google.jetpackcamera.core.common.loadAndRotateBitmap
+import androidx.media3.common.util.UnstableApi
+import androidx.media3.ui.compose.PlayerSurface
+import androidx.media3.ui.compose.modifiers.resizeWithContentScale
+import androidx.media3.ui.compose.state.rememberPresentationState
+import com.google.jetpackcamera.data.media.Media
+import com.google.jetpackcamera.data.media.MediaDescriptor
+private const val TAG = "PostCaptureScreen"
+
+@OptIn(UnstableApi::class)
@Composable
-fun PostCaptureScreen(viewModel: PostCaptureViewModel = hiltViewModel(), imageUri: Uri?) {
+fun PostCaptureScreen(
+ onRequestWindowColorMode: (Int) -> Unit = {},
+ viewModel: PostCaptureViewModel = hiltViewModel()
+) {
+ Log.d(TAG, "PostCaptureScreen")
+
val uiState: PostCaptureUiState by viewModel.uiState.collectAsState()
val context = LocalContext.current
- LaunchedEffect(imageUri) {
- viewModel.setLastCapturedImageUri(imageUri)
- }
-
Box(modifier = Modifier.fillMaxSize()) {
- uiState.imageUri?.let { uri ->
- val bitmap = remember(uri) {
- // TODO(yasith): Get the image rotation from the image
- loadAndRotateBitmap(context, uri, 270f)
- }
-
- if (bitmap != null) {
+ when (val media = uiState.media) {
+ is Media.Image -> {
+ val bitmap = media.bitmap
Canvas(modifier = Modifier.fillMaxSize()) {
drawIntoCanvas { canvas ->
val scale = maxOf(
@@ -90,10 +100,44 @@ fun PostCaptureScreen(viewModel: PostCaptureViewModel = hiltViewModel(), imageUr
}
}
}
- } ?: Text(
- text = "No Image Captured",
- modifier = Modifier.align(Alignment.Center)
- )
+ is Media.Video -> {
+ val presentationState = rememberPresentationState(viewModel.player)
+ PlayerSurface(
+ player = viewModel.player,
+ modifier = Modifier.resizeWithContentScale(
+ ContentScale.Fit,
+ presentationState.videoSizeDp
+ )
+ )
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
+ val capabilities =
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM) {
+ context.display.mode.supportedHdrTypes
+ } else {
+ context.display.hdrCapabilities?.supportedHdrTypes ?: intArrayOf()
+ }
+
+ if (capabilities.contains(HDR_TYPE_HLG)) {
+ Log.d(TAG, "HLG is supported, enabling")
+ onRequestWindowColorMode(ActivityInfo.COLOR_MODE_HDR)
+ }
+ }
+ viewModel.playVideo()
+ }
+ Media.None -> {
+ Text(
+ text = stringResource(R.string.no_media_available),
+ modifier = Modifier.align(Alignment.Center)
+ )
+ }
+ Media.Error -> {
+ Text(
+ text = stringResource(R.string.error_loading_media),
+ modifier = Modifier.align(Alignment.Center)
+ )
+ }
+ }
Row(
modifier = Modifier
@@ -104,7 +148,7 @@ fun PostCaptureScreen(viewModel: PostCaptureViewModel = hiltViewModel(), imageUr
) {
// Delete Image Button
IconButton(
- onClick = { viewModel.deleteImage(context.contentResolver) },
+ onClick = { viewModel.deleteMedia(context.contentResolver) },
modifier = Modifier
.size(56.dp)
.shadow(10.dp, CircleShape),
@@ -124,8 +168,14 @@ fun PostCaptureScreen(viewModel: PostCaptureViewModel = hiltViewModel(), imageUr
// Share Image Button
IconButton(
onClick = {
- imageUri?.let {
- shareImage(context, it)
+ val mediaDescriptor = uiState.mediaDescriptor
+
+ if (mediaDescriptor is MediaDescriptor.Image) {
+ shareImage(context, mediaDescriptor.uri)
+ }
+
+ if (mediaDescriptor is MediaDescriptor.Video) {
+ shareImage(context, mediaDescriptor.uri)
}
},
modifier = Modifier
diff --git a/feature/postcapture/src/main/java/com/google/jetpackcamera/feature/postcapture/PostCaptureViewModel.kt b/feature/postcapture/src/main/java/com/google/jetpackcamera/feature/postcapture/PostCaptureViewModel.kt
index a9ed0b890..0a6a95b98 100644
--- a/feature/postcapture/src/main/java/com/google/jetpackcamera/feature/postcapture/PostCaptureViewModel.kt
+++ b/feature/postcapture/src/main/java/com/google/jetpackcamera/feature/postcapture/PostCaptureViewModel.kt
@@ -16,30 +16,74 @@
package com.google.jetpackcamera.feature.postcapture
import android.content.ContentResolver
-import android.net.Uri
+import android.content.Context
import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import androidx.media3.common.MediaItem
+import androidx.media3.exoplayer.ExoPlayer
+import com.google.jetpackcamera.data.media.Media
+import com.google.jetpackcamera.data.media.MediaDescriptor
+import com.google.jetpackcamera.data.media.MediaRepository
import dagger.hilt.android.lifecycle.HiltViewModel
+import dagger.hilt.android.qualifiers.ApplicationContext
+import javax.inject.Inject
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update
+import kotlinx.coroutines.launch
@HiltViewModel
-class PostCaptureViewModel : ViewModel() {
+class PostCaptureViewModel @Inject constructor(
+ private val mediaRepository: MediaRepository,
+ @ApplicationContext private val context: Context
+) : ViewModel() {
+
+ init {
+ getLastCapture()
+ }
+
+ private val _uiState = MutableStateFlow(
+ PostCaptureUiState(
+ mediaDescriptor = MediaDescriptor.None,
+ media = Media.None
+ )
+ )
+
+ val player = ExoPlayer.Builder(context).build()
- private val _uiState = MutableStateFlow(PostCaptureUiState())
val uiState: StateFlow = _uiState
- fun setLastCapturedImageUri(imageUri: Uri?) {
- _uiState.update { it.copy(imageUri = imageUri, isImageDeleted = false) }
+ fun getLastCapture() {
+ viewModelScope.launch {
+ val mediaDescriptor = mediaRepository.getLastCapturedMedia()
+ val media = mediaRepository.load(mediaDescriptor)
+
+ _uiState.update { it.copy(mediaDescriptor = mediaDescriptor, media = media) }
+ }
+ }
+
+ fun deleteMedia(contentResolver: ContentResolver) {
+ when (val mediaDescriptor = uiState.value.mediaDescriptor) {
+ is MediaDescriptor.Image -> contentResolver.delete(mediaDescriptor.uri, null, null)
+ is MediaDescriptor.Video -> contentResolver.delete(mediaDescriptor.uri, null, null)
+ MediaDescriptor.None -> {}
+ }
+ _uiState.update { it.copy(mediaDescriptor = MediaDescriptor.None, media = Media.None) }
}
- fun deleteImage(contentResolver: ContentResolver) {
- contentResolver.delete(uiState.value.imageUri!!, null, null)
- _uiState.update { it.copy(imageUri = null, isImageDeleted = true) }
+ fun playVideo() {
+ val media = uiState.value.media
+ if (media is Media.Video) {
+ val mediaItem = MediaItem.fromUri(media.uri)
+ player.setMediaItem(mediaItem)
+ player.prepare()
+ player.setRepeatMode(ExoPlayer.REPEAT_MODE_ONE)
+ player.play()
+ }
}
}
data class PostCaptureUiState(
- val imageUri: Uri? = null,
- val isImageDeleted: Boolean = false
+ val mediaDescriptor: MediaDescriptor,
+ val media: Media
)
diff --git a/feature/postcapture/src/main/res/values/strings.xml b/feature/postcapture/src/main/res/values/strings.xml
new file mode 100644
index 000000000..8248ecc55
--- /dev/null
+++ b/feature/postcapture/src/main/res/values/strings.xml
@@ -0,0 +1,20 @@
+
+
+
+ No Media Available
+ Error loading media
+
\ No newline at end of file
diff --git a/feature/preview/build.gradle.kts b/feature/preview/build.gradle.kts
index abe853bfd..552f52459 100644
--- a/feature/preview/build.gradle.kts
+++ b/feature/preview/build.gradle.kts
@@ -140,9 +140,10 @@ dependencies {
implementation(libs.kotlinx.atomicfu)
// Project dependencies
- implementation(project(":data:settings"))
implementation(project(":core:camera"))
implementation(project(":core:common"))
+ implementation(project(":data:media"))
+ implementation(project(":data:settings"))
testImplementation(project(":core:common"))
}
diff --git a/feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/PreviewScreen.kt b/feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/PreviewScreen.kt
index 11140fa2d..39e12838a 100644
--- a/feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/PreviewScreen.kt
+++ b/feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/PreviewScreen.kt
@@ -52,7 +52,6 @@ import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.LifecycleStartEffect
import androidx.tracing.Trace
import com.google.jetpackcamera.core.camera.VideoRecordingState
-import com.google.jetpackcamera.core.common.getLastImageUri
import com.google.jetpackcamera.feature.preview.quicksettings.QuickSettingsScreenOverlay
import com.google.jetpackcamera.feature.preview.ui.CameraControlsOverlay
import com.google.jetpackcamera.feature.preview.ui.PreviewDisplay
@@ -82,7 +81,7 @@ private const val TAG = "PreviewScreen"
@Composable
fun PreviewScreen(
onNavigateToSettings: () -> Unit,
- onNavigateToPostCapture: (uri: Uri?) -> Unit,
+ onNavigateToPostCapture: () -> Unit,
previewMode: PreviewMode,
isDebugMode: Boolean,
modifier: Modifier = Modifier,
@@ -163,15 +162,11 @@ fun PreviewScreen(
onRequestWindowColorMode = onRequestWindowColorMode,
onSnackBarResult = viewModel::onSnackBarResult,
isDebugMode = isDebugMode,
- onImageWellClick = { uri -> onNavigateToPostCapture(uri) }
+ onImageWellClick = onNavigateToPostCapture
)
- // TODO(yasith): Remove and use ImageRepository after implementing
LaunchedEffect(Unit) {
- val lastCapturedImageUri = getLastImageUri(context)
- lastCapturedImageUri?.let { uri ->
- viewModel.updateLastCapturedImageUri(uri)
- }
+ viewModel.updateLastCapturedMedia()
}
}
}
@@ -217,7 +212,7 @@ private fun ContentScreen(
onRequestWindowColorMode: (Int) -> Unit = {},
onSnackBarResult: (String) -> Unit = {},
isDebugMode: Boolean = false,
- onImageWellClick: (uri: Uri?) -> Unit = {}
+ onImageWellClick: () -> Unit = {}
) {
val snackbarHostState = remember { SnackbarHostState() }
Scaffold(
diff --git a/feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/PreviewUiState.kt b/feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/PreviewUiState.kt
index 9b097e669..558f04a03 100644
--- a/feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/PreviewUiState.kt
+++ b/feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/PreviewUiState.kt
@@ -57,7 +57,7 @@ sealed interface PreviewUiState {
val audioUiState: AudioUiState = AudioUiState.Disabled,
val elapsedTimeUiState: ElapsedTimeUiState = ElapsedTimeUiState.Unavailable,
val captureButtonUiState: CaptureButtonUiState = CaptureButtonUiState.Unavailable,
- val imageWellUiState: ImageWellUiState = ImageWellUiState.NoPreviousCapture
+ val imageWellUiState: ImageWellUiState = ImageWellUiState.Unavailable
) : PreviewUiState
}
diff --git a/feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/PreviewViewModel.kt b/feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/PreviewViewModel.kt
index 12deedf40..3aadcfd71 100644
--- a/feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/PreviewViewModel.kt
+++ b/feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/PreviewViewModel.kt
@@ -29,6 +29,7 @@ import com.google.jetpackcamera.core.camera.CameraState
import com.google.jetpackcamera.core.camera.CameraUseCase
import com.google.jetpackcamera.core.camera.VideoRecordingState
import com.google.jetpackcamera.core.common.traceFirstFramePreview
+import com.google.jetpackcamera.data.media.MediaRepository
import com.google.jetpackcamera.feature.preview.ui.IMAGE_CAPTURE_EXTERNAL_UNSUPPORTED_TAG
import com.google.jetpackcamera.feature.preview.ui.IMAGE_CAPTURE_FAILURE_TAG
import com.google.jetpackcamera.feature.preview.ui.IMAGE_CAPTURE_SUCCESS_TAG
@@ -92,7 +93,8 @@ class PreviewViewModel @AssistedInject constructor(
@Assisted val isDebugMode: Boolean,
private val cameraUseCase: CameraUseCase,
private val settingsRepository: SettingsRepository,
- private val constraintsRepository: ConstraintsRepository
+ private val constraintsRepository: ConstraintsRepository,
+ private val mediaRepository: MediaRepository
) : ViewModel() {
private val _previewUiState: MutableStateFlow =
MutableStateFlow(PreviewUiState.NotReady)
@@ -237,11 +239,15 @@ class PreviewViewModel @AssistedInject constructor(
}
}
- fun updateLastCapturedImageUri(uri: Uri) {
+ fun updateLastCapturedMedia() {
viewModelScope.launch {
+ val lastCapturedMediaDescriptor = mediaRepository.getLastCapturedMedia()
_previewUiState.update { old ->
- (old as PreviewUiState.Ready)
- .copy(imageWellUiState = ImageWellUiState.LastCapture(uri))
+ (old as PreviewUiState.Ready).copy(
+ imageWellUiState = ImageWellUiState.LastCapture(
+ mediaDescriptor = lastCapturedMediaDescriptor
+ )
+ ) ?: old
}
}
}
@@ -745,9 +751,7 @@ class PreviewViewModel @AssistedInject constructor(
}, contentResolver, finalImageUri, ignoreUri).savedUri
},
onSuccess = { savedUri ->
- savedUri?.let {
- updateLastCapturedImageUri(it)
- }
+ updateLastCapturedMedia()
onImageCapture(ImageCaptureEvent.ImageSaved(savedUri), uriIndex)
},
onFailure = { exception ->
@@ -866,6 +870,7 @@ class PreviewViewModel @AssistedInject constructor(
withDismissAction = true,
testTag = VIDEO_CAPTURE_SUCCESS_TAG
)
+ updateLastCapturedMedia()
}
is CameraUseCase.OnVideoRecordEvent.OnVideoRecordError -> {
diff --git a/feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/ui/CameraControlsOverlay.kt b/feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/ui/CameraControlsOverlay.kt
index f9516d286..a2e1e811d 100644
--- a/feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/ui/CameraControlsOverlay.kt
+++ b/feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/ui/CameraControlsOverlay.kt
@@ -118,7 +118,7 @@ fun CameraControlsOverlay(
(PreviewViewModel.VideoCaptureEvent) -> Unit
) -> Unit = { _, _, _ -> },
onStopVideoRecording: () -> Unit = {},
- onImageWellClick: (uri: Uri?) -> Unit = {},
+ onImageWellClick: () -> Unit = {},
onLockVideoRecording: (Boolean) -> Unit
) {
// Show the current zoom level for a short period of time, only when the level changes.
@@ -284,7 +284,7 @@ private fun ControlsBottom(
(PreviewViewModel.VideoCaptureEvent) -> Unit
) -> Unit = { _, _, _ -> },
onStopVideoRecording: () -> Unit = {},
- onImageWellClick: (uri: Uri?) -> Unit = {},
+ onImageWellClick: () -> Unit = {},
onLockVideoRecording: (Boolean) -> Unit = {}
) {
Column(
diff --git a/feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/ui/ImageWell.kt b/feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/ui/ImageWell.kt
index 504fec2df..56995c6cb 100644
--- a/feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/ui/ImageWell.kt
+++ b/feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/ui/ImageWell.kt
@@ -16,7 +16,6 @@
package com.google.jetpackcamera.feature.preview.ui
import android.graphics.RectF
-import android.net.Uri
import androidx.compose.animation.AnimatedContent
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.border
@@ -31,22 +30,23 @@ import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.drawscope.drawIntoCanvas
import androidx.compose.ui.graphics.nativeCanvas
-import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
-import com.google.jetpackcamera.core.common.loadAndRotateBitmap
+import com.google.jetpackcamera.data.media.MediaDescriptor
import kotlin.math.min
@Composable
fun ImageWell(
modifier: Modifier = Modifier,
- imageWellUiState: ImageWellUiState = ImageWellUiState.NoPreviousCapture,
- onClick: (uri: Uri?) -> Unit
+ imageWellUiState: ImageWellUiState = ImageWellUiState.Unavailable,
+ onClick: () -> Unit
) {
- val context = LocalContext.current
-
when (imageWellUiState) {
is ImageWellUiState.LastCapture -> {
- val bitmap = loadAndRotateBitmap(context, imageWellUiState.uri, 270f)
+ val bitmap = when (imageWellUiState.mediaDescriptor) {
+ is MediaDescriptor.Image -> imageWellUiState.mediaDescriptor.thumbnail
+ is MediaDescriptor.Video -> imageWellUiState.mediaDescriptor.thumbnail
+ is MediaDescriptor.None -> null
+ }
bitmap?.let {
Box(
@@ -55,7 +55,7 @@ fun ImageWell(
.padding(18.dp)
.border(2.dp, Color.White, RoundedCornerShape(16.dp))
.clip(RoundedCornerShape(16.dp))
- .clickable(onClick = { onClick(imageWellUiState.uri) })
+ .clickable(onClick = onClick)
) {
AnimatedContent(
targetState = bitmap
@@ -96,14 +96,14 @@ fun ImageWell(
}
}
- is ImageWellUiState.NoPreviousCapture -> {
+ is ImageWellUiState.Unavailable -> {
}
}
}
// TODO(yasith): Add support for Video
sealed interface ImageWellUiState {
- data object NoPreviousCapture : ImageWellUiState
+ data object Unavailable : ImageWellUiState
- data class LastCapture(val uri: Uri) : ImageWellUiState
+ data class LastCapture(val mediaDescriptor: MediaDescriptor) : ImageWellUiState
}
diff --git a/feature/preview/src/test/java/com/google/jetpackcamera/feature/preview/PreviewViewModelTest.kt b/feature/preview/src/test/java/com/google/jetpackcamera/feature/preview/PreviewViewModelTest.kt
index a6528964e..fa8dc1aba 100644
--- a/feature/preview/src/test/java/com/google/jetpackcamera/feature/preview/PreviewViewModelTest.kt
+++ b/feature/preview/src/test/java/com/google/jetpackcamera/feature/preview/PreviewViewModelTest.kt
@@ -18,6 +18,7 @@ package com.google.jetpackcamera.feature.preview
import android.content.ContentResolver
import com.google.common.truth.Truth.assertThat
import com.google.jetpackcamera.core.camera.test.FakeCameraUseCase
+import com.google.jetpackcamera.data.media.FakeMediaRepository
import com.google.jetpackcamera.settings.SettableConstraintsRepositoryImpl
import com.google.jetpackcamera.settings.model.FlashMode
import com.google.jetpackcamera.settings.model.LensFacing
@@ -54,7 +55,8 @@ class PreviewViewModelTest {
false,
cameraUseCase = cameraUseCase,
constraintsRepository = constraintsRepository,
- settingsRepository = FakeSettingsRepository
+ settingsRepository = FakeSettingsRepository,
+ mediaRepository = FakeMediaRepository
)
advanceUntilIdle()
}
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 97ecfb251..4aee2b787 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -27,6 +27,7 @@ androidxDatastore = "1.1.1"
androidxGraphicsCore = "1.0.2"
androidxHiltNavigationCompose = "1.2.0"
androidxLifecycle = "2.8.7"
+androidxMedia3 = "1.6.0-rc01"
androidxNavigationCompose = "2.8.4"
androidxProfileinstaller = "1.4.1"
androidxTestEspresso = "3.6.1"
@@ -58,6 +59,8 @@ androidx-junit = { module = "androidx.test.ext:junit", version.ref = "androidxTe
androidx-lifecycle-livedata = { module = "androidx.lifecycle:lifecycle-livedata-ktx", version.ref = "androidxLifecycle" }
androidx-lifecycle-viewmodel-compose = { module = "androidx.lifecycle:lifecycle-viewmodel-compose", version.ref = "androidxLifecycle" }
androidx-lifecycle-runtime-compose = { module = "androidx.lifecycle:lifecycle-runtime-compose", version.ref = "androidxLifecycle" }
+androidx-media3-exoplayer = { module = "androidx.media3:media3-exoplayer", version.ref = "androidxMedia3" }
+androidx-media3-ui-compose = { module = "androidx.media3:media3-ui-compose", version.ref = "androidxMedia3" }
androidx-navigation-compose = { module = "androidx.navigation:navigation-compose", version.ref = "androidxNavigationCompose" }
androidx-orchestrator = { module = "androidx.test:orchestrator", version.ref = "orchestrator" }
androidx-profileinstaller = { module = "androidx.profileinstaller:profileinstaller", version.ref = "androidxProfileinstaller" }
diff --git a/settings.gradle.kts b/settings.gradle.kts
index 96740cfec..cb095531e 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -38,6 +38,7 @@ include(":feature:preview")
include(":core:camera")
include(":feature:settings")
include(":data:settings")
+include(":data:media")
include(":core:common")
include(":benchmark")
include(":feature:permissions")