diff --git a/app/src/androidTest/java/com/google/jetpackcamera/BackgroundDeviceTest.kt b/app/src/androidTest/java/com/google/jetpackcamera/BackgroundDeviceTest.kt index 747650f0..7ae4498b 100644 --- a/app/src/androidTest/java/com/google/jetpackcamera/BackgroundDeviceTest.kt +++ b/app/src/androidTest/java/com/google/jetpackcamera/BackgroundDeviceTest.kt @@ -34,8 +34,8 @@ import com.google.jetpackcamera.feature.preview.quicksettings.ui.QUICK_SETTINGS_ import com.google.jetpackcamera.feature.preview.quicksettings.ui.QUICK_SETTINGS_RATIO_1_1_BUTTON import com.google.jetpackcamera.feature.preview.quicksettings.ui.QUICK_SETTINGS_RATIO_BUTTON import com.google.jetpackcamera.feature.preview.ui.CAPTURE_BUTTON -import com.google.jetpackcamera.utils.APP_REQUIRED_PERMISSIONS import com.google.jetpackcamera.utils.APP_START_TIMEOUT_MILLIS +import com.google.jetpackcamera.utils.TEST_REQUIRED_PERMISSIONS import com.google.jetpackcamera.utils.runScenarioTest import org.junit.Before import org.junit.Rule @@ -46,7 +46,7 @@ import org.junit.runner.RunWith class BackgroundDeviceTest { @get:Rule val permissionsRule: GrantPermissionRule = - GrantPermissionRule.grant(*(APP_REQUIRED_PERMISSIONS).toTypedArray()) + GrantPermissionRule.grant(*(TEST_REQUIRED_PERMISSIONS).toTypedArray()) @get:Rule val composeTestRule = createEmptyComposeRule() diff --git a/app/src/androidTest/java/com/google/jetpackcamera/FlashDeviceTest.kt b/app/src/androidTest/java/com/google/jetpackcamera/FlashDeviceTest.kt index 0e57a00c..e65a77ca 100644 --- a/app/src/androidTest/java/com/google/jetpackcamera/FlashDeviceTest.kt +++ b/app/src/androidTest/java/com/google/jetpackcamera/FlashDeviceTest.kt @@ -35,9 +35,9 @@ import com.google.jetpackcamera.feature.preview.ui.FLIP_CAMERA_BUTTON import com.google.jetpackcamera.feature.preview.ui.IMAGE_CAPTURE_SUCCESS_TAG import com.google.jetpackcamera.feature.preview.ui.SCREEN_FLASH_OVERLAY import com.google.jetpackcamera.settings.model.LensFacing -import com.google.jetpackcamera.utils.APP_REQUIRED_PERMISSIONS import com.google.jetpackcamera.utils.APP_START_TIMEOUT_MILLIS import com.google.jetpackcamera.utils.IMAGE_CAPTURE_TIMEOUT_MILLIS +import com.google.jetpackcamera.utils.TEST_REQUIRED_PERMISSIONS import com.google.jetpackcamera.utils.assume import com.google.jetpackcamera.utils.getCurrentLensFacing import com.google.jetpackcamera.utils.onNodeWithContentDescription @@ -52,7 +52,7 @@ internal class FlashDeviceTest { @get:Rule val permissionsRule: GrantPermissionRule = - GrantPermissionRule.grant(*(APP_REQUIRED_PERMISSIONS).toTypedArray()) + GrantPermissionRule.grant(*(TEST_REQUIRED_PERMISSIONS).toTypedArray()) @get:Rule val composeTestRule = createEmptyComposeRule() diff --git a/app/src/androidTest/java/com/google/jetpackcamera/ImageCaptureDeviceTest.kt b/app/src/androidTest/java/com/google/jetpackcamera/ImageCaptureDeviceTest.kt index ff036c11..7cbfbd4e 100644 --- a/app/src/androidTest/java/com/google/jetpackcamera/ImageCaptureDeviceTest.kt +++ b/app/src/androidTest/java/com/google/jetpackcamera/ImageCaptureDeviceTest.kt @@ -34,17 +34,16 @@ import com.google.jetpackcamera.feature.preview.ui.CAPTURE_BUTTON import com.google.jetpackcamera.feature.preview.ui.IMAGE_CAPTURE_FAILURE_TAG import com.google.jetpackcamera.feature.preview.ui.IMAGE_CAPTURE_SUCCESS_TAG import com.google.jetpackcamera.feature.preview.ui.VIDEO_CAPTURE_EXTERNAL_UNSUPPORTED_TAG -import com.google.jetpackcamera.utils.APP_REQUIRED_PERMISSIONS import com.google.jetpackcamera.utils.APP_START_TIMEOUT_MILLIS import com.google.jetpackcamera.utils.IMAGE_CAPTURE_TIMEOUT_MILLIS +import com.google.jetpackcamera.utils.TEST_REQUIRED_PERMISSIONS import com.google.jetpackcamera.utils.VIDEO_CAPTURE_TIMEOUT_MILLIS import com.google.jetpackcamera.utils.deleteFilesInDirAfterTimestamp import com.google.jetpackcamera.utils.doesImageFileExist import com.google.jetpackcamera.utils.getIntent import com.google.jetpackcamera.utils.getTestUri -import com.google.jetpackcamera.utils.runScenarioTest +import com.google.jetpackcamera.utils.runMediaStoreAutoDeleteScenarioTest import com.google.jetpackcamera.utils.runScenarioTestForResult -import java.io.File import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith @@ -55,7 +54,7 @@ internal class ImageCaptureDeviceTest { @get:Rule val permissionsRule: GrantPermissionRule = - GrantPermissionRule.grant(*(APP_REQUIRED_PERMISSIONS).toTypedArray()) + GrantPermissionRule.grant(*(TEST_REQUIRED_PERMISSIONS).toTypedArray()) @get:Rule val composeTestRule = createEmptyComposeRule() @@ -64,8 +63,10 @@ internal class ImageCaptureDeviceTest { private val uiDevice = UiDevice.getInstance(instrumentation) @Test - fun image_capture() = runScenarioTest { - val timeStamp = System.currentTimeMillis() + fun image_capture() = runMediaStoreAutoDeleteScenarioTest( + mediaUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI, + filePrefix = "JCA" + ) { // Wait for the capture button to be displayed composeTestRule.waitUntil(timeoutMillis = APP_START_TIMEOUT_MILLIS) { composeTestRule.onNodeWithTag(CAPTURE_BUTTON).isDisplayed() @@ -77,8 +78,6 @@ internal class ImageCaptureDeviceTest { composeTestRule.waitUntil(timeoutMillis = IMAGE_CAPTURE_TIMEOUT_MILLIS) { composeTestRule.onNodeWithTag(IMAGE_CAPTURE_SUCCESS_TAG).isDisplayed() } - Truth.assertThat(File(DIR_PATH).lastModified() > timeStamp).isTrue() - deleteFilesInDirAfterTimestamp(DIR_PATH, instrumentation, timeStamp) } @Test diff --git a/app/src/androidTest/java/com/google/jetpackcamera/NavigationTest.kt b/app/src/androidTest/java/com/google/jetpackcamera/NavigationTest.kt index b3e82ab5..f85f2440 100644 --- a/app/src/androidTest/java/com/google/jetpackcamera/NavigationTest.kt +++ b/app/src/androidTest/java/com/google/jetpackcamera/NavigationTest.kt @@ -32,8 +32,8 @@ import com.google.jetpackcamera.feature.preview.ui.FLIP_CAMERA_BUTTON import com.google.jetpackcamera.feature.preview.ui.SETTINGS_BUTTON import com.google.jetpackcamera.settings.R import com.google.jetpackcamera.settings.ui.BACK_BUTTON -import com.google.jetpackcamera.utils.APP_REQUIRED_PERMISSIONS import com.google.jetpackcamera.utils.APP_START_TIMEOUT_MILLIS +import com.google.jetpackcamera.utils.TEST_REQUIRED_PERMISSIONS import com.google.jetpackcamera.utils.assume import com.google.jetpackcamera.utils.onNodeWithText import com.google.jetpackcamera.utils.runScenarioTest @@ -45,7 +45,7 @@ import org.junit.runner.RunWith class NavigationTest { @get:Rule val permissionsRule: GrantPermissionRule = - GrantPermissionRule.grant(*(APP_REQUIRED_PERMISSIONS).toTypedArray()) + GrantPermissionRule.grant(*(TEST_REQUIRED_PERMISSIONS).toTypedArray()) @get:Rule val composeTestRule = createEmptyComposeRule() diff --git a/app/src/androidTest/java/com/google/jetpackcamera/SwitchCameraTest.kt b/app/src/androidTest/java/com/google/jetpackcamera/SwitchCameraTest.kt index 5d732a9e..9be463cb 100644 --- a/app/src/androidTest/java/com/google/jetpackcamera/SwitchCameraTest.kt +++ b/app/src/androidTest/java/com/google/jetpackcamera/SwitchCameraTest.kt @@ -32,7 +32,7 @@ import com.google.jetpackcamera.feature.preview.quicksettings.ui.QUICK_SETTINGS_ import com.google.jetpackcamera.feature.preview.ui.FLIP_CAMERA_BUTTON import com.google.jetpackcamera.feature.preview.ui.PREVIEW_DISPLAY import com.google.jetpackcamera.settings.model.LensFacing -import com.google.jetpackcamera.utils.APP_REQUIRED_PERMISSIONS +import com.google.jetpackcamera.utils.TEST_REQUIRED_PERMISSIONS import com.google.jetpackcamera.utils.assume import com.google.jetpackcamera.utils.getCurrentLensFacing import com.google.jetpackcamera.utils.runScenarioTest @@ -44,7 +44,7 @@ import org.junit.runner.RunWith class SwitchCameraTest { @get:Rule val permissionsRule: GrantPermissionRule = - GrantPermissionRule.grant(*(APP_REQUIRED_PERMISSIONS).toTypedArray()) + GrantPermissionRule.grant(*(TEST_REQUIRED_PERMISSIONS).toTypedArray()) @get:Rule val composeTestRule = createEmptyComposeRule() diff --git a/app/src/androidTest/java/com/google/jetpackcamera/VideoAudioTest.kt b/app/src/androidTest/java/com/google/jetpackcamera/VideoAudioTest.kt index 0cfbd731..3af4d125 100644 --- a/app/src/androidTest/java/com/google/jetpackcamera/VideoAudioTest.kt +++ b/app/src/androidTest/java/com/google/jetpackcamera/VideoAudioTest.kt @@ -30,8 +30,8 @@ import androidx.test.uiautomator.Until import com.google.common.truth.Truth.assertThat import com.google.jetpackcamera.feature.preview.ui.AMPLITUDE_HOT_TAG import com.google.jetpackcamera.feature.preview.ui.CAPTURE_BUTTON -import com.google.jetpackcamera.utils.APP_REQUIRED_PERMISSIONS import com.google.jetpackcamera.utils.APP_START_TIMEOUT_MILLIS +import com.google.jetpackcamera.utils.TEST_REQUIRED_PERMISSIONS import com.google.jetpackcamera.utils.runScenarioTest import org.junit.Before import org.junit.Rule @@ -43,7 +43,7 @@ import org.junit.runner.RunWith class VideoAudioTest { @get:Rule val permissionsRule: GrantPermissionRule = - GrantPermissionRule.grant(*(APP_REQUIRED_PERMISSIONS).toTypedArray()) + GrantPermissionRule.grant(*(TEST_REQUIRED_PERMISSIONS).toTypedArray()) @get:Rule val composeTestRule = createEmptyComposeRule() diff --git a/app/src/androidTest/java/com/google/jetpackcamera/VideoRecordingDeviceTest.kt b/app/src/androidTest/java/com/google/jetpackcamera/VideoRecordingDeviceTest.kt index e51ed84e..41a910e7 100644 --- a/app/src/androidTest/java/com/google/jetpackcamera/VideoRecordingDeviceTest.kt +++ b/app/src/androidTest/java/com/google/jetpackcamera/VideoRecordingDeviceTest.kt @@ -34,18 +34,17 @@ import com.google.jetpackcamera.feature.preview.ui.CAPTURE_BUTTON import com.google.jetpackcamera.feature.preview.ui.IMAGE_CAPTURE_EXTERNAL_UNSUPPORTED_TAG import com.google.jetpackcamera.feature.preview.ui.VIDEO_CAPTURE_FAILURE_TAG import com.google.jetpackcamera.feature.preview.ui.VIDEO_CAPTURE_SUCCESS_TAG -import com.google.jetpackcamera.utils.APP_REQUIRED_PERMISSIONS import com.google.jetpackcamera.utils.APP_START_TIMEOUT_MILLIS import com.google.jetpackcamera.utils.IMAGE_CAPTURE_TIMEOUT_MILLIS +import com.google.jetpackcamera.utils.TEST_REQUIRED_PERMISSIONS import com.google.jetpackcamera.utils.VIDEO_CAPTURE_TIMEOUT_MILLIS import com.google.jetpackcamera.utils.VIDEO_DURATION_MILLIS import com.google.jetpackcamera.utils.deleteFilesInDirAfterTimestamp import com.google.jetpackcamera.utils.doesImageFileExist import com.google.jetpackcamera.utils.getIntent import com.google.jetpackcamera.utils.getTestUri -import com.google.jetpackcamera.utils.runScenarioTest +import com.google.jetpackcamera.utils.runMediaStoreAutoDeleteScenarioTest import com.google.jetpackcamera.utils.runScenarioTestForResult -import java.io.File import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith @@ -54,7 +53,7 @@ import org.junit.runner.RunWith internal class VideoRecordingDeviceTest { @get:Rule val permissionsRule: GrantPermissionRule = - GrantPermissionRule.grant(*(APP_REQUIRED_PERMISSIONS).toTypedArray()) + GrantPermissionRule.grant(*(TEST_REQUIRED_PERMISSIONS).toTypedArray()) @get:Rule val composeTestRule = createEmptyComposeRule() @@ -63,7 +62,9 @@ internal class VideoRecordingDeviceTest { private val uiDevice = UiDevice.getInstance(instrumentation) @Test - fun video_capture() = runScenarioTest { + fun video_capture(): Unit = runMediaStoreAutoDeleteScenarioTest( + mediaUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI + ) { val timeStamp = System.currentTimeMillis() // Wait for the capture button to be displayed composeTestRule.waitUntil(timeoutMillis = APP_START_TIMEOUT_MILLIS) { @@ -73,8 +74,6 @@ internal class VideoRecordingDeviceTest { composeTestRule.waitUntil(timeoutMillis = VIDEO_CAPTURE_TIMEOUT_MILLIS) { composeTestRule.onNodeWithTag(VIDEO_CAPTURE_SUCCESS_TAG).isDisplayed() } - Truth.assertThat(File(DIR_PATH).lastModified() > timeStamp).isTrue() - deleteFilesInDirAfterTimestamp(DIR_PATH, instrumentation, timeStamp) } @Test diff --git a/app/src/androidTest/java/com/google/jetpackcamera/utils/AppTestUtil.kt b/app/src/androidTest/java/com/google/jetpackcamera/utils/AppTestUtil.kt index 0d1e8d6a..c6cd707c 100644 --- a/app/src/androidTest/java/com/google/jetpackcamera/utils/AppTestUtil.kt +++ b/app/src/androidTest/java/com/google/jetpackcamera/utils/AppTestUtil.kt @@ -15,12 +15,124 @@ */ package com.google.jetpackcamera.utils +import android.app.Instrumentation +import android.database.ContentObserver +import android.database.Cursor +import android.net.Uri import android.os.Build +import android.provider.BaseColumns +import android.provider.MediaStore +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.callbackFlow +import kotlinx.coroutines.flow.transform -val APP_REQUIRED_PERMISSIONS: List = buildList { +private val APP_REQUIRED_PERMISSIONS: List = buildList { add(android.Manifest.permission.CAMERA) add(android.Manifest.permission.RECORD_AUDIO) if (Build.VERSION.SDK_INT <= 28) { add(android.Manifest.permission.WRITE_EXTERNAL_STORAGE) } } + +val TEST_REQUIRED_PERMISSIONS: List = buildList { + addAll(APP_REQUIRED_PERMISSIONS) + if (Build.VERSION.SDK_INT >= 33) { + add(android.Manifest.permission.READ_MEDIA_IMAGES) + add(android.Manifest.permission.READ_MEDIA_VIDEO) + } +} + +fun mediaStoreInsertedFlow( + mediaUri: Uri, + instrumentation: Instrumentation, + filePrefix: String = "" +): Flow> = with(instrumentation.targetContext.contentResolver) { + // Creates a map of the display names and corresponding URIs for all files contained within + // the URI argument. If the URI is a single file, the map will contain a single file. + // On API 29+, this will also only return files that are not "pending". Pending files + // have not yet been fully written. + fun queryWrittenFiles(uri: Uri): Map { + return buildMap { + query( + uri, + buildList { + add(BaseColumns._ID) + add(MediaStore.MediaColumns.DISPLAY_NAME) + if (Build.VERSION.SDK_INT >= 29) { + add(MediaStore.MediaColumns.IS_PENDING) + } + }.toTypedArray(), + null, + null, + null + )?.use { cursor: Cursor -> + cursor.moveToFirst() + val idCol = cursor.getColumnIndex(BaseColumns._ID) + val displayNameCol = cursor.getColumnIndex(MediaStore.MediaColumns.DISPLAY_NAME) + + while (!cursor.isAfterLast) { + val id = cursor.getLong(idCol) + val displayName = cursor.getString(displayNameCol) + val isPending = if (Build.VERSION.SDK_INT >= 29) { + cursor.getInt(cursor.getColumnIndex(MediaStore.MediaColumns.IS_PENDING)) + } else { + // On devices pre-API 29, we don't have an is_pending column, so never + // say that the file is pending + 0 + } + if (isPending == 0 && + (filePrefix.isEmpty() || displayName.startsWith(filePrefix)) + ) { + // Construct URI for a single file + val outputUri = if (uri.lastPathSegment?.equals("$id") == false) { + uri.buildUpon().appendPath("$id").build() + } else { + uri + } + put(displayName, outputUri) + } + cursor.moveToNext() + } + } + } + } + + // Get the full list of initially written files. We'll append files to this as we + // publish them. + val existingFiles = queryWrittenFiles(mediaUri).toMutableMap() + return callbackFlow { + val observer = object : ContentObserver(null) { + override fun onChange(selfChange: Boolean) { + onChange(selfChange, null) + } + + override fun onChange(selfChange: Boolean, uri: Uri?) { + onChange(selfChange, uri, 0) + } + + override fun onChange(selfChange: Boolean, uri: Uri?, flags: Int) { + onChange(selfChange, uri?.let { setOf(it) } ?: emptySet(), flags) + } + + override fun onChange(selfChange: Boolean, uris: Collection, flags: Int) { + uris.forEach { uri -> + queryWrittenFiles(uri).forEach { + trySend(it) + } + } + } + } + + registerContentObserver(mediaUri, true, observer) + + awaitClose { + unregisterContentObserver(observer) + } + }.transform { + if (!existingFiles.containsKey(it.key)) { + existingFiles[it.key] = it.value + emit(it.toPair()) + } + } +} diff --git a/app/src/androidTest/java/com/google/jetpackcamera/utils/UiTestUtil.kt b/app/src/androidTest/java/com/google/jetpackcamera/utils/UiTestUtil.kt index cdf4ea42..c5f973b6 100644 --- a/app/src/androidTest/java/com/google/jetpackcamera/utils/UiTestUtil.kt +++ b/app/src/androidTest/java/com/google/jetpackcamera/utils/UiTestUtil.kt @@ -21,6 +21,7 @@ import android.content.ComponentName import android.content.Intent import android.net.Uri import android.provider.MediaStore +import android.util.Log import androidx.compose.ui.semantics.SemanticsProperties import androidx.compose.ui.test.isDisplayed import androidx.compose.ui.test.junit4.ComposeTestRule @@ -28,6 +29,8 @@ import androidx.compose.ui.test.onNodeWithTag import androidx.compose.ui.test.performClick import androidx.lifecycle.Lifecycle import androidx.test.core.app.ActivityScenario +import androidx.test.platform.app.InstrumentationRegistry +import com.google.common.truth.Truth.assertWithMessage import com.google.jetpackcamera.MainActivity import com.google.jetpackcamera.feature.preview.R import com.google.jetpackcamera.feature.preview.quicksettings.ui.QUICK_SETTINGS_FLIP_CAMERA_BUTTON @@ -35,16 +38,86 @@ import com.google.jetpackcamera.settings.model.LensFacing import java.io.File import java.net.URLConnection import java.util.concurrent.TimeoutException +import kotlin.coroutines.CoroutineContext import kotlin.time.Duration import kotlin.time.Duration.Companion.seconds +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.NonCancellable +import kotlinx.coroutines.cancelAndJoin import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.take +import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.withContext import kotlinx.coroutines.withTimeoutOrNull const val APP_START_TIMEOUT_MILLIS = 10_000L const val IMAGE_CAPTURE_TIMEOUT_MILLIS = 5_000L const val VIDEO_CAPTURE_TIMEOUT_MILLIS = 5_000L const val VIDEO_DURATION_MILLIS = 2_000L +inline fun runMediaStoreAutoDeleteScenarioTest( + mediaUri: Uri, + filePrefix: String = "", + expectedNumFiles: Int = 1, + fileWaitTimeoutMs: Duration = 10.seconds, + fileObserverContext: CoroutineContext = Dispatchers.IO, + crossinline block: ActivityScenario.() -> Unit +) = runBlocking { + val debugTag = "MediaStoreAutoDelete" + val instrumentation = InstrumentationRegistry.getInstrumentation() + val insertedMediaStoreEntries = mutableMapOf() + val observeFilesJob = launch(fileObserverContext) { + mediaStoreInsertedFlow( + mediaUri = mediaUri, + instrumentation = instrumentation, + filePrefix = filePrefix + ).take(expectedNumFiles) + .collect { + Log.d(debugTag, "Discovered new media store file: ${it.first}") + insertedMediaStoreEntries[it.first] = it.second + } + } + + var succeeded = false + try { + runScenarioTest(block = block) + succeeded = true + } finally { + withContext(NonCancellable) { + if (!succeeded || + withTimeoutOrNull(fileWaitTimeoutMs) { + // Wait for normal completion with timeout + observeFilesJob.join() + } == null + ) { + // If the test didn't succeed, or we've timed out waiting for files, + // cancel file observer and ensure job is complete + observeFilesJob.cancelAndJoin() + } + + val detectedNumFiles = insertedMediaStoreEntries.size + // Delete all inserted files that we know about at this point + insertedMediaStoreEntries.forEach { + Log.d(debugTag, "Deleting media store file: $it") + val deletedRows = instrumentation.targetContext.contentResolver.delete( + it.value, + null, + null + ) + if (deletedRows > 0) { + Log.d(debugTag, "Deleted $deletedRows files") + } else { + Log.e(debugTag, "Failed to delete ${it.key}") + } + } + + if (succeeded) { + assertWithMessage("Expected number of saved files does not match detected number") + .that(detectedNumFiles).isEqualTo(expectedNumFiles) + } + } + } +} inline fun runScenarioTest( crossinline block: ActivityScenario.() -> Unit @@ -136,13 +209,13 @@ fun deleteFilesInDirAfterTimestamp( timeStamp: Long ): Boolean { var hasDeletedFile = false - for (file in File(directoryPath).listFiles()) { + for (file in File(directoryPath).listFiles() ?: emptyArray()) { if (file.lastModified() >= timeStamp) { file.delete() if (file.exists()) { - file.getCanonicalFile().delete() + file.canonicalFile.delete() if (file.exists()) { - instrumentation.targetContext.applicationContext.deleteFile(file.getName()) + instrumentation.targetContext.applicationContext.deleteFile(file.name) } } hasDeletedFile = true @@ -152,8 +225,8 @@ fun deleteFilesInDirAfterTimestamp( } fun doesImageFileExist(uri: Uri, prefix: String): Boolean { - val file = File(uri.path) - if (file.exists()) { + val file = uri.path?.let { File(it) } + if (file?.exists() == true) { val mimeType = URLConnection.guessContentTypeFromName(uri.path) return mimeType != null && mimeType.startsWith(prefix) } diff --git a/app/src/debug/AndroidManifest.xml b/app/src/debug/AndroidManifest.xml new file mode 100644 index 00000000..658a6d24 --- /dev/null +++ b/app/src/debug/AndroidManifest.xml @@ -0,0 +1,30 @@ + + + + + + + + \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index d5bfb5e6..f869f0fa 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -33,7 +33,7 @@ androidxProfileinstaller = "1.3.1" androidxTestEspresso = "3.5.1" androidxTestJunit = "1.1.5" androidxTestMonitor = "1.6.1" -androidxTestRules = "1.5.0" +androidxTestRules = "1.6.1" androidxTestUiautomator = "2.3.0" androidxTracing = "1.2.0" cmake = "3.22.1"