Merge "Add support for weights to ArcLayout" into androidx-main
diff --git a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/CameraXActivityTestExtensions.kt b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/CameraXActivityTestExtensions.kt
index 7f594e7..4bab0a9 100644
--- a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/CameraXActivityTestExtensions.kt
+++ b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/CameraXActivityTestExtensions.kt
@@ -16,6 +16,7 @@
package androidx.camera.integration.core
+import androidx.camera.integration.core.util.StressTestUtil.VIDEO_CAPTURE_AUTO_STOP_LENGTH_MS
import androidx.test.core.app.ActivityScenario
import androidx.test.espresso.Espresso
import androidx.test.espresso.IdlingRegistry
@@ -23,12 +24,18 @@
import androidx.test.espresso.assertion.ViewAssertions
import androidx.test.espresso.matcher.ViewMatchers
import androidx.testutils.withActivity
+import com.google.common.truth.Truth.assertThat
/**
* Waits until the viewfinder has received frames and its idling resource has become idle.
*/
internal fun ActivityScenario<CameraXActivity>.waitForViewfinderIdle() {
- val idlingResource = withActivity { viewIdlingResource }
+ val idlingResource = withActivity {
+ // Make sure that the test target use case is not null
+ assertThat(preview).isNotNull()
+ resetViewIdlingResource()
+ viewIdlingResource
+ }
try {
IdlingRegistry.getInstance().register(idlingResource)
// Check the activity launched and Preview displays frames.
@@ -38,12 +45,33 @@
IdlingRegistry.getInstance().unregister(idlingResource)
}
}
+/**
+ * Waits until the viewfinder has received frames and its idling resource has become idle.
+ */
+internal fun ActivityScenario<CameraXActivity>.switchCameraAndWaitForViewfinderIdle() {
+ val idlingResource = withActivity {
+ // Make sure that the test target use case is not null
+ assertThat(preview).isNotNull()
+ resetViewIdlingResource()
+ viewIdlingResource
+ }
+ try {
+ IdlingRegistry.getInstance().register(idlingResource)
+ Espresso.onView(ViewMatchers.withId(R.id.direction_toggle)).perform(click())
+ } finally { // Always release the idling resource, in case of timeout exceptions.
+ IdlingRegistry.getInstance().unregister(idlingResource)
+ }
+}
/**
* Waits until an image has been saved and its idling resource has become idle.
*/
internal fun ActivityScenario<CameraXActivity>.takePictureAndWaitForImageSavedIdle() {
- val idlingResource = withActivity { imageSavedIdlingResource }
+ val idlingResource = withActivity {
+ // Make sure that the test target use case is not null
+ assertThat(imageCapture).isNotNull()
+ imageSavedIdlingResource
+ }
try {
IdlingRegistry.getInstance().register(idlingResource)
// Perform click to take a picture.
@@ -52,4 +80,45 @@
IdlingRegistry.getInstance().unregister(idlingResource)
withActivity { deleteSessionImages() }
}
+}
+
+/**
+ * Waits until the imageAnalysis has received the required number of images and its idling resource
+ * has become idle.
+ */
+internal fun ActivityScenario<CameraXActivity>.waitForImageAnalysisIdle() {
+ val idlingResource = withActivity {
+ // Make sure that the test target use case is not null
+ assertThat(imageAnalysis).isNotNull()
+ resetAnalysisIdlingResource()
+ analysisIdlingResource
+ }
+ try {
+ IdlingRegistry.getInstance().register(idlingResource)
+ // Check the activity launched and the image analysis info is displayed on the text view.
+ Espresso.onView(ViewMatchers.withId(R.id.textView))
+ .check(ViewAssertions.matches(ViewMatchers.isDisplayed()))
+ } finally { // Always release the idling resource, in case of timeout exceptions.
+ IdlingRegistry.getInstance().unregister(idlingResource)
+ }
+}
+
+/**
+ * Waits until a video has been saved and its idling resource has become idle.
+ */
+internal fun ActivityScenario<CameraXActivity>.recordVideoAndWaitForVideoSavedIdle() {
+ val idlingResource = withActivity {
+ // Make sure that the test target use case is not null
+ assertThat(videoCapture).isNotNull()
+ setVideoCaptureAutoStopLength(VIDEO_CAPTURE_AUTO_STOP_LENGTH_MS)
+ videoSavedIdlingResource
+ }
+ try {
+ IdlingRegistry.getInstance().register(idlingResource)
+ // Perform click to record a video.
+ Espresso.onView(ViewMatchers.withId(R.id.Video)).perform(click())
+ } finally { // Always release the idling resource, in case of timeout exceptions.
+ IdlingRegistry.getInstance().unregister(idlingResource)
+ withActivity { deleteSessionVideos() }
+ }
}
\ No newline at end of file
diff --git a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/LifecycleStatusChangeStressTest.kt b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/LifecycleStatusChangeStressTest.kt
new file mode 100644
index 0000000..8460658
--- /dev/null
+++ b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/LifecycleStatusChangeStressTest.kt
@@ -0,0 +1,560 @@
+/*
+ * Copyright 2022 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 androidx.camera.integration.core
+
+import android.Manifest
+import android.content.Context
+import androidx.camera.camera2.Camera2Config
+import androidx.camera.core.Camera
+import androidx.camera.core.CameraSelector
+import androidx.camera.integration.core.CameraXActivity.BIND_IMAGE_ANALYSIS
+import androidx.camera.integration.core.CameraXActivity.BIND_IMAGE_CAPTURE
+import androidx.camera.integration.core.CameraXActivity.BIND_PREVIEW
+import androidx.camera.integration.core.CameraXActivity.BIND_VIDEO_CAPTURE
+import androidx.camera.integration.core.util.StressTestUtil.HOME_TIMEOUT_MS
+import androidx.camera.integration.core.util.StressTestUtil.STRESS_TEST_OPERATION_REPEAT_COUNT
+import androidx.camera.integration.core.util.StressTestUtil.STRESS_TEST_REPEAT_COUNT
+import androidx.camera.integration.core.util.StressTestUtil.VERIFICATION_TARGET_IMAGE_ANALYSIS
+import androidx.camera.integration.core.util.StressTestUtil.VERIFICATION_TARGET_IMAGE_CAPTURE
+import androidx.camera.integration.core.util.StressTestUtil.VERIFICATION_TARGET_PREVIEW
+import androidx.camera.integration.core.util.StressTestUtil.VERIFICATION_TARGET_VIDEO_CAPTURE
+import androidx.camera.integration.core.util.StressTestUtil.assumeCameraSupportUseCaseCombination
+import androidx.camera.integration.core.util.StressTestUtil.createCameraSelectorById
+import androidx.camera.integration.core.util.StressTestUtil.launchCameraXActivityAndWaitForPreviewReady
+import androidx.camera.lifecycle.ProcessCameraProvider
+import androidx.camera.testing.CameraUtil
+import androidx.camera.testing.CoreAppTestUtil
+import androidx.camera.testing.LabTestRule
+import androidx.camera.testing.StressTestRule
+import androidx.camera.testing.fakes.FakeLifecycleOwner
+import androidx.lifecycle.Lifecycle
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.filters.LargeTest
+import androidx.test.filters.SdkSuppress
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.rule.GrantPermissionRule
+import androidx.test.uiautomator.UiDevice
+import androidx.testutils.RepeatRule
+import java.util.concurrent.TimeUnit
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.withContext
+import org.junit.After
+import org.junit.Assume.assumeTrue
+import org.junit.Before
+import org.junit.ClassRule
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+
+@LargeTest
+@RunWith(Parameterized::class)
+@SdkSuppress(minSdkVersion = 21)
+class LifecycleStatusChangeStressTest(
+ private val cameraId: String
+) {
+ private val device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
+
+ @get:Rule
+ val useCamera = CameraUtil.grantCameraPermissionAndPreTest(
+ CameraUtil.PreTestCameraIdList(Camera2Config.defaultConfig())
+ )
+
+ @get:Rule
+ val permissionRule: GrantPermissionRule =
+ GrantPermissionRule.grant(
+ Manifest.permission.WRITE_EXTERNAL_STORAGE,
+ Manifest.permission.RECORD_AUDIO
+ )
+
+ @get:Rule
+ val labTest: LabTestRule = LabTestRule()
+
+ @get:Rule
+ val repeatRule = RepeatRule()
+
+ private val context = ApplicationProvider.getApplicationContext<Context>()
+
+ private lateinit var cameraProvider: ProcessCameraProvider
+ private lateinit var camera: Camera
+ private lateinit var cameraIdCameraSelector: CameraSelector
+
+ companion object {
+ @ClassRule
+ @JvmField val stressTest = StressTestRule()
+
+ @JvmStatic
+ @get:Parameterized.Parameters(name = "cameraId = {0}")
+ val parameters: Collection<String>
+ get() = CameraUtil.getBackwardCompatibleCameraIdListOrThrow()
+ }
+
+ @Before
+ fun setup(): Unit = runBlocking {
+ assumeTrue(CameraUtil.deviceHasCamera())
+ CoreAppTestUtil.assumeCompatibleDevice()
+ // Clear the device UI and check if there is no dialog or lock screen on the top of the
+ // window before start the test.
+ CoreAppTestUtil.prepareDeviceUI(InstrumentationRegistry.getInstrumentation())
+ // Use the natural orientation throughout these tests to ensure the activity isn't
+ // recreated unexpectedly. This will also freeze the sensors until
+ // mDevice.unfreezeRotation() in the tearDown() method. Any simulated rotations will be
+ // explicitly initiated from within the test.
+ device.setOrientationNatural()
+
+ cameraProvider = ProcessCameraProvider.getInstance(context)[10000, TimeUnit.MILLISECONDS]
+
+ cameraIdCameraSelector = createCameraSelectorById(cameraId)
+
+ camera = withContext(Dispatchers.Main) {
+ cameraProvider.bindToLifecycle(FakeLifecycleOwner(), cameraIdCameraSelector)
+ }
+ }
+
+ @After
+ fun tearDown(): Unit = runBlocking {
+ if (::cameraProvider.isInitialized) {
+ withContext(Dispatchers.Main) {
+ cameraProvider.unbindAll()
+ cameraProvider.shutdown()[10000, TimeUnit.MILLISECONDS]
+ }
+ }
+
+ // Unfreeze rotation so the device can choose the orientation via its own policy. Be nice
+ // to other tests :)
+ device.unfreezeRotation()
+ device.pressHome()
+ device.waitForIdle(HOME_TIMEOUT_MS)
+ }
+
+ @LabTestRule.LabTestOnly
+ @Test
+ @RepeatRule.Repeat(times = STRESS_TEST_REPEAT_COUNT)
+ fun pauseResumeActivity_checkPreviewInEachTime_withPreviewImageCapture() {
+ val useCaseCombination = BIND_PREVIEW or BIND_IMAGE_CAPTURE
+ pauseResumeActivity_checkOutput_repeatedly(
+ cameraId,
+ useCaseCombination,
+ VERIFICATION_TARGET_PREVIEW
+ )
+ }
+
+ @LabTestRule.LabTestOnly
+ @Test
+ @RepeatRule.Repeat(times = STRESS_TEST_REPEAT_COUNT)
+ fun pauseResumeActivity_checkImageCaptureInEachTime_withPreviewImageCapture() {
+ val useCaseCombination = BIND_PREVIEW or BIND_IMAGE_CAPTURE
+ pauseResumeActivity_checkOutput_repeatedly(
+ cameraId,
+ useCaseCombination,
+ VERIFICATION_TARGET_IMAGE_CAPTURE
+ )
+ }
+
+ @LabTestRule.LabTestOnly
+ @Test
+ @RepeatRule.Repeat(times = STRESS_TEST_REPEAT_COUNT)
+ fun pauseResumeActivity_checkImagePreviewInEachTime_withPreviewImageCaptureImageAnalysis() {
+ val useCaseCombination = BIND_PREVIEW or BIND_IMAGE_CAPTURE or BIND_IMAGE_ANALYSIS
+ pauseResumeActivity_checkOutput_repeatedly(
+ cameraId,
+ useCaseCombination,
+ VERIFICATION_TARGET_PREVIEW
+ )
+ }
+
+ @LabTestRule.LabTestOnly
+ @Test
+ @RepeatRule.Repeat(times = STRESS_TEST_REPEAT_COUNT)
+ fun pauseResumeActivity_checkImageCaptureInEachTime_withPreviewImageCaptureImageAnalysis() {
+ val useCaseCombination = BIND_PREVIEW or BIND_IMAGE_CAPTURE or BIND_IMAGE_ANALYSIS
+ pauseResumeActivity_checkOutput_repeatedly(
+ cameraId,
+ useCaseCombination,
+ VERIFICATION_TARGET_IMAGE_CAPTURE
+ )
+ }
+
+ @LabTestRule.LabTestOnly
+ @Test
+ @RepeatRule.Repeat(times = STRESS_TEST_REPEAT_COUNT)
+ fun pauseResumeActivity_checkImageAnalysisInEachTime_withPreviewImageCaptureImageAnalysis() {
+ val useCaseCombination = BIND_PREVIEW or BIND_IMAGE_CAPTURE or BIND_IMAGE_ANALYSIS
+ pauseResumeActivity_checkOutput_repeatedly(
+ cameraId,
+ useCaseCombination,
+ VERIFICATION_TARGET_IMAGE_ANALYSIS
+ )
+ }
+
+ @LabTestRule.LabTestOnly
+ @Test
+ @RepeatRule.Repeat(times = STRESS_TEST_REPEAT_COUNT)
+ fun pauseResumeActivity_checkPreviewInEachTime_withPreviewVideoCapture() {
+ val useCaseCombination = BIND_PREVIEW or BIND_VIDEO_CAPTURE
+ pauseResumeActivity_checkOutput_repeatedly(
+ cameraId,
+ useCaseCombination,
+ VERIFICATION_TARGET_PREVIEW
+ )
+ }
+
+ @LabTestRule.LabTestOnly
+ @Test
+ @RepeatRule.Repeat(times = STRESS_TEST_REPEAT_COUNT)
+ fun pauseResumeActivity_checkVideoCaptureInEachTime_withPreviewVideoCapture() {
+ val useCaseCombination = BIND_PREVIEW or BIND_VIDEO_CAPTURE
+ pauseResumeActivity_checkOutput_repeatedly(
+ cameraId,
+ useCaseCombination,
+ VERIFICATION_TARGET_VIDEO_CAPTURE
+ )
+ }
+
+ @LabTestRule.LabTestOnly
+ @Test
+ @RepeatRule.Repeat(times = STRESS_TEST_REPEAT_COUNT)
+ fun pauseResumeActivity_checkPreviewInEachTime_withPreviewVideoCaptureImageCapture() {
+ val useCaseCombination = BIND_PREVIEW or BIND_VIDEO_CAPTURE or BIND_IMAGE_CAPTURE
+ assumeCameraSupportUseCaseCombination(camera, useCaseCombination)
+ pauseResumeActivity_checkOutput_repeatedly(
+ cameraId,
+ useCaseCombination,
+ VERIFICATION_TARGET_PREVIEW
+ )
+ }
+
+ @LabTestRule.LabTestOnly
+ @Test
+ @RepeatRule.Repeat(times = STRESS_TEST_REPEAT_COUNT)
+ fun pauseResumeActivity_checkVideoCaptureInEachTime_withPreviewVideoCaptureImageCapture() {
+ val useCaseCombination = BIND_PREVIEW or BIND_VIDEO_CAPTURE or BIND_IMAGE_CAPTURE
+ assumeCameraSupportUseCaseCombination(camera, useCaseCombination)
+ pauseResumeActivity_checkOutput_repeatedly(
+ cameraId,
+ useCaseCombination,
+ VERIFICATION_TARGET_VIDEO_CAPTURE
+ )
+ }
+
+ @LabTestRule.LabTestOnly
+ @Test
+ @RepeatRule.Repeat(times = STRESS_TEST_REPEAT_COUNT)
+ fun pauseResumeActivity_checkImageCaptureInEachTime_withPreviewVideoCaptureImageCapture() {
+ val useCaseCombination = BIND_PREVIEW or BIND_VIDEO_CAPTURE or BIND_IMAGE_CAPTURE
+ assumeCameraSupportUseCaseCombination(camera, useCaseCombination)
+ pauseResumeActivity_checkOutput_repeatedly(
+ cameraId,
+ useCaseCombination,
+ VERIFICATION_TARGET_IMAGE_CAPTURE
+ )
+ }
+
+ @LabTestRule.LabTestOnly
+ @Test
+ @RepeatRule.Repeat(times = STRESS_TEST_REPEAT_COUNT)
+ fun pauseResumeActivity_checkPreviewInEachTime_withPreviewVideoCaptureImageAnalysis() {
+ val useCaseCombination = BIND_PREVIEW or BIND_VIDEO_CAPTURE or BIND_IMAGE_ANALYSIS
+ assumeCameraSupportUseCaseCombination(camera, useCaseCombination)
+ pauseResumeActivity_checkOutput_repeatedly(
+ cameraId,
+ useCaseCombination,
+ VERIFICATION_TARGET_PREVIEW
+ )
+ }
+
+ @LabTestRule.LabTestOnly
+ @Test
+ @RepeatRule.Repeat(times = STRESS_TEST_REPEAT_COUNT)
+ fun pauseResumeActivity_checkVideoCaptureInEachTime_withPreviewVideoCaptureImageAnalysis() {
+ val useCaseCombination = BIND_PREVIEW or BIND_VIDEO_CAPTURE or BIND_IMAGE_ANALYSIS
+ assumeCameraSupportUseCaseCombination(camera, useCaseCombination)
+ pauseResumeActivity_checkOutput_repeatedly(
+ cameraId,
+ useCaseCombination,
+ VERIFICATION_TARGET_VIDEO_CAPTURE
+ )
+ }
+
+ @LabTestRule.LabTestOnly
+ @Test
+ @RepeatRule.Repeat(times = STRESS_TEST_REPEAT_COUNT)
+ fun pauseResumeActivity_checkImageAnalysisInEachTime_withPreviewVideoCaptureImageAnalysis() {
+ val useCaseCombination = BIND_PREVIEW or BIND_VIDEO_CAPTURE or BIND_IMAGE_ANALYSIS
+ assumeCameraSupportUseCaseCombination(camera, useCaseCombination)
+ pauseResumeActivity_checkOutput_repeatedly(
+ cameraId,
+ useCaseCombination,
+ VERIFICATION_TARGET_IMAGE_ANALYSIS
+ )
+ }
+
+ /**
+ * Repeatedly pause, resume the activity and checks the use cases' capture functions can work.
+ */
+ private fun pauseResumeActivity_checkOutput_repeatedly(
+ cameraId: String,
+ useCaseCombination: Int,
+ verificationTarget: Int,
+ repeatCount: Int = STRESS_TEST_OPERATION_REPEAT_COUNT
+ ) {
+ // Launches CameraXActivity and wait for the preview ready.
+ val activityScenario =
+ launchCameraXActivityAndWaitForPreviewReady(cameraId, useCaseCombination)
+
+ // Pauses, resumes the activity, and then checks the test target use case can capture
+ // images successfully.
+ with(activityScenario) {
+ use {
+ for (i in 1..repeatCount) {
+ // Go through pause/resume then check again for view to get frames then idle.
+ moveToState(Lifecycle.State.CREATED)
+ moveToState(Lifecycle.State.RESUMED)
+
+ // Checks Preview can receive frames if it is the test target use case.
+ if (verificationTarget.and(VERIFICATION_TARGET_PREVIEW) != 0) {
+ waitForViewfinderIdle()
+ }
+
+ // Checks ImageCapture can take a picture if it is the test target use case.
+ if (verificationTarget.and(VERIFICATION_TARGET_IMAGE_CAPTURE) != 0) {
+ takePictureAndWaitForImageSavedIdle()
+ }
+
+ // Checks VideoCapture can record a video if it is the test target use case.
+ if (verificationTarget.and(VERIFICATION_TARGET_VIDEO_CAPTURE) != 0) {
+ recordVideoAndWaitForVideoSavedIdle()
+ }
+
+ // Checks ImageAnalysis can receive frames if it is the test target use case.
+ if (verificationTarget.and(VERIFICATION_TARGET_IMAGE_ANALYSIS) != 0) {
+ waitForImageAnalysisIdle()
+ }
+ }
+ }
+ }
+ }
+
+ @LabTestRule.LabTestOnly
+ @Test
+ @RepeatRule.Repeat(times = STRESS_TEST_REPEAT_COUNT)
+ fun checkPreview_afterPauseResumeRepeatedly_withPreviewImageCapture() {
+ val useCaseCombination = BIND_PREVIEW or BIND_IMAGE_CAPTURE
+ pauseResumeActivityRepeatedly_thenCheckOutput(
+ cameraId,
+ useCaseCombination,
+ VERIFICATION_TARGET_PREVIEW
+ )
+ }
+
+ @LabTestRule.LabTestOnly
+ @Test
+ @RepeatRule.Repeat(times = STRESS_TEST_REPEAT_COUNT)
+ fun checkImageCapture_afterPauseResumeRepeatedly_withPreviewImageCapture() {
+ val useCaseCombination = BIND_PREVIEW or BIND_IMAGE_CAPTURE
+ pauseResumeActivityRepeatedly_thenCheckOutput(
+ cameraId,
+ useCaseCombination,
+ VERIFICATION_TARGET_IMAGE_CAPTURE
+ )
+ }
+
+ @LabTestRule.LabTestOnly
+ @Test
+ @RepeatRule.Repeat(times = STRESS_TEST_REPEAT_COUNT)
+ fun checkPreview_afterPauseResumeRepeatedly_withPreviewImageCaptureImageAnalysis() {
+ val useCaseCombination = BIND_PREVIEW or BIND_IMAGE_CAPTURE or BIND_IMAGE_ANALYSIS
+ pauseResumeActivityRepeatedly_thenCheckOutput(
+ cameraId,
+ useCaseCombination,
+ VERIFICATION_TARGET_PREVIEW
+ )
+ }
+
+ @LabTestRule.LabTestOnly
+ @Test
+ @RepeatRule.Repeat(times = STRESS_TEST_REPEAT_COUNT)
+ fun checkImageCapture_afterPauseResumeRepeatedly_withPreviewImageCaptureImageAnalysis() {
+ val useCaseCombination = BIND_PREVIEW or BIND_IMAGE_CAPTURE or BIND_IMAGE_ANALYSIS
+ pauseResumeActivityRepeatedly_thenCheckOutput(
+ cameraId,
+ useCaseCombination,
+ VERIFICATION_TARGET_IMAGE_CAPTURE
+ )
+ }
+
+ @LabTestRule.LabTestOnly
+ @Test
+ @RepeatRule.Repeat(times = STRESS_TEST_REPEAT_COUNT)
+ fun checkImageAnalysis_afterPauseResumeRepeatedly_withPreviewImageCaptureImageAnalysis() {
+ val useCaseCombination = BIND_PREVIEW or BIND_IMAGE_CAPTURE or BIND_IMAGE_ANALYSIS
+ pauseResumeActivityRepeatedly_thenCheckOutput(
+ cameraId,
+ useCaseCombination,
+ VERIFICATION_TARGET_IMAGE_ANALYSIS
+ )
+ }
+
+ @LabTestRule.LabTestOnly
+ @Test
+ @RepeatRule.Repeat(times = STRESS_TEST_REPEAT_COUNT)
+ fun checkPreview_afterPauseResumeRepeatedly_withPreviewVideoCapture() {
+ val useCaseCombination = BIND_PREVIEW or BIND_VIDEO_CAPTURE
+ pauseResumeActivityRepeatedly_thenCheckOutput(
+ cameraId,
+ useCaseCombination,
+ VERIFICATION_TARGET_PREVIEW
+ )
+ }
+
+ @LabTestRule.LabTestOnly
+ @Test
+ @RepeatRule.Repeat(times = STRESS_TEST_REPEAT_COUNT)
+ fun checkVideoCapture_afterPauseResumeRepeatedly_withPreviewVideoCapture() {
+ val useCaseCombination = BIND_PREVIEW or BIND_VIDEO_CAPTURE
+ pauseResumeActivityRepeatedly_thenCheckOutput(
+ cameraId,
+ useCaseCombination,
+ VERIFICATION_TARGET_VIDEO_CAPTURE
+ )
+ }
+
+ @LabTestRule.LabTestOnly
+ @Test
+ @RepeatRule.Repeat(times = STRESS_TEST_REPEAT_COUNT)
+ fun checkPreview_afterPauseResumeRepeatedly_withPreviewVideoCaptureImageCapture() {
+ val useCaseCombination = BIND_PREVIEW or BIND_VIDEO_CAPTURE or BIND_IMAGE_CAPTURE
+ assumeCameraSupportUseCaseCombination(camera, useCaseCombination)
+ pauseResumeActivityRepeatedly_thenCheckOutput(
+ cameraId,
+ useCaseCombination,
+ VERIFICATION_TARGET_PREVIEW
+ )
+ }
+
+ @LabTestRule.LabTestOnly
+ @Test
+ @RepeatRule.Repeat(times = STRESS_TEST_REPEAT_COUNT)
+ fun checkVideoCapture_afterPauseResumeRepeatedly_withPreviewVideoCaptureImageCapture() {
+ val useCaseCombination = BIND_PREVIEW or BIND_VIDEO_CAPTURE or BIND_IMAGE_CAPTURE
+ assumeCameraSupportUseCaseCombination(camera, useCaseCombination)
+ pauseResumeActivityRepeatedly_thenCheckOutput(
+ cameraId,
+ useCaseCombination,
+ VERIFICATION_TARGET_VIDEO_CAPTURE
+ )
+ }
+
+ @LabTestRule.LabTestOnly
+ @Test
+ @RepeatRule.Repeat(times = STRESS_TEST_REPEAT_COUNT)
+ fun checkImageCapture_afterPauseResumeRepeatedly_withPreviewVideoCaptureImageCapture() {
+ val useCaseCombination = BIND_PREVIEW or BIND_VIDEO_CAPTURE or BIND_IMAGE_CAPTURE
+ assumeCameraSupportUseCaseCombination(camera, useCaseCombination)
+ pauseResumeActivityRepeatedly_thenCheckOutput(
+ cameraId,
+ useCaseCombination,
+ VERIFICATION_TARGET_IMAGE_CAPTURE
+ )
+ }
+
+ @LabTestRule.LabTestOnly
+ @Test
+ @RepeatRule.Repeat(times = STRESS_TEST_REPEAT_COUNT)
+ fun checkPreview_afterPauseResumeRepeatedly_withPreviewVideoCaptureImageAnalysis() {
+ val useCaseCombination = BIND_PREVIEW or BIND_VIDEO_CAPTURE or BIND_IMAGE_ANALYSIS
+ assumeCameraSupportUseCaseCombination(camera, useCaseCombination)
+ pauseResumeActivityRepeatedly_thenCheckOutput(
+ cameraId,
+ useCaseCombination,
+ VERIFICATION_TARGET_PREVIEW
+ )
+ }
+
+ @LabTestRule.LabTestOnly
+ @Test
+ @RepeatRule.Repeat(times = STRESS_TEST_REPEAT_COUNT)
+ fun checkVideoCapture_afterPauseResumeRepeatedly_withPreviewVideoCaptureImageAnalysis() {
+ val useCaseCombination = BIND_PREVIEW or BIND_VIDEO_CAPTURE or BIND_IMAGE_ANALYSIS
+ assumeCameraSupportUseCaseCombination(camera, useCaseCombination)
+ pauseResumeActivityRepeatedly_thenCheckOutput(
+ cameraId,
+ useCaseCombination,
+ VERIFICATION_TARGET_VIDEO_CAPTURE
+ )
+ }
+
+ @LabTestRule.LabTestOnly
+ @Test
+ @RepeatRule.Repeat(times = STRESS_TEST_REPEAT_COUNT)
+ fun checkImageAnalysis_afterPauseResumeRepeatedly_withPreviewVideoCaptureImageAnalysis() {
+ val useCaseCombination = BIND_PREVIEW or BIND_VIDEO_CAPTURE or BIND_IMAGE_ANALYSIS
+ assumeCameraSupportUseCaseCombination(camera, useCaseCombination)
+ pauseResumeActivityRepeatedly_thenCheckOutput(
+ cameraId,
+ useCaseCombination,
+ VERIFICATION_TARGET_IMAGE_ANALYSIS
+ )
+ }
+
+ /**
+ * Pause and resume the activity repeatedly, and then checks the use cases' capture functions
+ * can work.
+ */
+ private fun pauseResumeActivityRepeatedly_thenCheckOutput(
+ cameraId: String,
+ useCaseCombination: Int,
+ verificationTarget: Int,
+ repeatCount: Int = STRESS_TEST_OPERATION_REPEAT_COUNT
+ ) {
+ // Launches CameraXActivity and wait for the preview ready.
+ val activityScenario =
+ launchCameraXActivityAndWaitForPreviewReady(cameraId, useCaseCombination)
+
+ // Pauses, resumes the activity repleatedly, and then checks the test target use case can
+ // capture images successfully.
+ with(activityScenario) {
+ use {
+ for (i in 1..repeatCount) {
+ moveToState(Lifecycle.State.CREATED)
+ moveToState(Lifecycle.State.RESUMED)
+ }
+
+ // Checks Preview can receive frames if it is the test target use case.
+ if (verificationTarget.and(VERIFICATION_TARGET_PREVIEW) != 0) {
+ waitForViewfinderIdle()
+ }
+
+ // Checks ImageCapture can take a picture if it is the test target use case.
+ if (verificationTarget.and(VERIFICATION_TARGET_IMAGE_CAPTURE) != 0) {
+ takePictureAndWaitForImageSavedIdle()
+ }
+
+ // Checks VideoCapture can record a video if it is the test target use case.
+ if (verificationTarget.and(VERIFICATION_TARGET_VIDEO_CAPTURE) != 0) {
+ recordVideoAndWaitForVideoSavedIdle()
+ }
+
+ // Checks ImageAnalysis can receive frames if it is the test target use case.
+ if (verificationTarget.and(VERIFICATION_TARGET_IMAGE_ANALYSIS) != 0) {
+ waitForImageAnalysisIdle()
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/SwitchCameraStressTest.kt b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/SwitchCameraStressTest.kt
new file mode 100644
index 0000000..b985799
--- /dev/null
+++ b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/SwitchCameraStressTest.kt
@@ -0,0 +1,585 @@
+/*
+ * Copyright 2022 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 androidx.camera.integration.core
+
+import android.Manifest
+import android.content.Context
+import android.hardware.camera2.CameraCharacteristics
+import androidx.camera.camera2.Camera2Config
+import androidx.camera.camera2.interop.Camera2CameraInfo
+import androidx.camera.core.Camera
+import androidx.camera.core.CameraSelector
+import androidx.camera.integration.core.CameraXActivity.BIND_IMAGE_ANALYSIS
+import androidx.camera.integration.core.CameraXActivity.BIND_IMAGE_CAPTURE
+import androidx.camera.integration.core.CameraXActivity.BIND_PREVIEW
+import androidx.camera.integration.core.CameraXActivity.BIND_VIDEO_CAPTURE
+import androidx.camera.integration.core.util.StressTestUtil.HOME_TIMEOUT_MS
+import androidx.camera.integration.core.util.StressTestUtil.STRESS_TEST_OPERATION_REPEAT_COUNT
+import androidx.camera.integration.core.util.StressTestUtil.STRESS_TEST_REPEAT_COUNT
+import androidx.camera.integration.core.util.StressTestUtil.VERIFICATION_TARGET_IMAGE_ANALYSIS
+import androidx.camera.integration.core.util.StressTestUtil.VERIFICATION_TARGET_IMAGE_CAPTURE
+import androidx.camera.integration.core.util.StressTestUtil.VERIFICATION_TARGET_PREVIEW
+import androidx.camera.integration.core.util.StressTestUtil.VERIFICATION_TARGET_VIDEO_CAPTURE
+import androidx.camera.integration.core.util.StressTestUtil.assumeCameraSupportUseCaseCombination
+import androidx.camera.integration.core.util.StressTestUtil.createCameraSelectorById
+import androidx.camera.integration.core.util.StressTestUtil.launchCameraXActivityAndWaitForPreviewReady
+import androidx.camera.lifecycle.ProcessCameraProvider
+import androidx.camera.testing.CameraUtil
+import androidx.camera.testing.CoreAppTestUtil
+import androidx.camera.testing.LabTestRule
+import androidx.camera.testing.StressTestRule
+import androidx.camera.testing.fakes.FakeLifecycleOwner
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.filters.LargeTest
+import androidx.test.filters.SdkSuppress
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.rule.GrantPermissionRule
+import androidx.test.uiautomator.UiDevice
+import androidx.testutils.RepeatRule
+import java.util.concurrent.TimeUnit
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.withContext
+import org.junit.After
+import org.junit.Assume
+import org.junit.Before
+import org.junit.ClassRule
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+
+@LargeTest
+@RunWith(Parameterized::class)
+@SdkSuppress(minSdkVersion = 21)
+class SwitchCameraStressTest(
+ private val cameraId: String
+) {
+ private val device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
+
+ @get:Rule
+ val useCamera = CameraUtil.grantCameraPermissionAndPreTest(
+ CameraUtil.PreTestCameraIdList(Camera2Config.defaultConfig())
+ )
+
+ @get:Rule
+ val permissionRule: GrantPermissionRule =
+ GrantPermissionRule.grant(
+ Manifest.permission.WRITE_EXTERNAL_STORAGE,
+ Manifest.permission.RECORD_AUDIO
+ )
+
+ @get:Rule
+ val labTest: LabTestRule = LabTestRule()
+
+ @get:Rule
+ val repeatRule = RepeatRule()
+
+ private val context = ApplicationProvider.getApplicationContext<Context>()
+
+ private lateinit var cameraProvider: ProcessCameraProvider
+ private lateinit var camera: Camera
+ private lateinit var cameraIdCameraSelector: CameraSelector
+
+ companion object {
+ @ClassRule
+ @JvmField val stressTest = StressTestRule()
+
+ @JvmStatic
+ @get:Parameterized.Parameters(name = "cameraId = {0}")
+ val parameters: Collection<String>
+ get() = CameraUtil.getBackwardCompatibleCameraIdListOrThrow()
+ }
+
+ @Before
+ fun setup(): Unit = runBlocking {
+ Assume.assumeTrue(CameraUtil.deviceHasCamera())
+ CoreAppTestUtil.assumeCompatibleDevice()
+ // Clear the device UI and check if there is no dialog or lock screen on the top of the
+ // window before start the test.
+ CoreAppTestUtil.prepareDeviceUI(InstrumentationRegistry.getInstrumentation())
+ // Use the natural orientation throughout these tests to ensure the activity isn't
+ // recreated unexpectedly. This will also freeze the sensors until
+ // mDevice.unfreezeRotation() in the tearDown() method. Any simulated rotations will be
+ // explicitly initiated from within the test.
+ device.setOrientationNatural()
+
+ cameraProvider = ProcessCameraProvider.getInstance(context)[10000, TimeUnit.MILLISECONDS]
+
+ cameraIdCameraSelector = createCameraSelectorById(cameraId)
+
+ camera = withContext(Dispatchers.Main) {
+ cameraProvider.bindToLifecycle(FakeLifecycleOwner(), cameraIdCameraSelector)
+ }
+ }
+
+ @After
+ fun tearDown(): Unit = runBlocking {
+ if (::cameraProvider.isInitialized) {
+ withContext(Dispatchers.Main) {
+ cameraProvider.unbindAll()
+ cameraProvider.shutdown()[10000, TimeUnit.MILLISECONDS]
+ }
+ }
+
+ // Unfreeze rotation so the device can choose the orientation via its own policy. Be nice
+ // to other tests :)
+ device.unfreezeRotation()
+ device.pressHome()
+ device.waitForIdle(HOME_TIMEOUT_MS)
+ }
+
+ @LabTestRule.LabTestOnly
+ @Test
+ @RepeatRule.Repeat(times = STRESS_TEST_REPEAT_COUNT)
+ fun switchCamera_checkPreviewInEachTime_withPreviewImageCapture() {
+ val useCaseCombination = BIND_PREVIEW or BIND_IMAGE_CAPTURE
+ switchCamera_checkOutput_repeatedly(
+ cameraId,
+ useCaseCombination,
+ VERIFICATION_TARGET_PREVIEW
+ )
+ }
+
+ @LabTestRule.LabTestOnly
+ @Test
+ @RepeatRule.Repeat(times = STRESS_TEST_REPEAT_COUNT)
+ fun switchCamera_checkImageCaptureInEachTime_withPreviewImageCapture() {
+ val useCaseCombination = BIND_PREVIEW or BIND_IMAGE_CAPTURE
+ switchCamera_checkOutput_repeatedly(
+ cameraId,
+ useCaseCombination,
+ VERIFICATION_TARGET_IMAGE_CAPTURE
+ )
+ }
+
+ @LabTestRule.LabTestOnly
+ @Test
+ @RepeatRule.Repeat(times = STRESS_TEST_REPEAT_COUNT)
+ fun switchCamera_checkPreviewInEachTime_withPreviewImageCaptureImageAnalysis() {
+ val useCaseCombination = BIND_PREVIEW or BIND_IMAGE_CAPTURE or BIND_IMAGE_ANALYSIS
+ switchCamera_checkOutput_repeatedly(
+ cameraId,
+ useCaseCombination,
+ VERIFICATION_TARGET_PREVIEW
+ )
+ }
+
+ @LabTestRule.LabTestOnly
+ @Test
+ @RepeatRule.Repeat(times = STRESS_TEST_REPEAT_COUNT)
+ fun switchCamera_checkImageCaptureInEachTime_withPreviewImageCaptureImageAnalysis() {
+ val useCaseCombination = BIND_PREVIEW or BIND_IMAGE_CAPTURE or BIND_IMAGE_ANALYSIS
+ switchCamera_checkOutput_repeatedly(
+ cameraId,
+ useCaseCombination,
+ VERIFICATION_TARGET_IMAGE_CAPTURE
+ )
+ }
+
+ @LabTestRule.LabTestOnly
+ @Test
+ @RepeatRule.Repeat(times = STRESS_TEST_REPEAT_COUNT)
+ fun switchCamera_checkImageAnalysisInEachTime_withPreviewImageCaptureImageAnalysis() {
+ val useCaseCombination = BIND_PREVIEW or BIND_IMAGE_CAPTURE or BIND_IMAGE_ANALYSIS
+ switchCamera_checkOutput_repeatedly(
+ cameraId,
+ useCaseCombination,
+ VERIFICATION_TARGET_IMAGE_ANALYSIS
+ )
+ }
+
+ @LabTestRule.LabTestOnly
+ @Test
+ @RepeatRule.Repeat(times = STRESS_TEST_REPEAT_COUNT)
+ fun switchCamera_checkPreviewInEachTime_withPreviewVideoCapture() {
+ val useCaseCombination = BIND_PREVIEW or BIND_VIDEO_CAPTURE
+ switchCamera_checkOutput_repeatedly(
+ cameraId,
+ useCaseCombination,
+ VERIFICATION_TARGET_PREVIEW
+ )
+ }
+
+ @LabTestRule.LabTestOnly
+ @Test
+ @RepeatRule.Repeat(times = STRESS_TEST_REPEAT_COUNT)
+ fun switchCamera_checkVideoCaptureInEachTime_withPreviewVideoCapture() {
+ val useCaseCombination = BIND_PREVIEW or BIND_VIDEO_CAPTURE
+ switchCamera_checkOutput_repeatedly(
+ cameraId,
+ useCaseCombination,
+ VERIFICATION_TARGET_VIDEO_CAPTURE
+ )
+ }
+
+ @LabTestRule.LabTestOnly
+ @Test
+ @RepeatRule.Repeat(times = STRESS_TEST_REPEAT_COUNT)
+ fun switchCamera_checkPreviewInEachTime_withPreviewVideoCaptureImageCapture() {
+ val useCaseCombination = BIND_PREVIEW or BIND_VIDEO_CAPTURE or BIND_IMAGE_CAPTURE
+ assumeBothLensFacingCamerasSupportUseCaseCombination(camera, useCaseCombination)
+ switchCamera_checkOutput_repeatedly(
+ cameraId,
+ useCaseCombination,
+ VERIFICATION_TARGET_PREVIEW
+ )
+ }
+
+ @LabTestRule.LabTestOnly
+ @Test
+ @RepeatRule.Repeat(times = STRESS_TEST_REPEAT_COUNT)
+ fun switchCamera_checkVideoCaptureInEachTime_withPreviewVideoCaptureImageCapture() {
+ val useCaseCombination = BIND_PREVIEW or BIND_VIDEO_CAPTURE or BIND_IMAGE_CAPTURE
+ assumeBothLensFacingCamerasSupportUseCaseCombination(camera, useCaseCombination)
+ switchCamera_checkOutput_repeatedly(
+ cameraId,
+ useCaseCombination,
+ VERIFICATION_TARGET_VIDEO_CAPTURE
+ )
+ }
+
+ @LabTestRule.LabTestOnly
+ @Test
+ @RepeatRule.Repeat(times = STRESS_TEST_REPEAT_COUNT)
+ fun switchCamera_checkImageCaptureInEachTime_withPreviewVideoCaptureImageCapture() {
+ val useCaseCombination = BIND_PREVIEW or BIND_VIDEO_CAPTURE or BIND_IMAGE_CAPTURE
+ assumeBothLensFacingCamerasSupportUseCaseCombination(camera, useCaseCombination)
+ switchCamera_checkOutput_repeatedly(
+ cameraId,
+ useCaseCombination,
+ VERIFICATION_TARGET_IMAGE_CAPTURE
+ )
+ }
+
+ @LabTestRule.LabTestOnly
+ @Test
+ @RepeatRule.Repeat(times = STRESS_TEST_REPEAT_COUNT)
+ fun switchCamera_checkPreviewInEachTime_withPreviewVideoCaptureImageAnalysis() {
+ val useCaseCombination = BIND_PREVIEW or BIND_VIDEO_CAPTURE or BIND_IMAGE_ANALYSIS
+ assumeBothLensFacingCamerasSupportUseCaseCombination(camera, useCaseCombination)
+ switchCamera_checkOutput_repeatedly(
+ cameraId,
+ useCaseCombination,
+ VERIFICATION_TARGET_PREVIEW
+ )
+ }
+
+ @LabTestRule.LabTestOnly
+ @Test
+ @RepeatRule.Repeat(times = STRESS_TEST_REPEAT_COUNT)
+ fun switchCamera_checkVideoCaptureInEachTime_withPreviewVideoCaptureImageAnalysis() {
+ val useCaseCombination = BIND_PREVIEW or BIND_VIDEO_CAPTURE or BIND_IMAGE_ANALYSIS
+ assumeBothLensFacingCamerasSupportUseCaseCombination(camera, useCaseCombination)
+ switchCamera_checkOutput_repeatedly(
+ cameraId,
+ useCaseCombination,
+ VERIFICATION_TARGET_VIDEO_CAPTURE
+ )
+ }
+
+ @LabTestRule.LabTestOnly
+ @Test
+ @RepeatRule.Repeat(times = STRESS_TEST_REPEAT_COUNT)
+ fun switchCamera_checkImageAnalysisInEachTime_withPreviewVideoCaptureImageAnalysis() {
+ val useCaseCombination = BIND_PREVIEW or BIND_VIDEO_CAPTURE or BIND_IMAGE_ANALYSIS
+ assumeBothLensFacingCamerasSupportUseCaseCombination(camera, useCaseCombination)
+ switchCamera_checkOutput_repeatedly(
+ cameraId,
+ useCaseCombination,
+ VERIFICATION_TARGET_IMAGE_ANALYSIS
+ )
+ }
+
+ /**
+ * Repeatedly switch the cameras and checks the use cases' capture functions can work.
+ */
+ private fun switchCamera_checkOutput_repeatedly(
+ cameraId: String,
+ useCaseCombination: Int,
+ verificationTarget: Int,
+ repeatCount: Int = STRESS_TEST_OPERATION_REPEAT_COUNT
+ ) {
+ // Launches CameraXActivity and wait for the preview ready.
+ val activityScenario =
+ launchCameraXActivityAndWaitForPreviewReady(cameraId, useCaseCombination)
+
+ // Repeatedly switches camera and checks the test target use case can capture images
+ // successfully.
+ with(activityScenario) {
+ use {
+ for (i in 1..repeatCount) {
+ // Switches camera and wait for preview idle.
+ switchCameraAndWaitForViewfinderIdle()
+
+ // Checks Preview can receive frames if it is the test target use case.
+ if (verificationTarget.and(VERIFICATION_TARGET_PREVIEW) != 0) {
+ waitForViewfinderIdle()
+ }
+
+ // Checks ImageCapture can take a picture if it is the test target use case.
+ if (verificationTarget.and(VERIFICATION_TARGET_IMAGE_CAPTURE) != 0) {
+ takePictureAndWaitForImageSavedIdle()
+ }
+
+ // Checks VideoCapture can record a video if it is the test target use case.
+ if (verificationTarget.and(VERIFICATION_TARGET_VIDEO_CAPTURE) != 0) {
+ recordVideoAndWaitForVideoSavedIdle()
+ }
+
+ // Checks ImageAnalysis can receive frames if it is the test target use case.
+ if (verificationTarget.and(VERIFICATION_TARGET_IMAGE_ANALYSIS) != 0) {
+ waitForImageAnalysisIdle()
+ }
+ }
+ }
+ }
+ }
+
+ @LabTestRule.LabTestOnly
+ @Test
+ @RepeatRule.Repeat(times = STRESS_TEST_REPEAT_COUNT)
+ fun checkPreview_afterSwitchCameraRepeatedly_withPreviewImageCapture() {
+ val useCaseCombination = BIND_PREVIEW or BIND_IMAGE_CAPTURE
+ switchCamera_repeatedly_thenCheckOutput(
+ cameraId,
+ useCaseCombination,
+ VERIFICATION_TARGET_PREVIEW
+ )
+ }
+
+ @LabTestRule.LabTestOnly
+ @Test
+ @RepeatRule.Repeat(times = STRESS_TEST_REPEAT_COUNT)
+ fun checkImageCapture_afterSwitchCameraRepeatedly_withPreviewImageCapture() {
+ val useCaseCombination = BIND_PREVIEW or BIND_IMAGE_CAPTURE
+ switchCamera_repeatedly_thenCheckOutput(
+ cameraId,
+ useCaseCombination,
+ VERIFICATION_TARGET_IMAGE_CAPTURE
+ )
+ }
+
+ @LabTestRule.LabTestOnly
+ @Test
+ @RepeatRule.Repeat(times = STRESS_TEST_REPEAT_COUNT)
+ fun checkPreview_afterSwitchCameraRepeatedly_withPreviewImageCaptureImageAnalysis() {
+ val useCaseCombination = BIND_PREVIEW or BIND_IMAGE_CAPTURE or BIND_IMAGE_ANALYSIS
+ switchCamera_repeatedly_thenCheckOutput(
+ cameraId,
+ useCaseCombination,
+ VERIFICATION_TARGET_PREVIEW
+ )
+ }
+
+ @LabTestRule.LabTestOnly
+ @Test
+ @RepeatRule.Repeat(times = STRESS_TEST_REPEAT_COUNT)
+ fun checkImageCapture_afterSwitchCameraRepeatedly_withPreviewImageCaptureImageAnalysis() {
+ val useCaseCombination = BIND_PREVIEW or BIND_IMAGE_CAPTURE or BIND_IMAGE_ANALYSIS
+ switchCamera_repeatedly_thenCheckOutput(
+ cameraId,
+ useCaseCombination,
+ VERIFICATION_TARGET_IMAGE_CAPTURE
+ )
+ }
+
+ @LabTestRule.LabTestOnly
+ @Test
+ @RepeatRule.Repeat(times = STRESS_TEST_REPEAT_COUNT)
+ fun checkImageAnalysis_afterSwitchCameraRepeatedly_withPreviewImageCaptureImageAnalysis() {
+ val useCaseCombination = BIND_PREVIEW or BIND_IMAGE_CAPTURE or BIND_IMAGE_ANALYSIS
+ switchCamera_repeatedly_thenCheckOutput(
+ cameraId,
+ useCaseCombination,
+ VERIFICATION_TARGET_IMAGE_ANALYSIS
+ )
+ }
+
+ @LabTestRule.LabTestOnly
+ @Test
+ @RepeatRule.Repeat(times = STRESS_TEST_REPEAT_COUNT)
+ fun checkPreview_afterSwitchCameraRepeatedly_withPreviewVideoCapture() {
+ val useCaseCombination = BIND_PREVIEW or BIND_VIDEO_CAPTURE
+ switchCamera_repeatedly_thenCheckOutput(
+ cameraId,
+ useCaseCombination,
+ VERIFICATION_TARGET_PREVIEW
+ )
+ }
+
+ @LabTestRule.LabTestOnly
+ @Test
+ @RepeatRule.Repeat(times = STRESS_TEST_REPEAT_COUNT)
+ fun checkVideoCapture_afterSwitchCameraRepeatedly_withPreviewVideoCapture() {
+ val useCaseCombination = BIND_PREVIEW or BIND_VIDEO_CAPTURE
+ switchCamera_repeatedly_thenCheckOutput(
+ cameraId,
+ useCaseCombination,
+ VERIFICATION_TARGET_VIDEO_CAPTURE
+ )
+ }
+
+ @LabTestRule.LabTestOnly
+ @Test
+ @RepeatRule.Repeat(times = STRESS_TEST_REPEAT_COUNT)
+ fun checkPreview_afterSwitchCameraRepeatedly_withPreviewVideoCaptureImageCapture() {
+ val useCaseCombination = BIND_PREVIEW or BIND_VIDEO_CAPTURE or BIND_IMAGE_CAPTURE
+ assumeBothLensFacingCamerasSupportUseCaseCombination(camera, useCaseCombination)
+ switchCamera_repeatedly_thenCheckOutput(
+ cameraId,
+ useCaseCombination,
+ VERIFICATION_TARGET_PREVIEW
+ )
+ }
+
+ @LabTestRule.LabTestOnly
+ @Test
+ @RepeatRule.Repeat(times = STRESS_TEST_REPEAT_COUNT)
+ fun checkVideoCapture_afterSwitchCameraRepeatedly_withPreviewVideoCaptureImageCapture() {
+ val useCaseCombination = BIND_PREVIEW or BIND_VIDEO_CAPTURE or BIND_IMAGE_CAPTURE
+ assumeBothLensFacingCamerasSupportUseCaseCombination(camera, useCaseCombination)
+ switchCamera_repeatedly_thenCheckOutput(
+ cameraId,
+ useCaseCombination,
+ VERIFICATION_TARGET_VIDEO_CAPTURE
+ )
+ }
+
+ @LabTestRule.LabTestOnly
+ @Test
+ @RepeatRule.Repeat(times = STRESS_TEST_REPEAT_COUNT)
+ fun checkImageCapture_afterSwitchCameraRepeatedly_withPreviewVideoCaptureImageCapture() {
+ val useCaseCombination = BIND_PREVIEW or BIND_VIDEO_CAPTURE or BIND_IMAGE_CAPTURE
+ assumeBothLensFacingCamerasSupportUseCaseCombination(camera, useCaseCombination)
+ switchCamera_repeatedly_thenCheckOutput(
+ cameraId,
+ useCaseCombination,
+ VERIFICATION_TARGET_IMAGE_CAPTURE
+ )
+ }
+
+ @LabTestRule.LabTestOnly
+ @Test
+ @RepeatRule.Repeat(times = STRESS_TEST_REPEAT_COUNT)
+ fun checkVideoCapture_afterSwitchCameraRepeatedly_withPreviewVideoCaptureImageAnalysis() {
+ val useCaseCombination = BIND_PREVIEW or BIND_VIDEO_CAPTURE or BIND_IMAGE_ANALYSIS
+ assumeBothLensFacingCamerasSupportUseCaseCombination(camera, useCaseCombination)
+ switchCamera_repeatedly_thenCheckOutput(
+ cameraId,
+ useCaseCombination,
+ VERIFICATION_TARGET_VIDEO_CAPTURE
+ )
+ }
+
+ @LabTestRule.LabTestOnly
+ @Test
+ @RepeatRule.Repeat(times = STRESS_TEST_REPEAT_COUNT)
+ fun checkImageAnalysis_afterSwitchCameraRepeatedly_withPreviewVideoCaptureImageAnalysis() {
+ val useCaseCombination = BIND_PREVIEW or BIND_VIDEO_CAPTURE or BIND_IMAGE_ANALYSIS
+ assumeBothLensFacingCamerasSupportUseCaseCombination(camera, useCaseCombination)
+ switchCamera_repeatedly_thenCheckOutput(
+ cameraId,
+ useCaseCombination,
+ VERIFICATION_TARGET_IMAGE_ANALYSIS
+ )
+ }
+
+ @LabTestRule.LabTestOnly
+ @Test
+ @RepeatRule.Repeat(times = STRESS_TEST_REPEAT_COUNT)
+ fun checkPreview_afterSwitchCameraRepeatedly_withPreviewVideoCaptureImageAnalysis() {
+ val useCaseCombination = BIND_PREVIEW or BIND_VIDEO_CAPTURE or BIND_IMAGE_ANALYSIS
+ assumeBothLensFacingCamerasSupportUseCaseCombination(camera, useCaseCombination)
+ switchCamera_repeatedly_thenCheckOutput(
+ cameraId,
+ useCaseCombination,
+ VERIFICATION_TARGET_PREVIEW
+ )
+ }
+
+ /**
+ * Switch the cameras repeatedly,and then checks the use cases' capture functions can work.
+ */
+ private fun switchCamera_repeatedly_thenCheckOutput(
+ cameraId: String,
+ useCaseCombination: Int,
+ verificationTarget: Int,
+ repeatCount: Int = STRESS_TEST_OPERATION_REPEAT_COUNT
+ ) {
+ // Launches CameraXActivity and wait for the preview ready.
+ val activityScenario =
+ launchCameraXActivityAndWaitForPreviewReady(cameraId, useCaseCombination)
+
+ // Switches camera repeatedly, and then checks the test target use case can capture images
+ // successfully.
+ with(activityScenario) {
+ use {
+ for (i in 1..repeatCount) {
+ // Switches camera and wait for preview idle.
+ switchCameraAndWaitForViewfinderIdle()
+ }
+
+ // Checks Preview can receive frames if it is the test target use case.
+ if (verificationTarget.and(VERIFICATION_TARGET_PREVIEW) != 0) {
+ waitForViewfinderIdle()
+ }
+
+ // Checks ImageCapture can take a picture if it is the test target use case.
+ if (verificationTarget.and(VERIFICATION_TARGET_IMAGE_CAPTURE) != 0) {
+ takePictureAndWaitForImageSavedIdle()
+ }
+
+ // Checks VideoCapture can record a video if it is the test target use case.
+ if (verificationTarget.and(VERIFICATION_TARGET_VIDEO_CAPTURE) != 0) {
+ recordVideoAndWaitForVideoSavedIdle()
+ }
+
+ // Checks ImageAnalysis can receive frames if it is the test target use case.
+ if (verificationTarget.and(VERIFICATION_TARGET_IMAGE_ANALYSIS) != 0) {
+ waitForImageAnalysisIdle()
+ }
+ }
+ }
+ }
+
+ private fun assumeBothLensFacingCamerasSupportUseCaseCombination(
+ camera: Camera,
+ useCaseCombination: Int
+ ): Unit = runBlocking {
+ // Checks whether the input camera can support the use case combination
+ assumeCameraSupportUseCaseCombination(camera, useCaseCombination)
+
+ val camera2CameraInfo = Camera2CameraInfo.from(camera.cameraInfo)
+ val lensFacing =
+ camera2CameraInfo.getCameraCharacteristic(CameraCharacteristics.LENS_FACING)
+
+ val otherLensFacingCameraSelector =
+ if (lensFacing == CameraCharacteristics.LENS_FACING_BACK) {
+ CameraSelector.DEFAULT_FRONT_CAMERA
+ } else {
+ CameraSelector.DEFAULT_BACK_CAMERA
+ }
+
+ val otherLensFacingCamera = withContext(Dispatchers.Main) {
+ cameraProvider.bindToLifecycle(FakeLifecycleOwner(), otherLensFacingCameraSelector)
+ }
+
+ // Checks whether the camera of the other lens facing can support the use case combination
+ assumeCameraSupportUseCaseCombination(otherLensFacingCamera, useCaseCombination)
+ }
+}
\ No newline at end of file
diff --git a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/util/StressTestUtil.kt b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/util/StressTestUtil.kt
index 31bd201..8342a50 100644
--- a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/util/StressTestUtil.kt
+++ b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/util/StressTestUtil.kt
@@ -16,15 +16,120 @@
package androidx.camera.integration.core.util
+import android.content.Context
+import android.content.Intent
import androidx.annotation.OptIn
import androidx.camera.camera2.interop.Camera2CameraInfo
import androidx.camera.camera2.interop.ExperimentalCamera2Interop
+import androidx.camera.core.Camera
import androidx.camera.core.CameraFilter
import androidx.camera.core.CameraInfo
import androidx.camera.core.CameraSelector
+import androidx.camera.core.ImageAnalysis
+import androidx.camera.core.ImageCapture
+import androidx.camera.core.Preview
+import androidx.camera.integration.core.CameraXActivity
+import androidx.camera.integration.core.CameraXActivity.BIND_IMAGE_ANALYSIS
+import androidx.camera.integration.core.CameraXActivity.BIND_IMAGE_CAPTURE
+import androidx.camera.integration.core.CameraXActivity.BIND_PREVIEW
+import androidx.camera.integration.core.CameraXActivity.BIND_VIDEO_CAPTURE
+import androidx.camera.integration.core.CameraXActivity.INTENT_EXTRA_CAMERA_ID
+import androidx.camera.integration.core.CameraXActivity.INTENT_EXTRA_USE_CASE_COMBINATION
+import androidx.camera.integration.core.waitForViewfinderIdle
+import androidx.camera.video.Recorder
+import androidx.camera.video.VideoCapture
+import androidx.test.core.app.ActivityScenario
+import androidx.test.core.app.ApplicationProvider
+import org.junit.Assume.assumeTrue
+
+private const val CORE_TEST_APP_PACKAGE = "androidx.camera.integration.core"
object StressTestUtil {
+ /**
+ * Launches CameraXActivity and wait for the preview ready.
+ *
+ * <p>Test cases can start activity by this function and then add other specific test
+ * operations after the activity is launched.
+ *
+ * <p>If the target camera device can't support the specified use case combination, an
+ * AssumptionViolatedException will be thrown to skip the test.
+ *
+ * @param cameraId Launches the activity with the specified camera id
+ * @param useCaseCombination Launches the activity with the specified use case combination.
+ * [BIND_PREVIEW], [BIND_IMAGE_CAPTURE], [BIND_VIDEO_CAPTURE] and [BIND_IMAGE_ANALYSIS] can be
+ * used to set the combination.
+ */
+ @JvmStatic
+ fun launchCameraXActivityAndWaitForPreviewReady(
+ cameraId: String,
+ useCaseCombination: Int
+ ): ActivityScenario<CameraXActivity> {
+ if (useCaseCombination.and(BIND_PREVIEW) == 0) {
+ throw IllegalArgumentException("Preview must be included!")
+ }
+
+ val intent = ApplicationProvider.getApplicationContext<Context>().packageManager
+ .getLaunchIntentForPackage(CORE_TEST_APP_PACKAGE)!!.apply {
+ putExtra(INTENT_EXTRA_CAMERA_ID, cameraId)
+ putExtra(INTENT_EXTRA_USE_CASE_COMBINATION, useCaseCombination)
+ flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
+ }
+
+ val activityScenario: ActivityScenario<CameraXActivity> = ActivityScenario.launch(intent)
+
+ activityScenario.onActivity {
+ // Checks that the camera id is correct
+ val camera2CameraInfo = Camera2CameraInfo.from(it.camera!!.cameraInfo)
+
+ if (camera2CameraInfo.cameraId != cameraId) {
+ it.finish()
+ throw IllegalArgumentException("The activity is not launched with the correct" +
+ " camera of expected id.")
+ }
+ }
+
+ // Ensure ActivityScenario is cleaned up properly
+ // Wait for viewfinder to receive enough frames for its IdlingResource to idle.
+ activityScenario.waitForViewfinderIdle()
+
+ return activityScenario
+ }
+
+ /**
+ * Checks and skips the test if the target camera can't support the use case combination.
+ */
+ @JvmStatic
+ fun assumeCameraSupportUseCaseCombination(camera: Camera, useCaseCombination: Int) {
+ val preview = Preview.Builder().build()
+ val imageCapture = if (useCaseCombination.and(BIND_IMAGE_CAPTURE) != 0) {
+ ImageCapture.Builder().build()
+ } else {
+ null
+ }
+ val videoCapture = if (useCaseCombination.and(BIND_VIDEO_CAPTURE) != 0) {
+ VideoCapture.withOutput(Recorder.Builder().build())
+ } else {
+ null
+ }
+ val imageAnalysis = if (useCaseCombination.and(BIND_IMAGE_ANALYSIS) != 0) {
+ ImageAnalysis.Builder().build()
+ } else {
+ null
+ }
+
+ assumeTrue(
+ camera.isUseCasesCombinationSupported(
+ *listOfNotNull(
+ preview,
+ imageCapture,
+ videoCapture,
+ imageAnalysis
+ ).toTypedArray()
+ )
+ )
+ }
+
@JvmStatic
@OptIn(ExperimentalCamera2Interop::class)
fun createCameraSelectorById(cameraId: String) =
@@ -58,4 +163,34 @@
*
*/
const val STRESS_TEST_OPERATION_REPEAT_COUNT = 10
+
+ /**
+ * Timeout duration to wait for idle after pressing HOME key
+ */
+ const val HOME_TIMEOUT_MS = 3000L
+
+ /**
+ * Auto-stop duration for video capture related tests.
+ */
+ const val VIDEO_CAPTURE_AUTO_STOP_LENGTH_MS = 3000L
+
+ /**
+ * Constant to specify that the verification target is [Preview].
+ */
+ const val VERIFICATION_TARGET_PREVIEW = 0x1
+
+ /**
+ * Constant to specify that the verification target is [ImageCapture].
+ */
+ const val VERIFICATION_TARGET_IMAGE_CAPTURE = 0x2
+
+ /**
+ * Constant to specify that the verification target is [VideoCapture].
+ */
+ const val VERIFICATION_TARGET_VIDEO_CAPTURE = 0x4
+
+ /**
+ * Constant to specify that the verification target is [ImageAnalysis].
+ */
+ const val VERIFICATION_TARGET_IMAGE_ANALYSIS = 0x8
}
\ No newline at end of file
diff --git a/camera/integration-tests/coretestapp/src/main/java/androidx/camera/integration/core/CameraXActivity.java b/camera/integration-tests/coretestapp/src/main/java/androidx/camera/integration/core/CameraXActivity.java
index afabb4a..f35f604 100644
--- a/camera/integration-tests/coretestapp/src/main/java/androidx/camera/integration/core/CameraXActivity.java
+++ b/camera/integration-tests/coretestapp/src/main/java/androidx/camera/integration/core/CameraXActivity.java
@@ -29,8 +29,10 @@
import android.content.ContentResolver;
import android.content.ContentValues;
import android.content.Context;
+import android.content.Intent;
import android.content.pm.PackageManager;
import android.database.Cursor;
+import android.hardware.camera2.CameraCharacteristics;
import android.hardware.display.DisplayManager;
import android.media.MediaScannerConnection;
import android.net.Uri;
@@ -65,14 +67,18 @@
import androidx.annotation.MainThread;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import androidx.annotation.OptIn;
import androidx.annotation.UiThread;
import androidx.annotation.VisibleForTesting;
import androidx.appcompat.app.AppCompatActivity;
import androidx.camera.camera2.internal.compat.quirk.CrashWhenTakingPhotoWithAutoFlashAEModeQuirk;
import androidx.camera.camera2.internal.compat.quirk.ImageCaptureFailWithAutoFlashQuirk;
import androidx.camera.camera2.internal.compat.quirk.ImageCaptureFlashNotFireQuirk;
+import androidx.camera.camera2.interop.Camera2CameraInfo;
+import androidx.camera.camera2.interop.ExperimentalCamera2Interop;
import androidx.camera.core.Camera;
import androidx.camera.core.CameraControl;
+import androidx.camera.core.CameraFilter;
import androidx.camera.core.CameraInfo;
import androidx.camera.core.CameraSelector;
import androidx.camera.core.DisplayOrientedMeteringPointFactory;
@@ -155,6 +161,25 @@
// "default_test_case".
private static final String INTENT_EXTRA_E2E_TEST_CASE = "e2e_test_case";
public static final String INTENT_EXTRA_CAMERA_IMPLEMENTATION = "camera_implementation";
+ // Launch the activity with the specified camera id.
+ @VisibleForTesting
+ public static final String INTENT_EXTRA_CAMERA_ID = "camera_id";
+ // Launch the activity with the specified use case combination.
+ @VisibleForTesting
+ public static final String INTENT_EXTRA_USE_CASE_COMBINATION = "use_case_combination";
+ @VisibleForTesting
+ // Sets this bit to bind Preview when using INTENT_EXTRA_USE_CASE_COMBINATION
+ public static final int BIND_PREVIEW = 0x1;
+ @VisibleForTesting
+ // Sets this bit to bind ImageCapture when using INTENT_EXTRA_USE_CASE_COMBINATION
+ public static final int BIND_IMAGE_CAPTURE = 0x2;
+ @VisibleForTesting
+ // Sets this bit to bind VideoCapture when using INTENT_EXTRA_USE_CASE_COMBINATION
+ public static final int BIND_VIDEO_CAPTURE = 0x4;
+ @VisibleForTesting
+ // Sets this bit to bind ImageAnalysis when using INTENT_EXTRA_USE_CASE_COMBINATION
+ public static final int BIND_IMAGE_ANALYSIS = 0x8;
+ private static final int UNKNOWN_LENS_FACING = -1;
static final CameraSelector BACK_SELECTOR =
new CameraSelector.Builder().requireLensFacing(CameraSelector.LENS_FACING_BACK).build();
static final CameraSelector FRONT_SELECTOR =
@@ -163,6 +188,9 @@
private final AtomicLong mImageAnalysisFrameCount = new AtomicLong(0);
private final AtomicLong mPreviewFrameCount = new AtomicLong(0);
+ // Automatically stops the video recording when this length value is set to be non-zero and
+ // video length reaches the length in ms.
+ private long mVideoCaptureAutoStopLength = 0;
final MutableLiveData<String> mImageAnalysisResult = new MutableLiveData<>();
private static final String BACKWARD = "BACKWARD";
private static final String SWITCH_TEST_CASE = "switch_test_case";
@@ -183,6 +211,9 @@
ExecutorService mImageCaptureExecutorService;
Camera mCamera;
+ private CameraSelector mLaunchingCameraIdSelector = null;
+ private int mLaunchingCameraLensFacing = UNKNOWN_LENS_FACING;
+
private ToggleButton mVideoToggle;
private ToggleButton mPhotoToggle;
private ToggleButton mAnalysisToggle;
@@ -208,7 +239,8 @@
private RecordUi mRecordUi;
private Quality mVideoQuality;
- SessionImagesUriSet mSessionImagesUriSet = new SessionImagesUriSet();
+ SessionMediaUriSet mSessionImagesUriSet = new SessionMediaUriSet();
+ SessionMediaUriSet mSessionVideosUriSet = new SessionMediaUriSet();
// Analyzer to be used with ImageAnalysis.
private ImageAnalysis.Analyzer mAnalyzer = new ImageAnalysis.Analyzer() {
@@ -220,7 +252,8 @@
mImageAnalysisResult.setValue(
Long.toString(image.getImageInfo().getTimestamp()));
try {
- if (!mAnalysisIdlingResource.isIdleNow()) {
+ if (mImageAnalysisFrameCount.get() >= FRAMES_UNTIL_IMAGE_ANALYSIS_IS_READY
+ && !mAnalysisIdlingResource.isIdleNow()) {
mAnalysisIdlingResource.decrement();
}
} catch (IllegalStateException e) {
@@ -270,13 +303,17 @@
// Espresso testing variables
private static final int FRAMES_UNTIL_VIEW_IS_READY = 5;
+ // Espresso testing variables
+ private static final int FRAMES_UNTIL_IMAGE_ANALYSIS_IS_READY = 5;
private final CountingIdlingResource mViewIdlingResource = new CountingIdlingResource("view");
private final CountingIdlingResource mInitializationIdlingResource =
new CountingIdlingResource("initialization");
- final CountingIdlingResource mAnalysisIdlingResource =
+ private final CountingIdlingResource mAnalysisIdlingResource =
new CountingIdlingResource("analysis");
- final CountingIdlingResource mImageSavedIdlingResource =
+ private final CountingIdlingResource mImageSavedIdlingResource =
new CountingIdlingResource("imagesaved");
+ private final CountingIdlingResource mVideoSavedIdlingResource =
+ new CountingIdlingResource("videosaved");
/**
* Retrieve idling resource that waits for image received by analyzer).
@@ -306,6 +343,15 @@
}
/**
+ * Retrieve idling resource that waits for a video being recorded and saved.
+ */
+ @VisibleForTesting
+ @NonNull
+ public IdlingResource getVideoSavedIdlingResource() {
+ return mVideoSavedIdlingResource;
+ }
+
+ /**
* Retrieve idling resource that waits for initialization to finish.
*/
@VisibleForTesting
@@ -336,7 +382,32 @@
public void resetViewIdlingResource() {
mPreviewFrameCount.set(0);
// Make the view idling resource non-idle, until required framecount achieved.
- mViewIdlingResource.increment();
+ if (mViewIdlingResource.isIdleNow()) {
+ mViewIdlingResource.increment();
+ }
+ }
+
+ /**
+ * Retrieve idling resource that waits for ImageAnalysis to receive images.
+ */
+ @VisibleForTesting
+ public void resetAnalysisIdlingResource() {
+ mImageAnalysisFrameCount.set(0);
+ // Make the analysis idling resource non-idle, until required images achieved.
+ if (mAnalysisIdlingResource.isIdleNow()) {
+ mAnalysisIdlingResource.increment();
+ }
+ }
+
+ /**
+ * Retrieve idling resource that waits for VideoCapture to record a video.
+ */
+ @VisibleForTesting
+ public void resetVideoSavedIdlingResource() {
+ // Make the video saved idling resource non-idle, until required video length recorded.
+ if (mVideoSavedIdlingResource.isIdleNow()) {
+ mVideoSavedIdlingResource.increment();
+ }
}
/**
@@ -348,6 +419,13 @@
mSessionImagesUriSet.deleteAllUris();
}
+ /**
+ * Delete videos that were taking during this session so far.
+ */
+ @VisibleForTesting
+ public void deleteSessionVideos() {
+ mSessionVideosUriSet.deleteAllUris();
+ }
@ImageCapture.CaptureMode
int getCaptureMode() {
@@ -425,6 +503,9 @@
pendingRecording = getVideoCapture().getOutput().prepareRecording(
this, getNewVideoOutputMediaStoreOptions());
}
+
+ resetVideoSavedIdlingResource();
+
mActiveRecording = pendingRecording
.withAudioEnabled()
.start(ContextCompat.getMainExecutor(CameraXActivity.this),
@@ -531,12 +612,14 @@
getApplicationContext().getContentResolver(),
uri
);
+ updateVideoSavedSessionData(uri);
} else if (outputOptions instanceof FileOutputOptions) {
videoFilePath = ((FileOutputOptions) outputOptions).getFile().getPath();
MediaScannerConnection.scanFile(this,
new String[] { videoFilePath }, null,
(path, uri1) -> {
Log.i(TAG, "Scanned " + path + " -> uri= " + uri1);
+ updateVideoSavedSessionData(uri1);
});
msg = "Saved file " + videoFilePath;
} else {
@@ -562,6 +645,16 @@
}
};
+ private void updateVideoSavedSessionData(@NonNull Uri uri) {
+ if (mSessionVideosUriSet != null) {
+ mSessionVideosUriSet.add(uri);
+ }
+
+ if (!mVideoSavedIdlingResource.isIdleNow()) {
+ mVideoSavedIdlingResource.decrement();
+ }
+ }
+
@NonNull
private MediaStoreOutputOptions getNewVideoOutputMediaStoreOptions() {
String videoFileName = "video_" + System.currentTimeMillis();
@@ -589,12 +682,16 @@
}
private void updateRecordingStats(@NonNull RecordingStats stats) {
- double durationSec = TimeUnit.NANOSECONDS.toMillis(stats.getRecordedDurationNanos())
- / 1000d;
+ double durationMs = TimeUnit.NANOSECONDS.toMillis(stats.getRecordedDurationNanos());
// Show megabytes in International System of Units (SI)
double sizeMb = stats.getNumBytesRecorded() / (1000d * 1000d);
- String msg = String.format("%.2f sec\n%.2f MB", durationSec, sizeMb);
+ String msg = String.format("%.2f sec\n%.2f MB", durationMs / 1000d, sizeMb);
mRecordUi.getTextStats().setText(msg);
+
+ if (mVideoCaptureAutoStopLength > 0 && durationMs >= mVideoCaptureAutoStopLength
+ && mRecordUi.getState() == RecordUi.State.RECORDING) {
+ mRecordUi.getButtonRecord().callOnClick();
+ }
}
private void setUpTakePictureButton() {
@@ -655,16 +752,60 @@
@SuppressWarnings("ObjectToString")
private void setUpCameraDirectionButton() {
mCameraDirectionButton.setOnClickListener(v -> {
- if (mCurrentCameraSelector == BACK_SELECTOR) {
- mCurrentCameraSelector = FRONT_SELECTOR;
- } else if (mCurrentCameraSelector == FRONT_SELECTOR) {
- mCurrentCameraSelector = BACK_SELECTOR;
- }
Log.d(TAG, "Change camera direction: " + mCurrentCameraSelector);
- tryBindUseCases();
+ CameraSelector switchedCameraSelector =
+ getSwitchedCameraSelector(mCurrentCameraSelector);
+
+ if (isUseCasesCombinationSupported(switchedCameraSelector, mUseCases)) {
+ mCurrentCameraSelector = switchedCameraSelector;
+ tryBindUseCases();
+ } else {
+ String msg = "Camera of the other lens facing can't support current use case "
+ + "combination.";
+ Log.d(TAG, msg);
+ Toast.makeText(this, msg, Toast.LENGTH_LONG).show();
+ }
});
}
+ @NonNull
+ private CameraSelector getSwitchedCameraSelector(
+ @NonNull CameraSelector currentCameraSelector) {
+ CameraSelector switchedCameraSelector;
+ // When the activity is launched with a specific camera id, camera switch function
+ // will switch the cameras between the camera of the specified camera id and the
+ // default camera of the opposite lens facing.
+ if (mLaunchingCameraIdSelector != null) {
+ if (currentCameraSelector != mLaunchingCameraIdSelector) {
+ switchedCameraSelector = mLaunchingCameraIdSelector;
+ } else {
+ if (mLaunchingCameraLensFacing == CameraSelector.LENS_FACING_BACK) {
+ switchedCameraSelector = FRONT_SELECTOR;
+ } else {
+ switchedCameraSelector = BACK_SELECTOR;
+ }
+ }
+ } else {
+ if (currentCameraSelector == BACK_SELECTOR) {
+ switchedCameraSelector = FRONT_SELECTOR;
+ } else {
+ switchedCameraSelector = BACK_SELECTOR;
+ }
+ }
+
+ return switchedCameraSelector;
+ }
+
+ private boolean isUseCasesCombinationSupported(@NonNull CameraSelector cameraSelector,
+ @NonNull List<UseCase> useCases) {
+ if (mCameraProvider == null) {
+ throw new IllegalStateException("Need to obtain mCameraProvider first!");
+ }
+
+ Camera targetCamera = mCameraProvider.bindToLifecycle(this, cameraSelector);
+ return targetCamera.isUseCasesCombinationSupported(useCases.toArray(new UseCase[0]));
+ }
+
private void setUpTorchButton() {
mTorchButton.setOnClickListener(v -> {
Objects.requireNonNull(getCameraInfo());
@@ -817,6 +958,25 @@
mZslToggle.setOnCheckedChangeListener(mOnCheckedChangeListener);
}
+ private void updateUseCaseCombinationByIntent(@NonNull Intent intent) {
+ Bundle bundle = intent.getExtras();
+
+ if (bundle == null) {
+ return;
+ }
+
+ int useCaseCombination = bundle.getInt(INTENT_EXTRA_USE_CASE_COMBINATION, 0);
+
+ if (useCaseCombination == 0) {
+ return;
+ }
+
+ mPreviewToggle.setChecked((useCaseCombination & BIND_PREVIEW) != 0L);
+ mPhotoToggle.setChecked((useCaseCombination & BIND_IMAGE_CAPTURE) != 0L);
+ mVideoToggle.setChecked((useCaseCombination & BIND_VIDEO_CAPTURE) != 0L);
+ mAnalysisToggle.setChecked((useCaseCombination & BIND_IMAGE_ANALYSIS) != 0L);
+ }
+
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
@@ -835,6 +995,8 @@
mAnalysisToggle = findViewById(R.id.AnalysisToggle);
mPreviewToggle = findViewById(R.id.PreviewToggle);
+ updateUseCaseCombinationByIntent(getIntent());
+
mTakePicture = findViewById(R.id.Picture);
mFlashButton = findViewById(R.id.flash_toggle);
mCameraDirectionButton = findViewById(R.id.direction_toggle);
@@ -904,12 +1066,19 @@
// Get params from adb extra string
Bundle bundle = this.getIntent().getExtras();
if (bundle != null) {
- String newCameraDirection = bundle.getString(INTENT_EXTRA_CAMERA_DIRECTION);
- if (newCameraDirection != null) {
- if (newCameraDirection.equals(BACKWARD)) {
- mCurrentCameraSelector = BACK_SELECTOR;
- } else {
- mCurrentCameraSelector = FRONT_SELECTOR;
+ String launchingCameraId = bundle.getString(INTENT_EXTRA_CAMERA_ID, null);
+
+ if (launchingCameraId != null) {
+ mLaunchingCameraIdSelector = createCameraSelectorById(launchingCameraId);
+ mCurrentCameraSelector = mLaunchingCameraIdSelector;
+ } else {
+ String newCameraDirection = bundle.getString(INTENT_EXTRA_CAMERA_DIRECTION);
+ if (newCameraDirection != null) {
+ if (newCameraDirection.equals(BACKWARD)) {
+ mCurrentCameraSelector = BACK_SELECTOR;
+ } else {
+ mCurrentCameraSelector = FRONT_SELECTOR;
+ }
}
}
@@ -963,6 +1132,7 @@
*
* @param calledBySelf flag indicates if this is a recursive call.
*/
+ @OptIn(markerClass = ExperimentalCamera2Interop.class)
void tryBindUseCases(boolean calledBySelf) {
boolean isViewFinderReady = mViewFinder.getWidth() != 0 && mViewFinder.getHeight() != 0;
boolean isCameraReady = mCameraProvider != null;
@@ -990,8 +1160,23 @@
mCameraProvider.unbindAll();
try {
+ // Binds to lifecycle without use cases to make sure mCamera can be retrieved for
+ // tests to do necessary checks.
+ mCamera = mCameraProvider.bindToLifecycle(this, mCurrentCameraSelector);
+
+ // Retrieves the lens facing info when the activity is launched with a specified
+ // camera id.
+ if (mCurrentCameraSelector == mLaunchingCameraIdSelector
+ && mLaunchingCameraLensFacing == UNKNOWN_LENS_FACING) {
+ Camera2CameraInfo camera2CameraInfo =
+ Camera2CameraInfo.from(mCamera.getCameraInfo());
+ mLaunchingCameraLensFacing = camera2CameraInfo.getCameraCharacteristic(
+ CameraCharacteristics.LENS_FACING);
+ }
+
List<UseCase> useCases = buildUseCases();
mCamera = bindToLifecycleSafely(useCases);
+
// Set the use cases after a successful binding.
mUseCases = useCases;
} catch (IllegalArgumentException ex) {
@@ -1054,8 +1239,8 @@
.setTargetName("ImageAnalysis")
.build();
useCases.add(imageAnalysis);
- // Make the analysis idling resource non-idle, until a frame received.
- mAnalysisIdlingResource.increment();
+ // Make the analysis idling resource non-idle, until the required frames received.
+ resetAnalysisIdlingResource();
imageAnalysis.setAnalyzer(ContextCompat.getMainExecutor(this), mAnalyzer);
}
@@ -1358,20 +1543,20 @@
}
}
- private class SessionImagesUriSet {
- private final Set<Uri> mSessionImages;
+ private class SessionMediaUriSet {
+ private final Set<Uri> mSessionMediaUris;
- SessionImagesUriSet() {
- mSessionImages = Collections.synchronizedSet(new HashSet<>());
+ SessionMediaUriSet() {
+ mSessionMediaUris = Collections.synchronizedSet(new HashSet<>());
}
public void add(@NonNull Uri uri) {
- mSessionImages.add(uri);
+ mSessionMediaUris.add(uri);
}
public void deleteAllUris() {
- synchronized (mSessionImages) {
- Iterator<Uri> it = mSessionImages.iterator();
+ synchronized (mSessionMediaUris) {
+ Iterator<Uri> it = mSessionMediaUris.iterator();
while (it.hasNext()) {
getContentResolver().delete(it.next(), null, null);
it.remove();
@@ -1501,6 +1686,11 @@
return findUseCase(VideoCapture.class);
}
+ @VisibleForTesting
+ void setVideoCaptureAutoStopLength(long autoStopLengthInMs) {
+ mVideoCaptureAutoStopLength = autoStopLengthInMs;
+ }
+
/**
* Finds the use case by the given class.
*/
@@ -1518,6 +1708,12 @@
@VisibleForTesting
@Nullable
+ public Camera getCamera() {
+ return mCamera;
+ }
+
+ @VisibleForTesting
+ @Nullable
CameraInfo getCameraInfo() {
return mCamera != null ? mCamera.getCameraInfo() : null;
}
@@ -1593,4 +1789,21 @@
throw new IllegalArgumentException("Undefined item id: " + itemId);
}
}
+
+ private static CameraSelector createCameraSelectorById(@Nullable String cameraId) {
+ return new CameraSelector.Builder().addCameraFilter(new CameraFilter() {
+ @NonNull
+ @Override
+ @OptIn(markerClass = ExperimentalCamera2Interop.class)
+ public List<CameraInfo> filter(@NonNull List<CameraInfo> cameraInfos) {
+ for (CameraInfo cameraInfo : cameraInfos) {
+ if (cameraId.equals(Camera2CameraInfo.from(cameraInfo).getCameraId())) {
+ return Collections.singletonList(cameraInfo);
+ }
+ }
+
+ throw new IllegalArgumentException("No camera can be find for id: " + cameraId);
+ }
+ }).build();
+ }
}
diff --git a/camera/integration-tests/extensionstestapp/src/androidTest/java/androidx/camera/integration/extensions/LifecycleStatusChangeStressTest.kt b/camera/integration-tests/extensionstestapp/src/androidTest/java/androidx/camera/integration/extensions/LifecycleStatusChangeStressTest.kt
index ba07cb2..ca77bfa 100644
--- a/camera/integration-tests/extensionstestapp/src/androidTest/java/androidx/camera/integration/extensions/LifecycleStatusChangeStressTest.kt
+++ b/camera/integration-tests/extensionstestapp/src/androidTest/java/androidx/camera/integration/extensions/LifecycleStatusChangeStressTest.kt
@@ -20,7 +20,6 @@
import android.content.Context
import android.content.Intent
import androidx.camera.camera2.Camera2Config
-import androidx.camera.extensions.ExtensionMode
import androidx.camera.integration.extensions.util.ExtensionsTestUtil
import androidx.camera.integration.extensions.util.ExtensionsTestUtil.STRESS_TEST_OPERATION_REPEAT_COUNT
import androidx.camera.testing.CameraUtil
@@ -40,6 +39,7 @@
import androidx.test.filters.LargeTest
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.rule.GrantPermissionRule
+import androidx.test.uiautomator.UiDevice
import androidx.testutils.RepeatRule
import com.google.common.truth.Truth.assertThat
import org.junit.After
@@ -63,6 +63,7 @@
private val cameraId: String,
private val extensionMode: Int
) {
+ private val device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
@get:Rule
val useCamera = CameraUtil.grantCameraPermissionAndPreTest(
@@ -91,21 +92,28 @@
@Parameterized.Parameters(name = "cameraId = {0}, extensionMode = {1}")
@JvmStatic
- fun parameters() = ExtensionsTestUtil.getAllCameraIdModeCombinations()
+ fun parameters() = ExtensionsTestUtil.getAllCameraIdExtensionModeCombinations()
}
@Before
fun setUp() {
- if (extensionMode != ExtensionMode.NONE) {
- assumeTrue(ExtensionsTestUtil.isTargetDeviceAvailableForExtensions())
- }
+ assumeTrue(ExtensionsTestUtil.isTargetDeviceAvailableForExtensions())
// Clear the device UI and check if there is no dialog or lock screen on the top of the
// window before starting the test.
CoreAppTestUtil.prepareDeviceUI(InstrumentationRegistry.getInstrumentation())
+ // Use the natural orientation throughout these tests to ensure the activity isn't
+ // recreated unexpectedly. This will also freeze the sensors until
+ // mDevice.unfreezeRotation() in the tearDown() method. Any simulated rotations will be
+ // explicitly initiated from within the test.
+ device.setOrientationNatural()
}
@After
fun tearDown() {
+ // Unfreeze rotation so the device can choose the orientation via its own policy. Be nice
+ // to other tests :)
+ device.unfreezeRotation()
+
if (::activityScenario.isInitialized) {
activityScenario.onActivity { it.finish() }
}
diff --git a/camera/integration-tests/extensionstestapp/src/androidTest/java/androidx/camera/integration/extensions/SwitchAvailableModesStressTest.kt b/camera/integration-tests/extensionstestapp/src/androidTest/java/androidx/camera/integration/extensions/SwitchAvailableModesStressTest.kt
index 9248ad0..1c35c37 100644
--- a/camera/integration-tests/extensionstestapp/src/androidTest/java/androidx/camera/integration/extensions/SwitchAvailableModesStressTest.kt
+++ b/camera/integration-tests/extensionstestapp/src/androidTest/java/androidx/camera/integration/extensions/SwitchAvailableModesStressTest.kt
@@ -38,6 +38,7 @@
import androidx.test.filters.LargeTest
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.rule.GrantPermissionRule
+import androidx.test.uiautomator.UiDevice
import androidx.testutils.RepeatRule
import org.junit.After
import org.junit.Assume.assumeTrue
@@ -56,6 +57,7 @@
@LargeTest
@RunWith(Parameterized::class)
class SwitchAvailableModesStressTest(private val cameraId: String) {
+ private val device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
@get:Rule
val useCamera = CameraUtil.grantCameraPermissionAndPreTest(
@@ -93,10 +95,19 @@
// Clear the device UI and check if there is no dialog or lock screen on the top of the
// window before starting the test.
CoreAppTestUtil.prepareDeviceUI(InstrumentationRegistry.getInstrumentation())
+ // Use the natural orientation throughout these tests to ensure the activity isn't
+ // recreated unexpectedly. This will also freeze the sensors until
+ // mDevice.unfreezeRotation() in the tearDown() method. Any simulated rotations will be
+ // explicitly initiated from within the test.
+ device.setOrientationNatural()
}
@After
fun tearDown() {
+ // Unfreeze rotation so the device can choose the orientation via its own policy. Be nice
+ // to other tests :)
+ device.unfreezeRotation()
+
if (::activityScenario.isInitialized) {
activityScenario.onActivity { it.finish() }
}
diff --git a/camera/integration-tests/extensionstestapp/src/androidTest/java/androidx/camera/integration/extensions/SwitchCameraStressTest.kt b/camera/integration-tests/extensionstestapp/src/androidTest/java/androidx/camera/integration/extensions/SwitchCameraStressTest.kt
index f3c09c0..36b9064 100644
--- a/camera/integration-tests/extensionstestapp/src/androidTest/java/androidx/camera/integration/extensions/SwitchCameraStressTest.kt
+++ b/camera/integration-tests/extensionstestapp/src/androidTest/java/androidx/camera/integration/extensions/SwitchCameraStressTest.kt
@@ -39,6 +39,7 @@
import androidx.test.filters.LargeTest
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.rule.GrantPermissionRule
+import androidx.test.uiautomator.UiDevice
import androidx.testutils.RepeatRule
import org.junit.After
import org.junit.Assume.assumeTrue
@@ -57,6 +58,7 @@
@LargeTest
@RunWith(Parameterized::class)
class SwitchCameraStressTest(private val extensionMode: Int) {
+ private val device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
@get:Rule
val useCamera = CameraUtil.grantCameraPermissionAndPreTest(
@@ -86,7 +88,6 @@
@Parameterized.Parameters(name = "extensionMode = {0}")
@JvmStatic
fun parameters() = arrayOf(
- ExtensionMode.NONE,
ExtensionMode.BOKEH,
ExtensionMode.HDR,
ExtensionMode.NIGHT,
@@ -97,16 +98,23 @@
@Before
fun setUp() {
- if (extensionMode != ExtensionMode.NONE) {
- assumeTrue(ExtensionsTestUtil.isTargetDeviceAvailableForExtensions())
- }
+ assumeTrue(ExtensionsTestUtil.isTargetDeviceAvailableForExtensions())
// Clear the device UI and check if there is no dialog or lock screen on the top of the
// window before starting the test.
CoreAppTestUtil.prepareDeviceUI(InstrumentationRegistry.getInstrumentation())
+ // Use the natural orientation throughout these tests to ensure the activity isn't
+ // recreated unexpectedly. This will also freeze the sensors until
+ // mDevice.unfreezeRotation() in the tearDown() method. Any simulated rotations will be
+ // explicitly initiated from within the test.
+ device.setOrientationNatural()
}
@After
fun tearDown() {
+ // Unfreeze rotation so the device can choose the orientation via its own policy. Be nice
+ // to other tests :)
+ device.unfreezeRotation()
+
if (::activityScenario.isInitialized) {
activityScenario.onActivity { it.finish() }
}
diff --git a/glance/glance/api/current.txt b/glance/glance/api/current.txt
index 70c8b44..11f94b4 100644
--- a/glance/glance/api/current.txt
+++ b/glance/glance/api/current.txt
@@ -631,6 +631,7 @@
@androidx.compose.runtime.Immutable public final class TextStyle {
ctor public TextStyle(optional androidx.glance.unit.ColorProvider? color, optional androidx.compose.ui.unit.TextUnit? fontSize, optional androidx.glance.text.FontWeight? fontWeight, optional androidx.glance.text.FontStyle? fontStyle, optional androidx.glance.text.TextAlign? textAlign, optional androidx.glance.text.TextDecoration? textDecoration);
+ method public androidx.glance.text.TextStyle copy(optional androidx.glance.unit.ColorProvider? color, optional androidx.compose.ui.unit.TextUnit? fontSize, optional androidx.glance.text.FontWeight? fontWeight, optional androidx.glance.text.FontStyle? fontStyle, optional androidx.glance.text.TextAlign? textAlign, optional androidx.glance.text.TextDecoration? textDecoration);
method public androidx.glance.unit.ColorProvider? getColor();
method public androidx.compose.ui.unit.TextUnit? getFontSize();
method public androidx.glance.text.FontStyle? getFontStyle();
diff --git a/glance/glance/api/public_plus_experimental_current.txt b/glance/glance/api/public_plus_experimental_current.txt
index 70c8b44..11f94b4 100644
--- a/glance/glance/api/public_plus_experimental_current.txt
+++ b/glance/glance/api/public_plus_experimental_current.txt
@@ -631,6 +631,7 @@
@androidx.compose.runtime.Immutable public final class TextStyle {
ctor public TextStyle(optional androidx.glance.unit.ColorProvider? color, optional androidx.compose.ui.unit.TextUnit? fontSize, optional androidx.glance.text.FontWeight? fontWeight, optional androidx.glance.text.FontStyle? fontStyle, optional androidx.glance.text.TextAlign? textAlign, optional androidx.glance.text.TextDecoration? textDecoration);
+ method public androidx.glance.text.TextStyle copy(optional androidx.glance.unit.ColorProvider? color, optional androidx.compose.ui.unit.TextUnit? fontSize, optional androidx.glance.text.FontWeight? fontWeight, optional androidx.glance.text.FontStyle? fontStyle, optional androidx.glance.text.TextAlign? textAlign, optional androidx.glance.text.TextDecoration? textDecoration);
method public androidx.glance.unit.ColorProvider? getColor();
method public androidx.compose.ui.unit.TextUnit? getFontSize();
method public androidx.glance.text.FontStyle? getFontStyle();
diff --git a/glance/glance/api/restricted_current.txt b/glance/glance/api/restricted_current.txt
index 70c8b44..11f94b4 100644
--- a/glance/glance/api/restricted_current.txt
+++ b/glance/glance/api/restricted_current.txt
@@ -631,6 +631,7 @@
@androidx.compose.runtime.Immutable public final class TextStyle {
ctor public TextStyle(optional androidx.glance.unit.ColorProvider? color, optional androidx.compose.ui.unit.TextUnit? fontSize, optional androidx.glance.text.FontWeight? fontWeight, optional androidx.glance.text.FontStyle? fontStyle, optional androidx.glance.text.TextAlign? textAlign, optional androidx.glance.text.TextDecoration? textDecoration);
+ method public androidx.glance.text.TextStyle copy(optional androidx.glance.unit.ColorProvider? color, optional androidx.compose.ui.unit.TextUnit? fontSize, optional androidx.glance.text.FontWeight? fontWeight, optional androidx.glance.text.FontStyle? fontStyle, optional androidx.glance.text.TextAlign? textAlign, optional androidx.glance.text.TextDecoration? textDecoration);
method public androidx.glance.unit.ColorProvider? getColor();
method public androidx.compose.ui.unit.TextUnit? getFontSize();
method public androidx.glance.text.FontStyle? getFontStyle();
diff --git a/glance/glance/src/androidMain/kotlin/androidx/glance/text/TextStyle.kt b/glance/glance/src/androidMain/kotlin/androidx/glance/text/TextStyle.kt
index f1c6d5b..be3f8b2 100644
--- a/glance/glance/src/androidMain/kotlin/androidx/glance/text/TextStyle.kt
+++ b/glance/glance/src/androidMain/kotlin/androidx/glance/text/TextStyle.kt
@@ -32,6 +32,22 @@
val textAlign: TextAlign? = null,
val textDecoration: TextDecoration? = null,
) {
+ fun copy(
+ color: ColorProvider? = this.color,
+ fontSize: TextUnit? = this.fontSize,
+ fontWeight: FontWeight? = this.fontWeight,
+ fontStyle: FontStyle? = this.fontStyle,
+ textAlign: TextAlign? = this.textAlign,
+ textDecoration: TextDecoration? = this.textDecoration
+ ) = TextStyle(
+ color = color,
+ fontSize = fontSize,
+ fontWeight = fontWeight,
+ fontStyle = fontStyle,
+ textAlign = textAlign,
+ textDecoration = textDecoration
+ )
+
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is TextStyle) return false
diff --git a/wear/tiles/tiles-material/src/androidTest/java/androidx/wear/tiles/material/layouts/TestCasesGenerator.java b/wear/tiles/tiles-material/src/androidTest/java/androidx/wear/tiles/material/layouts/TestCasesGenerator.java
index 9738382..8a5ca91 100644
--- a/wear/tiles/tiles-material/src/androidTest/java/androidx/wear/tiles/material/layouts/TestCasesGenerator.java
+++ b/wear/tiles/tiles-material/src/androidTest/java/androidx/wear/tiles/material/layouts/TestCasesGenerator.java
@@ -28,12 +28,15 @@
import androidx.wear.tiles.ActionBuilders.LaunchAction;
import androidx.wear.tiles.DeviceParametersBuilders.DeviceParameters;
import androidx.wear.tiles.LayoutElementBuilders.Box;
+import androidx.wear.tiles.LayoutElementBuilders.Column;
import androidx.wear.tiles.LayoutElementBuilders.LayoutElement;
+import androidx.wear.tiles.LayoutElementBuilders.Spacer;
import androidx.wear.tiles.ModifiersBuilders.Background;
import androidx.wear.tiles.ModifiersBuilders.Clickable;
import androidx.wear.tiles.ModifiersBuilders.Modifiers;
import androidx.wear.tiles.material.Button;
import androidx.wear.tiles.material.ButtonDefaults;
+import androidx.wear.tiles.material.Chip;
import androidx.wear.tiles.material.ChipColors;
import androidx.wear.tiles.material.CircularProgressIndicator;
import androidx.wear.tiles.material.Colors;
@@ -129,6 +132,33 @@
.setPrimaryChipContent(primaryChipBuilder.build())
.setContent(buildColoredBox(Color.YELLOW))
.build());
+ testCases.put(
+ "two_chips_content_primarychiplayout_golden" + goldenSuffix,
+ new PrimaryLayout.Builder(deviceParameters)
+ .setPrimaryChipContent(primaryChipBuilder.build())
+ .setContent(
+ new Column.Builder()
+ .setWidth(expand())
+ .setHeight(expand())
+ .addContent(
+ new Chip.Builder(
+ context,
+ clickable,
+ deviceParameters)
+ .setPrimaryLabelContent("First chip")
+ .setWidth(expand())
+ .build())
+ .addContent(new Spacer.Builder().setHeight(dp(4)).build())
+ .addContent(
+ new Chip.Builder(
+ context,
+ clickable,
+ deviceParameters)
+ .setPrimaryLabelContent("Second chip")
+ .setWidth(expand())
+ .build())
+ .build())
+ .build());
primaryChipBuilder =
new CompactChip.Builder(context, "Action", clickable, deviceParameters);
diff --git a/wear/tiles/tiles-material/src/main/java/androidx/wear/tiles/material/ChipDefaults.java b/wear/tiles/tiles-material/src/main/java/androidx/wear/tiles/material/ChipDefaults.java
index 192ad83..e75135e 100644
--- a/wear/tiles/tiles-material/src/main/java/androidx/wear/tiles/material/ChipDefaults.java
+++ b/wear/tiles/tiles-material/src/main/java/androidx/wear/tiles/material/ChipDefaults.java
@@ -46,6 +46,15 @@
public static final DpProp COMPACT_HEIGHT = dp(32);
/**
+ * The default height of tappable area for standard {@link CompactChip}
+ *
+ * @hide
+ */
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ @NonNull
+ public static final DpProp COMPACT_HEIGHT_TAPPABLE = dp(48);
+
+ /**
* The default height for standard {@link TitleChip}
*
* @hide
diff --git a/wear/tiles/tiles-material/src/main/java/androidx/wear/tiles/material/CompactChip.java b/wear/tiles/tiles-material/src/main/java/androidx/wear/tiles/material/CompactChip.java
index a7db44e8..21bb316 100644
--- a/wear/tiles/tiles-material/src/main/java/androidx/wear/tiles/material/CompactChip.java
+++ b/wear/tiles/tiles-material/src/main/java/androidx/wear/tiles/material/CompactChip.java
@@ -16,12 +16,15 @@
package androidx.wear.tiles.material;
+import static androidx.wear.tiles.DimensionBuilders.wrap;
import static androidx.wear.tiles.LayoutElementBuilders.HORIZONTAL_ALIGN_CENTER;
import static androidx.wear.tiles.material.ChipDefaults.COMPACT_HEIGHT;
+import static androidx.wear.tiles.material.ChipDefaults.COMPACT_HEIGHT_TAPPABLE;
import static androidx.wear.tiles.material.ChipDefaults.COMPACT_HORIZONTAL_PADDING;
import static androidx.wear.tiles.material.ChipDefaults.COMPACT_PRIMARY_COLORS;
import static androidx.wear.tiles.material.Helper.checkNotNull;
import static androidx.wear.tiles.material.Helper.checkTag;
+import static androidx.wear.tiles.material.Helper.getTagBytes;
import android.content.Context;
@@ -30,10 +33,12 @@
import androidx.annotation.RestrictTo;
import androidx.annotation.RestrictTo.Scope;
import androidx.wear.tiles.DeviceParametersBuilders.DeviceParameters;
-import androidx.wear.tiles.DimensionBuilders.WrappedDimensionProp;
+import androidx.wear.tiles.LayoutElementBuilders;
import androidx.wear.tiles.LayoutElementBuilders.Box;
import androidx.wear.tiles.LayoutElementBuilders.LayoutElement;
import androidx.wear.tiles.ModifiersBuilders.Clickable;
+import androidx.wear.tiles.ModifiersBuilders.ElementMetadata;
+import androidx.wear.tiles.ModifiersBuilders.Modifiers;
import androidx.wear.tiles.proto.LayoutElementProto;
/**
@@ -70,10 +75,13 @@
/** Tool tag for Metadata in Modifiers, so we know that Box is actually a CompactChip. */
static final String METADATA_TAG = "CMPCHP";
+ @NonNull private final Box mImpl;
@NonNull private final Chip mElement;
- CompactChip(@NonNull Chip element) {
- this.mElement = element;
+ CompactChip(@NonNull Box element) {
+ this.mImpl = element;
+ // We know for sure that content of the Box is Chip.
+ this.mElement = new Chip((Box) element.getContents().get(0));
}
/** Builder class for {@link androidx.wear.tiles.material.CompactChip}. */
@@ -126,7 +134,7 @@
.setChipColors(mChipColors)
.setContentDescription(mText)
.setHorizontalAlignment(HORIZONTAL_ALIGN_CENTER)
- .setWidth(new WrappedDimensionProp.Builder().build())
+ .setWidth(wrap())
.setHeight(COMPACT_HEIGHT)
.setMaxLines(1)
.setHorizontalPadding(COMPACT_HORIZONTAL_PADDING)
@@ -134,7 +142,23 @@
.setPrimaryLabelTypography(Typography.TYPOGRAPHY_CAPTION1)
.setIsPrimaryLabelScalable(false);
- return new CompactChip(chipBuilder.build());
+ Box tappableChip =
+ new Box.Builder()
+ .setModifiers(
+ new Modifiers.Builder()
+ .setClickable(mClickable)
+ .setMetadata(
+ new ElementMetadata.Builder()
+ .setTagData(getTagBytes(METADATA_TAG))
+ .build())
+ .build())
+ .setWidth(wrap())
+ .setHeight(COMPACT_HEIGHT_TAPPABLE)
+ .setVerticalAlignment(LayoutElementBuilders.VERTICAL_ALIGN_CENTER)
+ .addContent(chipBuilder.build())
+ .build();
+
+ return new CompactChip(tappableChip);
}
}
@@ -179,8 +203,18 @@
if (!checkTag(boxElement.getModifiers(), METADATA_TAG)) {
return null;
}
+ // Now to check that inner content of the Box is CompactChip's Chip.
+ LayoutElement innerElement = boxElement.getContents().get(0);
+ if (!(innerElement instanceof Box)) {
+ return null;
+ }
+ Box innerBoxElement = (Box) innerElement;
+ if (!checkTag(innerBoxElement.getModifiers(), METADATA_TAG)) {
+ return null;
+ }
+
// Now we are sure that this element is a CompactChip.
- return new CompactChip(new Chip(boxElement));
+ return new CompactChip(boxElement);
}
/** @hide */
@@ -188,6 +222,6 @@
@NonNull
@Override
public LayoutElementProto.LayoutElement toLayoutElementProto() {
- return mElement.toLayoutElementProto();
+ return mImpl.toLayoutElementProto();
}
}
diff --git a/wear/tiles/tiles-material/src/main/java/androidx/wear/tiles/material/layouts/LayoutDefaults.java b/wear/tiles/tiles-material/src/main/java/androidx/wear/tiles/material/layouts/LayoutDefaults.java
index c56a119..b540215 100644
--- a/wear/tiles/tiles-material/src/main/java/androidx/wear/tiles/material/layouts/LayoutDefaults.java
+++ b/wear/tiles/tiles-material/src/main/java/androidx/wear/tiles/material/layouts/LayoutDefaults.java
@@ -28,12 +28,12 @@
/**
* The default percentage for the bottom margin for primary chip in the {@link PrimaryLayout}.
*/
- static final float PRIMARY_LAYOUT_MARGIN_BOTTOM_ROUND_PERCENT = 6.3f / 100;
+ static final float PRIMARY_LAYOUT_MARGIN_BOTTOM_ROUND_PERCENT = 2.1f / 100;
/**
* The default percentage for the bottom margin for primary chip in the {@link PrimaryLayout}.
*/
- static final float PRIMARY_LAYOUT_MARGIN_BOTTOM_SQUARE_PERCENT = 2.2f / 100;
+ static final float PRIMARY_LAYOUT_MARGIN_BOTTOM_SQUARE_PERCENT = 0;
/**
* The default percentage for the top margin for primary chip in the {@link PrimaryLayout} on
@@ -71,9 +71,6 @@
*/
static final float PRIMARY_LAYOUT_CHIP_HORIZONTAL_PADDING_SQUARE_DP = 0;
- /** The default spacer height for primary chip in the {@link PrimaryLayout}. */
- static final DpProp PRIMARY_LAYOUT_SPACER_HEIGHT = dp(12);
-
/** The default horizontal margin in the {@link EdgeContentLayout}. */
static final float EDGE_CONTENT_LAYOUT_MARGIN_HORIZONTAL_ROUND_DP = 14;
diff --git a/wear/tiles/tiles-material/src/main/java/androidx/wear/tiles/material/layouts/PrimaryLayout.java b/wear/tiles/tiles-material/src/main/java/androidx/wear/tiles/material/layouts/PrimaryLayout.java
index f4e207c..228e91a 100644
--- a/wear/tiles/tiles-material/src/main/java/androidx/wear/tiles/material/layouts/PrimaryLayout.java
+++ b/wear/tiles/tiles-material/src/main/java/androidx/wear/tiles/material/layouts/PrimaryLayout.java
@@ -20,7 +20,7 @@
import static androidx.wear.tiles.DimensionBuilders.dp;
import static androidx.wear.tiles.DimensionBuilders.expand;
import static androidx.wear.tiles.DimensionBuilders.wrap;
-import static androidx.wear.tiles.material.ChipDefaults.COMPACT_HEIGHT;
+import static androidx.wear.tiles.material.ChipDefaults.COMPACT_HEIGHT_TAPPABLE;
import static androidx.wear.tiles.material.Helper.checkNotNull;
import static androidx.wear.tiles.material.Helper.checkTag;
import static androidx.wear.tiles.material.Helper.getMetadataTagBytes;
@@ -35,7 +35,6 @@
import static androidx.wear.tiles.material.layouts.LayoutDefaults.PRIMARY_LAYOUT_MARGIN_HORIZONTAL_SQUARE_PERCENT;
import static androidx.wear.tiles.material.layouts.LayoutDefaults.PRIMARY_LAYOUT_MARGIN_TOP_ROUND_PERCENT;
import static androidx.wear.tiles.material.layouts.LayoutDefaults.PRIMARY_LAYOUT_MARGIN_TOP_SQUARE_PERCENT;
-import static androidx.wear.tiles.material.layouts.LayoutDefaults.PRIMARY_LAYOUT_SPACER_HEIGHT;
import android.annotation.SuppressLint;
@@ -235,10 +234,7 @@
float horizontalPadding = getHorizontalPadding();
float horizontalChipPadding = getChipHorizontalPadding();
- float primaryChipHeight =
- mPrimaryChip != null
- ? (COMPACT_HEIGHT.getValue() + PRIMARY_LAYOUT_SPACER_HEIGHT.getValue())
- : 0;
+ float primaryChipHeight = mPrimaryChip != null ? COMPACT_HEIGHT_TAPPABLE.getValue() : 0;
DpProp mainContentHeight =
dp(
@@ -292,31 +288,21 @@
layoutBuilder.addContent(innerContentBuilder.build());
if (mPrimaryChip != null) {
- layoutBuilder
- .addContent(
- new Spacer.Builder()
- .setHeight(PRIMARY_LAYOUT_SPACER_HEIGHT)
- .build())
- .addContent(
- new Box.Builder()
- .setVerticalAlignment(
- LayoutElementBuilders.VERTICAL_ALIGN_BOTTOM)
- .setWidth(expand())
- .setHeight(wrap())
- .setModifiers(
+ layoutBuilder.addContent(
+ new Box.Builder()
+ .setVerticalAlignment(LayoutElementBuilders.VERTICAL_ALIGN_BOTTOM)
+ .setWidth(expand())
+ .setHeight(wrap())
+ .setModifiers(
new Modifiers.Builder()
- .setPadding(
- new Padding.Builder()
- .setStart(
- dp(
- horizontalChipPadding))
- .setEnd(
- dp(
- horizontalChipPadding))
- .build())
- .build())
- .addContent(mPrimaryChip)
- .build());
+ .setPadding(
+ new Padding.Builder()
+ .setStart(dp(horizontalChipPadding))
+ .setEnd(dp(horizontalChipPadding))
+ .build())
+ .build())
+ .addContent(mPrimaryChip)
+ .build());
}
byte[] metadata = METADATA_TAG_BASE.clone();
@@ -424,7 +410,7 @@
@Nullable
public LayoutElement getPrimaryChipContent() {
if (areElementsPresent(CHIP_PRESENT)) {
- return ((Box) mAllContent.get(2)).getContents().get(0);
+ return ((Box) mAllContent.get(1)).getContents().get(0);
}
return null;
}