Merge "Migrate to Guava 27.0.1 and use new ListenableFuture." into androidx-master-dev
am: 14c45092d6
Change-Id: Id64aaf73930a22139172547b3703c6a62f9b5f73
diff --git a/camera/.gitignore b/camera/.gitignore
new file mode 100644
index 0000000..f43947b
--- /dev/null
+++ b/camera/.gitignore
@@ -0,0 +1,5 @@
+local.properties
+**/build
+maven-repo/
+*.DS_Store
+
diff --git a/camera/OWNERS b/camera/OWNERS
new file mode 100644
index 0000000..f912330
--- /dev/null
+++ b/camera/OWNERS
@@ -0,0 +1,6 @@
+fungja@google.com
+nilknarfuw@google.com
+trevormcguire@google.com
+dmchen@google.com
+vinitmodi@google.com
+ericng@google.com
diff --git a/camera/camera2/proguard.flags b/camera/camera2/proguard.flags
new file mode 100644
index 0000000..9cfa301
--- /dev/null
+++ b/camera/camera2/proguard.flags
@@ -0,0 +1,74 @@
+# Copyright (C) 2019 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.
+
+# Save the obfuscation mapping to a file, so we can de-obfuscate any stack
+# traces later on. Keep a fixed source file attribute and all line number
+# tables to get line numbers in the stack traces.
+# You can comment this out if you're not interested in stack traces.
+
+-printmapping out.map
+-keepparameternames
+-renamesourcefileattribute SourceFile
+-keepattributes Exceptions,InnerClasses,Deprecated,
+ SourceFile,LineNumberTable,EnclosingMethod
+
+# Preserve all annotations.
+
+-keepattributes *Annotation*
+
+# Preserve all public classes, and their public and protected fields and
+# methods.
+
+-keep public class * {
+ public protected *;
+}
+
+# Preserve all .class method names.
+
+-keepclassmembernames class * {
+ java.lang.Class class$(java.lang.String);
+ java.lang.Class class$(java.lang.String, boolean);
+}
+
+# Preserve all native method names and the names of their classes.
+
+-keepclasseswithmembernames class * {
+ native <methods>;
+}
+
+# Preserve the special static methods that are required in all enumeration
+# classes.
+
+-keepclassmembers class * extends java.lang.Enum {
+ public static **[] values();
+ public static ** valueOf(java.lang.String);
+}
+
+# Explicitly preserve all serialization members. The Serializable interface
+# is only a marker interface, so it wouldn't save them.
+# You can comment this out if your library doesn't use serialization.
+# If your code contains serializable classes that have to be backward
+# compatible, please refer to the manual.
+
+-keepclassmembers class * implements java.io.Serializable {
+ static final long serialVersionUID;
+ static final java.io.ObjectStreamField[] serialPersistentFields;
+ private void writeObject(java.io.ObjectOutputStream);
+ private void readObject(java.io.ObjectInputStream);
+ java.lang.Object writeReplace();
+ java.lang.Object readResolve();
+}
+
+# Keep generic types for the TypeReference class
+-keepattributes Signature
diff --git a/camera/camera2/src/androidTest/AndroidManifest.xml b/camera/camera2/src/androidTest/AndroidManifest.xml
new file mode 100644
index 0000000..ecb6540
--- /dev/null
+++ b/camera/camera2/src/androidTest/AndroidManifest.xml
@@ -0,0 +1,54 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2019 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.
+-->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="androidx.camera.camera2">
+
+ <uses-sdk
+ android:minSdkVersion="21"
+ android:targetSdkVersion="27" />
+
+ <uses-permission android:name="android.permission.CAMERA" />
+ <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
+ <uses-permission android:name="android.permission.RECORD_AUDIO" />
+
+ <uses-feature android:name="android.hardware.camera" />
+
+ <application android:debuggable="true">
+ <uses-library
+ android:name="android.test.runner"
+ android:required="false" />
+ <uses-library
+ android:name="android.test.base"
+ android:required="false" />
+ <uses-library
+ android:name="android.test.mock"
+ android:required="false" />
+
+ <activity
+ android:name="androidx.camera.core.FakeActivity"
+ android:label="Fake Activity">
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN" />
+ <category android:name="android.intent.category.LAUNCHER" />
+ </intent-filter>
+ </activity>
+ </application>
+
+ <instrumentation
+ android:name="com.google.android.apps.common.testing.testrunner.Google3InstrumentationTestRunner"
+ android:targetPackage="androidx.camera.camera2">
+ </instrumentation>
+</manifest>
diff --git a/camera/camera2/src/androidTest/java/androidx/camera/camera2/Camera2CameraCaptureResultAndroidTest.java b/camera/camera2/src/androidTest/java/androidx/camera/camera2/Camera2CameraCaptureResultAndroidTest.java
new file mode 100644
index 0000000..090db55
--- /dev/null
+++ b/camera/camera2/src/androidTest/java/androidx/camera/camera2/Camera2CameraCaptureResultAndroidTest.java
@@ -0,0 +1,274 @@
+/*
+ * Copyright (C) 2019 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.camera2;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.when;
+
+import android.hardware.camera2.CaptureResult;
+
+import androidx.camera.core.CameraCaptureMetaData.AeState;
+import androidx.camera.core.CameraCaptureMetaData.AfMode;
+import androidx.camera.core.CameraCaptureMetaData.AfState;
+import androidx.camera.core.CameraCaptureMetaData.AwbState;
+import androidx.camera.core.CameraCaptureMetaData.FlashState;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.Mockito;
+
+@RunWith(JUnit4.class)
+public final class Camera2CameraCaptureResultAndroidTest {
+
+ private CaptureResult captureResult;
+ private Camera2CameraCaptureResult cameraCaptureResult;
+
+ @Before
+ public void setUp() {
+ captureResult = Mockito.mock(CaptureResult.class);
+ cameraCaptureResult = new Camera2CameraCaptureResult(captureResult);
+ }
+
+ @Test
+ public void getAfMode_withNull() {
+ when(captureResult.get(CaptureResult.CONTROL_AF_MODE)).thenReturn(null);
+ assertThat(cameraCaptureResult.getAfMode()).isEqualTo(AfMode.UNKNOWN);
+ }
+
+ @Test
+ public void getAfMode_withAfModeOff() {
+ when(captureResult.get(CaptureResult.CONTROL_AF_MODE))
+ .thenReturn(CaptureResult.CONTROL_AF_MODE_OFF);
+ assertThat(cameraCaptureResult.getAfMode()).isEqualTo(AfMode.OFF);
+ }
+
+ @Test
+ public void getAfMode_withAfModeEdof() {
+ when(captureResult.get(CaptureResult.CONTROL_AF_MODE))
+ .thenReturn(CaptureResult.CONTROL_AF_MODE_EDOF);
+ assertThat(cameraCaptureResult.getAfMode()).isEqualTo(AfMode.OFF);
+ }
+
+ @Test
+ public void getAfMode_withAfModeAuto() {
+ when(captureResult.get(CaptureResult.CONTROL_AF_MODE))
+ .thenReturn(CaptureResult.CONTROL_AF_MODE_AUTO);
+ assertThat(cameraCaptureResult.getAfMode()).isEqualTo(AfMode.ON_MANUAL_AUTO);
+ }
+
+ @Test
+ public void getAfMode_withAfModeMacro() {
+ when(captureResult.get(CaptureResult.CONTROL_AF_MODE))
+ .thenReturn(CaptureResult.CONTROL_AF_MODE_MACRO);
+ assertThat(cameraCaptureResult.getAfMode()).isEqualTo(AfMode.ON_MANUAL_AUTO);
+ }
+
+ @Test
+ public void getAfMode_withAfModeContinuousPicture() {
+ when(captureResult.get(CaptureResult.CONTROL_AF_MODE))
+ .thenReturn(CaptureResult.CONTROL_AF_MODE_CONTINUOUS_PICTURE);
+ assertThat(cameraCaptureResult.getAfMode()).isEqualTo(AfMode.ON_CONTINUOUS_AUTO);
+ }
+
+ @Test
+ public void getAfMode_withAfModeContinuousVideo() {
+ when(captureResult.get(CaptureResult.CONTROL_AF_MODE))
+ .thenReturn(CaptureResult.CONTROL_AF_MODE_CONTINUOUS_VIDEO);
+ assertThat(cameraCaptureResult.getAfMode()).isEqualTo(AfMode.ON_CONTINUOUS_AUTO);
+ }
+
+ @Test
+ public void getAfState_withNull() {
+ when(captureResult.get(CaptureResult.CONTROL_AF_STATE)).thenReturn(null);
+ assertThat(cameraCaptureResult.getAfState()).isEqualTo(AfState.UNKNOWN);
+ }
+
+ @Test
+ public void getAfState_withAfStateInactive() {
+ when(captureResult.get(CaptureResult.CONTROL_AF_STATE))
+ .thenReturn(CaptureResult.CONTROL_AF_STATE_INACTIVE);
+ assertThat(cameraCaptureResult.getAfState()).isEqualTo(AfState.INACTIVE);
+ }
+
+ @Test
+ public void getAfState_withAfStateActiveScan() {
+ when(captureResult.get(CaptureResult.CONTROL_AF_STATE))
+ .thenReturn(CaptureResult.CONTROL_AF_STATE_ACTIVE_SCAN);
+ assertThat(cameraCaptureResult.getAfState()).isEqualTo(AfState.SCANNING);
+ }
+
+ @Test
+ public void getAfState_withAfStatePassiveScan() {
+ when(captureResult.get(CaptureResult.CONTROL_AF_STATE))
+ .thenReturn(CaptureResult.CONTROL_AF_STATE_PASSIVE_SCAN);
+ assertThat(cameraCaptureResult.getAfState()).isEqualTo(AfState.SCANNING);
+ }
+
+ @Test
+ public void getAfState_withAfStatePassiveUnfocused() {
+ when(captureResult.get(CaptureResult.CONTROL_AF_STATE))
+ .thenReturn(CaptureResult.CONTROL_AF_STATE_PASSIVE_UNFOCUSED);
+ assertThat(cameraCaptureResult.getAfState()).isEqualTo(AfState.SCANNING);
+ }
+
+ @Test
+ public void getAfState_withAfStatePassiveFocused() {
+ when(captureResult.get(CaptureResult.CONTROL_AF_STATE))
+ .thenReturn(CaptureResult.CONTROL_AF_STATE_PASSIVE_FOCUSED);
+ assertThat(cameraCaptureResult.getAfState()).isEqualTo(AfState.FOCUSED);
+ }
+
+ @Test
+ public void getAfState_withAfStateFocusedLocked() {
+ when(captureResult.get(CaptureResult.CONTROL_AF_STATE))
+ .thenReturn(CaptureResult.CONTROL_AF_STATE_FOCUSED_LOCKED);
+ assertThat(cameraCaptureResult.getAfState()).isEqualTo(AfState.LOCKED_FOCUSED);
+ }
+
+ @Test
+ public void getAfState_withAfStateNotFocusedLocked() {
+ when(captureResult.get(CaptureResult.CONTROL_AF_STATE))
+ .thenReturn(CaptureResult.CONTROL_AF_STATE_NOT_FOCUSED_LOCKED);
+ assertThat(cameraCaptureResult.getAfState()).isEqualTo(AfState.LOCKED_NOT_FOCUSED);
+ }
+
+ @Test
+ public void getAeState_withNull() {
+ when(captureResult.get(CaptureResult.CONTROL_AE_STATE)).thenReturn(null);
+ assertThat(cameraCaptureResult.getAeState()).isEqualTo(AeState.UNKNOWN);
+ }
+
+ @Test
+ public void getAeState_withAeStateInactive() {
+ when(captureResult.get(CaptureResult.CONTROL_AE_STATE))
+ .thenReturn(CaptureResult.CONTROL_AE_STATE_INACTIVE);
+ assertThat(cameraCaptureResult.getAeState()).isEqualTo(AeState.INACTIVE);
+ }
+
+ @Test
+ public void getAeState_withAeStateSearching() {
+ when(captureResult.get(CaptureResult.CONTROL_AE_STATE))
+ .thenReturn(CaptureResult.CONTROL_AE_STATE_SEARCHING);
+ assertThat(cameraCaptureResult.getAeState()).isEqualTo(AeState.SEARCHING);
+ }
+
+ @Test
+ public void getAeState_withAeStatePrecapture() {
+ when(captureResult.get(CaptureResult.CONTROL_AE_STATE))
+ .thenReturn(CaptureResult.CONTROL_AE_STATE_PRECAPTURE);
+ assertThat(cameraCaptureResult.getAeState()).isEqualTo(AeState.SEARCHING);
+ }
+
+ @Test
+ public void getAeState_withAeStateFlashRequired() {
+ when(captureResult.get(CaptureResult.CONTROL_AE_STATE))
+ .thenReturn(CaptureResult.CONTROL_AE_STATE_FLASH_REQUIRED);
+ assertThat(cameraCaptureResult.getAeState()).isEqualTo(AeState.FLASH_REQUIRED);
+ }
+
+ @Test
+ public void getAeState_withAeStateConverged() {
+ when(captureResult.get(CaptureResult.CONTROL_AE_STATE))
+ .thenReturn(CaptureResult.CONTROL_AE_STATE_CONVERGED);
+ assertThat(cameraCaptureResult.getAeState()).isEqualTo(AeState.CONVERGED);
+ }
+
+ @Test
+ public void getAeState_withAeStateLocked() {
+ when(captureResult.get(CaptureResult.CONTROL_AE_STATE))
+ .thenReturn(CaptureResult.CONTROL_AE_STATE_LOCKED);
+ assertThat(cameraCaptureResult.getAeState()).isEqualTo(AeState.LOCKED);
+ }
+
+ @Test
+ public void getAwbState_withNull() {
+ when(captureResult.get(CaptureResult.CONTROL_AWB_STATE)).thenReturn(null);
+ assertThat(cameraCaptureResult.getAwbState()).isEqualTo(AwbState.UNKNOWN);
+ }
+
+ @Test
+ public void getAwbState_withAwbStateInactive() {
+ when(captureResult.get(CaptureResult.CONTROL_AWB_STATE))
+ .thenReturn(CaptureResult.CONTROL_AWB_STATE_INACTIVE);
+ assertThat(cameraCaptureResult.getAwbState()).isEqualTo(AwbState.INACTIVE);
+ }
+
+ @Test
+ public void getAwbState_withAwbStateSearching() {
+ when(captureResult.get(CaptureResult.CONTROL_AWB_STATE))
+ .thenReturn(CaptureResult.CONTROL_AWB_STATE_SEARCHING);
+ assertThat(cameraCaptureResult.getAwbState()).isEqualTo(AwbState.METERING);
+ }
+
+ @Test
+ public void getAwbState_withAwbStateConverged() {
+ when(captureResult.get(CaptureResult.CONTROL_AWB_STATE))
+ .thenReturn(CaptureResult.CONTROL_AWB_STATE_CONVERGED);
+ assertThat(cameraCaptureResult.getAwbState()).isEqualTo(AwbState.CONVERGED);
+ }
+
+ @Test
+ public void getAwbState_withAwbStateLocked() {
+ when(captureResult.get(CaptureResult.CONTROL_AWB_STATE))
+ .thenReturn(CaptureResult.CONTROL_AWB_STATE_LOCKED);
+ assertThat(cameraCaptureResult.getAwbState()).isEqualTo(AwbState.LOCKED);
+ }
+
+ @Test
+ public void getFlashState_withNull() {
+ when(captureResult.get(CaptureResult.FLASH_STATE)).thenReturn(null);
+ assertThat(cameraCaptureResult.getFlashState()).isEqualTo(FlashState.UNKNOWN);
+ }
+
+ @Test
+ public void getFlashState_withFlashStateUnavailable() {
+ when(captureResult.get(CaptureResult.FLASH_STATE))
+ .thenReturn(CaptureResult.FLASH_STATE_UNAVAILABLE);
+ assertThat(cameraCaptureResult.getFlashState()).isEqualTo(FlashState.NONE);
+ }
+
+ @Test
+ public void getFlashState_withFlashStateCharging() {
+ when(captureResult.get(CaptureResult.FLASH_STATE))
+ .thenReturn(CaptureResult.FLASH_STATE_CHARGING);
+ assertThat(cameraCaptureResult.getFlashState()).isEqualTo(FlashState.NONE);
+ }
+
+ @Test
+ public void getFlashState_withFlashStateReady() {
+ when(captureResult.get(CaptureResult.FLASH_STATE))
+ .thenReturn(CaptureResult.FLASH_STATE_READY);
+ assertThat(cameraCaptureResult.getFlashState()).isEqualTo(FlashState.READY);
+ }
+
+ @Test
+ public void getFlashState_withFlashStateFired() {
+ when(captureResult.get(CaptureResult.FLASH_STATE))
+ .thenReturn(CaptureResult.FLASH_STATE_FIRED);
+ assertThat(cameraCaptureResult.getFlashState()).isEqualTo(FlashState.FIRED);
+ }
+
+ @Test
+ public void getFlashState_withFlashStatePartial() {
+ when(captureResult.get(CaptureResult.FLASH_STATE))
+ .thenReturn(CaptureResult.FLASH_STATE_PARTIAL);
+ assertThat(cameraCaptureResult.getFlashState()).isEqualTo(FlashState.FIRED);
+ }
+}
diff --git a/camera/camera2/src/androidTest/java/androidx/camera/camera2/Camera2CameraControlAndroidTest.java b/camera/camera2/src/androidTest/java/androidx/camera/camera2/Camera2CameraControlAndroidTest.java
new file mode 100644
index 0000000..fc6fcd3
--- /dev/null
+++ b/camera/camera2/src/androidTest/java/androidx/camera/camera2/Camera2CameraControlAndroidTest.java
@@ -0,0 +1,483 @@
+/*
+ * Copyright (C) 2019 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.camera2;
+
+import static android.hardware.camera2.CameraMetadata.CONTROL_AE_MODE_OFF;
+import static android.hardware.camera2.CameraMetadata.CONTROL_AE_MODE_ON;
+import static android.hardware.camera2.CameraMetadata.CONTROL_AE_MODE_ON_ALWAYS_FLASH;
+import static android.hardware.camera2.CameraMetadata.CONTROL_AE_MODE_ON_AUTO_FLASH;
+import static android.hardware.camera2.CameraMetadata.FLASH_MODE_OFF;
+import static android.hardware.camera2.CameraMetadata.FLASH_MODE_TORCH;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Matchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import android.graphics.Rect;
+import android.hardware.camera2.CaptureRequest;
+import android.hardware.camera2.params.MeteringRectangle;
+import android.os.Handler;
+
+import androidx.camera.core.CaptureRequestConfiguration;
+import androidx.camera.core.FlashMode;
+import androidx.camera.core.SessionConfiguration;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.ArgumentCaptor;
+
+@RunWith(JUnit4.class)
+public class Camera2CameraControlAndroidTest {
+
+ Camera2CameraControl camera2CameraControl;
+ Camera2RequestRunner camera2RequestRunner;
+
+ @Before
+ public void setUp() {
+ camera2RequestRunner = mock(Camera2RequestRunner.class);
+ camera2CameraControl = new Camera2CameraControl(camera2RequestRunner, new Handler());
+ }
+
+ @Test
+ public void setCropRegion_cropRectSetAndRepeatingRequestUpdated() {
+ Rect rect = new Rect(0, 0, 10, 10);
+
+ camera2CameraControl.setCropRegion(rect);
+ SessionConfiguration sessionConfiguration =
+ camera2CameraControl.getControlSessionConfiguration();
+ Camera2Configuration repeatingConfig =
+ new Camera2Configuration(sessionConfiguration.getImplementationOptions());
+ assertThat(repeatingConfig.getCaptureRequestOption(CaptureRequest.SCALER_CROP_REGION, null))
+ .isEqualTo(rect);
+ Camera2Configuration singleConfig =
+ new Camera2Configuration(camera2CameraControl.getSingleRequestImplOptions());
+ assertThat(singleConfig.getCaptureRequestOption(CaptureRequest.SCALER_CROP_REGION, null))
+ .isEqualTo(rect);
+ verify(camera2RequestRunner).updateRepeatingRequest();
+ }
+
+ @Test
+ public void focus_focusRectSetAndRequestsExecuted() {
+ Rect focusRect = new Rect(0, 0, 10, 10);
+ Rect meteringRect = new Rect(20, 20, 30, 30);
+
+ camera2CameraControl.focus(focusRect, meteringRect);
+
+ SessionConfiguration sessionConfiguration =
+ camera2CameraControl.getControlSessionConfiguration();
+ Camera2Configuration repeatingConfig =
+ new Camera2Configuration(sessionConfiguration.getImplementationOptions());
+
+ assertThat(
+ repeatingConfig.getCaptureRequestOption(
+ CaptureRequest.CONTROL_AF_REGIONS, (MeteringRectangle[]) null))
+ .isEqualTo(
+ new MeteringRectangle[]{
+ new MeteringRectangle(focusRect,
+ MeteringRectangle.METERING_WEIGHT_MAX)
+ });
+
+ assertThat(
+ repeatingConfig.getCaptureRequestOption(
+ CaptureRequest.CONTROL_AE_REGIONS, (MeteringRectangle[]) null))
+ .isEqualTo(
+ new MeteringRectangle[]{
+ new MeteringRectangle(
+ meteringRect, MeteringRectangle.METERING_WEIGHT_MAX)
+ });
+
+ assertThat(
+ repeatingConfig.getCaptureRequestOption(
+ CaptureRequest.CONTROL_AWB_REGIONS, (MeteringRectangle[]) null))
+ .isEqualTo(
+ new MeteringRectangle[]{
+ new MeteringRectangle(
+ meteringRect, MeteringRectangle.METERING_WEIGHT_MAX)
+ });
+
+ Camera2Configuration singleConfig =
+ new Camera2Configuration(camera2CameraControl.getSingleRequestImplOptions());
+ assertThat(
+ singleConfig.getCaptureRequestOption(
+ CaptureRequest.CONTROL_AF_REGIONS, (MeteringRectangle[]) null))
+ .isEqualTo(
+ new MeteringRectangle[]{
+ new MeteringRectangle(focusRect,
+ MeteringRectangle.METERING_WEIGHT_MAX)
+ });
+
+ assertThat(
+ singleConfig.getCaptureRequestOption(
+ CaptureRequest.CONTROL_AE_REGIONS, (MeteringRectangle[]) null))
+ .isEqualTo(
+ new MeteringRectangle[]{
+ new MeteringRectangle(
+ meteringRect, MeteringRectangle.METERING_WEIGHT_MAX)
+ });
+
+ assertThat(
+ singleConfig.getCaptureRequestOption(
+ CaptureRequest.CONTROL_AWB_REGIONS, (MeteringRectangle[]) null))
+ .isEqualTo(
+ new MeteringRectangle[]{
+ new MeteringRectangle(
+ meteringRect, MeteringRectangle.METERING_WEIGHT_MAX)
+ });
+
+ verify(camera2RequestRunner).updateRepeatingRequest();
+ assertThat(camera2CameraControl.isFocusLocked()).isTrue();
+
+ ArgumentCaptor<CaptureRequestConfiguration> argumentCaptor =
+ ArgumentCaptor.forClass(CaptureRequestConfiguration.class);
+ verify(camera2RequestRunner).submitSingleRequest(argumentCaptor.capture());
+ CaptureRequestConfiguration resultCaptureConfig = argumentCaptor.getValue();
+ assertThat(
+ resultCaptureConfig
+ .getCameraCharacteristics()
+ .get(CaptureRequest.CONTROL_AF_TRIGGER)
+ .getValue())
+ .isEqualTo(CaptureRequest.CONTROL_AF_TRIGGER_START);
+ }
+
+ @Test
+ public void cancelFocus_regionRestored() {
+ Rect focusRect = new Rect(0, 0, 10, 10);
+ Rect meteringRect = new Rect(20, 20, 30, 30);
+
+ camera2CameraControl.focus(focusRect, meteringRect);
+ camera2CameraControl.cancelFocus();
+
+ SessionConfiguration sessionConfiguration =
+ camera2CameraControl.getControlSessionConfiguration();
+ Camera2Configuration repeatingConfig =
+ new Camera2Configuration(sessionConfiguration.getImplementationOptions());
+ MeteringRectangle zeroRegion =
+ new MeteringRectangle(new Rect(), MeteringRectangle.METERING_WEIGHT_DONT_CARE);
+
+ assertThat(
+ repeatingConfig.getCaptureRequestOption(
+ CaptureRequest.CONTROL_AF_REGIONS, (MeteringRectangle[]) null))
+ .isEqualTo(new MeteringRectangle[]{zeroRegion});
+ assertThat(
+ repeatingConfig.getCaptureRequestOption(
+ CaptureRequest.CONTROL_AE_REGIONS, (MeteringRectangle[]) null))
+ .isEqualTo(new MeteringRectangle[]{zeroRegion});
+ assertThat(
+ repeatingConfig.getCaptureRequestOption(
+ CaptureRequest.CONTROL_AWB_REGIONS, (MeteringRectangle[]) null))
+ .isEqualTo(new MeteringRectangle[]{zeroRegion});
+
+ Camera2Configuration singleConfig =
+ new Camera2Configuration(camera2CameraControl.getSingleRequestImplOptions());
+ assertThat(
+ singleConfig.getCaptureRequestOption(
+ CaptureRequest.CONTROL_AF_REGIONS, (MeteringRectangle[]) null))
+ .isEqualTo(new MeteringRectangle[]{zeroRegion});
+ assertThat(
+ singleConfig.getCaptureRequestOption(
+ CaptureRequest.CONTROL_AE_REGIONS, (MeteringRectangle[]) null))
+ .isEqualTo(new MeteringRectangle[]{zeroRegion});
+ assertThat(
+ singleConfig.getCaptureRequestOption(
+ CaptureRequest.CONTROL_AWB_REGIONS, (MeteringRectangle[]) null))
+ .isEqualTo(new MeteringRectangle[]{zeroRegion});
+
+ assertThat(camera2CameraControl.isFocusLocked()).isFalse();
+ verify(camera2RequestRunner, times(2)).updateRepeatingRequest();
+ }
+
+ @Test
+ public void defaultAFAWBMode_ShouldBeCAFWhenNotFocusLocked() {
+ Camera2Configuration repeatingConfig =
+ new Camera2Configuration(
+ camera2CameraControl
+ .getControlSessionConfiguration()
+ .getImplementationOptions());
+
+ assertThat(
+ repeatingConfig.getCaptureRequestOption(
+ CaptureRequest.CONTROL_MODE, CaptureRequest.CONTROL_MODE_OFF))
+ .isEqualTo(CaptureRequest.CONTROL_MODE_AUTO);
+ assertThat(
+ repeatingConfig.getCaptureRequestOption(
+ CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_OFF))
+ .isEqualTo(CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE);
+ assertThat(
+ repeatingConfig.getCaptureRequestOption(
+ CaptureRequest.CONTROL_AWB_MODE,
+ CaptureRequest.CONTROL_AWB_MODE_OFF))
+ .isEqualTo(CaptureRequest.CONTROL_AWB_MODE_AUTO);
+
+ Camera2Configuration singleConfig =
+ new Camera2Configuration(camera2CameraControl.getSingleRequestImplOptions());
+ assertThat(
+ singleConfig.getCaptureRequestOption(
+ CaptureRequest.CONTROL_MODE, CaptureRequest.CONTROL_MODE_OFF))
+ .isEqualTo(CaptureRequest.CONTROL_MODE_AUTO);
+ assertThat(
+ singleConfig.getCaptureRequestOption(
+ CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_OFF))
+ .isEqualTo(CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE);
+ assertThat(
+ singleConfig.getCaptureRequestOption(
+ CaptureRequest.CONTROL_AWB_MODE,
+ CaptureRequest.CONTROL_AWB_MODE_OFF))
+ .isEqualTo(CaptureRequest.CONTROL_AWB_MODE_AUTO);
+ }
+
+ @Test
+ public void focus_afModeSetToAuto() {
+ Rect focusRect = new Rect(0, 0, 10, 10);
+ camera2CameraControl.focus(focusRect, focusRect);
+
+ Camera2Configuration repeatingConfig =
+ new Camera2Configuration(
+ camera2CameraControl
+ .getControlSessionConfiguration()
+ .getImplementationOptions());
+ assertThat(
+ repeatingConfig.getCaptureRequestOption(
+ CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_OFF))
+ .isEqualTo(CaptureRequest.CONTROL_AF_MODE_AUTO);
+
+ Camera2Configuration singleConfig =
+ new Camera2Configuration(camera2CameraControl.getSingleRequestImplOptions());
+ assertThat(
+ singleConfig.getCaptureRequestOption(
+ CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_OFF))
+ .isEqualTo(CaptureRequest.CONTROL_AF_MODE_AUTO);
+
+ camera2CameraControl.cancelFocus();
+
+ Camera2Configuration repeatingConfig2 =
+ new Camera2Configuration(
+ camera2CameraControl
+ .getControlSessionConfiguration()
+ .getImplementationOptions());
+ assertThat(
+ repeatingConfig2.getCaptureRequestOption(
+ CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_OFF))
+ .isEqualTo(CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE);
+
+ Camera2Configuration singleConfig2 =
+ new Camera2Configuration(camera2CameraControl.getSingleRequestImplOptions());
+ assertThat(
+ singleConfig2.getCaptureRequestOption(
+ CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_OFF))
+ .isEqualTo(CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE);
+ }
+
+ @Test
+ public void setFlashModeAuto_aeModeSetAndRequestUpdated() {
+ camera2CameraControl.setFlashMode(FlashMode.AUTO);
+
+ SessionConfiguration sessionConfiguration =
+ camera2CameraControl.getControlSessionConfiguration();
+ Camera2Configuration camera2Configuration =
+ new Camera2Configuration(sessionConfiguration.getImplementationOptions());
+ assertThat(
+ camera2Configuration.getCaptureRequestOption(
+ CaptureRequest.CONTROL_AE_MODE, CONTROL_AE_MODE_OFF))
+ .isEqualTo(CONTROL_AE_MODE_ON_AUTO_FLASH);
+ assertThat(camera2CameraControl.getFlashMode()).isEqualTo(FlashMode.AUTO);
+ verify(camera2RequestRunner).updateRepeatingRequest();
+ }
+
+ @Test
+ public void setFlashModeOff_aeModeSetAndRequestUpdated() {
+ camera2CameraControl.setFlashMode(FlashMode.OFF);
+
+ SessionConfiguration sessionConfiguration =
+ camera2CameraControl.getControlSessionConfiguration();
+ Camera2Configuration camera2Configuration =
+ new Camera2Configuration(sessionConfiguration.getImplementationOptions());
+ assertThat(
+ camera2Configuration.getCaptureRequestOption(
+ CaptureRequest.CONTROL_AE_MODE, CONTROL_AE_MODE_OFF))
+ .isEqualTo(CONTROL_AE_MODE_ON);
+ assertThat(camera2CameraControl.getFlashMode()).isEqualTo(FlashMode.OFF);
+ verify(camera2RequestRunner).updateRepeatingRequest();
+ }
+
+ @Test
+ public void setFlashModeOn_aeModeSetAndRequestUpdated() {
+ camera2CameraControl.setFlashMode(FlashMode.ON);
+
+ SessionConfiguration sessionConfiguration =
+ camera2CameraControl.getControlSessionConfiguration();
+ Camera2Configuration camera2Configuration =
+ new Camera2Configuration(sessionConfiguration.getImplementationOptions());
+ assertThat(
+ camera2Configuration.getCaptureRequestOption(
+ CaptureRequest.CONTROL_AE_MODE, CONTROL_AE_MODE_OFF))
+ .isEqualTo(CONTROL_AE_MODE_ON_ALWAYS_FLASH);
+ assertThat(camera2CameraControl.getFlashMode()).isEqualTo(FlashMode.ON);
+ verify(camera2RequestRunner).updateRepeatingRequest();
+ }
+
+ @Test
+ public void enableTorch_aeModeSetAndRequestUpdated() {
+ camera2CameraControl.enableTorch(true);
+
+ SessionConfiguration sessionConfiguration =
+ camera2CameraControl.getControlSessionConfiguration();
+ Camera2Configuration camera2Configuration =
+ new Camera2Configuration(sessionConfiguration.getImplementationOptions());
+ assertThat(
+ camera2Configuration.getCaptureRequestOption(
+ CaptureRequest.CONTROL_AE_MODE, CONTROL_AE_MODE_OFF))
+ .isEqualTo(CONTROL_AE_MODE_ON);
+ assertThat(
+ camera2Configuration.getCaptureRequestOption(
+ CaptureRequest.FLASH_MODE, FLASH_MODE_OFF))
+ .isEqualTo(FLASH_MODE_TORCH);
+ assertThat(camera2CameraControl.isTorchOn()).isTrue();
+ verify(camera2RequestRunner).updateRepeatingRequest();
+ }
+
+ @Test
+ public void disableTorchFlashModeAuto_aeModeSetAndRequestUpdated() {
+ camera2CameraControl.setFlashMode(FlashMode.AUTO);
+ camera2CameraControl.enableTorch(false);
+
+ SessionConfiguration sessionConfiguration =
+ camera2CameraControl.getControlSessionConfiguration();
+ Camera2Configuration camera2Configuration =
+ new Camera2Configuration(sessionConfiguration.getImplementationOptions());
+ assertThat(
+ camera2Configuration.getCaptureRequestOption(
+ CaptureRequest.CONTROL_AE_MODE, CONTROL_AE_MODE_OFF))
+ .isEqualTo(CONTROL_AE_MODE_ON_AUTO_FLASH);
+ assertThat(camera2Configuration.getCaptureRequestOption(CaptureRequest.FLASH_MODE, -1))
+ .isEqualTo(-1);
+ assertThat(camera2CameraControl.isTorchOn()).isFalse();
+ verify(camera2RequestRunner, times(2)).updateRepeatingRequest();
+ verify(camera2RequestRunner, times(1)).submitSingleRequest(any());
+
+ ArgumentCaptor<CaptureRequestConfiguration> argumentCaptor =
+ ArgumentCaptor.forClass(CaptureRequestConfiguration.class);
+ verify(camera2RequestRunner).submitSingleRequest(argumentCaptor.capture());
+ CaptureRequestConfiguration resultCaptureConfig = argumentCaptor.getValue();
+ assertThat(
+ resultCaptureConfig
+ .getCameraCharacteristics()
+ .get(CaptureRequest.CONTROL_AE_MODE)
+ .getValue())
+ .isEqualTo(CaptureRequest.CONTROL_AE_MODE_ON);
+ }
+
+ @Test
+ public void triggerAf_singleRequestSent() {
+ camera2CameraControl.triggerAf();
+
+ ArgumentCaptor<CaptureRequestConfiguration> argumentCaptor =
+ ArgumentCaptor.forClass(CaptureRequestConfiguration.class);
+ verify(camera2RequestRunner).submitSingleRequest(argumentCaptor.capture());
+ CaptureRequestConfiguration resultCaptureConfig = argumentCaptor.getValue();
+ assertThat(
+ resultCaptureConfig
+ .getCameraCharacteristics()
+ .get(CaptureRequest.CONTROL_AF_TRIGGER)
+ .getValue())
+ .isEqualTo(CaptureRequest.CONTROL_AF_TRIGGER_START);
+ }
+
+ @Test
+ public void triggerAePrecapture_singleRequestSent() {
+ camera2CameraControl.triggerAePrecapture();
+
+ ArgumentCaptor<CaptureRequestConfiguration> argumentCaptor =
+ ArgumentCaptor.forClass(CaptureRequestConfiguration.class);
+ verify(camera2RequestRunner).submitSingleRequest(argumentCaptor.capture());
+ CaptureRequestConfiguration resultCaptureConfig = argumentCaptor.getValue();
+ assertThat(
+ resultCaptureConfig
+ .getCameraCharacteristics()
+ .get(CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER)
+ .getValue())
+ .isEqualTo(CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_START);
+ }
+
+ @Test
+ public void cancelAfAeTrigger_singleRequestSent() {
+ camera2CameraControl.cancelAfAeTrigger(true, true);
+
+ ArgumentCaptor<CaptureRequestConfiguration> argumentCaptor =
+ ArgumentCaptor.forClass(CaptureRequestConfiguration.class);
+ verify(camera2RequestRunner).submitSingleRequest(argumentCaptor.capture());
+ CaptureRequestConfiguration resultCaptureConfig = argumentCaptor.getValue();
+ assertThat(
+ resultCaptureConfig
+ .getCameraCharacteristics()
+ .get(CaptureRequest.CONTROL_AF_TRIGGER)
+ .getValue())
+ .isEqualTo(CaptureRequest.CONTROL_AF_TRIGGER_CANCEL);
+ assertThat(
+ resultCaptureConfig
+ .getCameraCharacteristics()
+ .get(CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER)
+ .getValue())
+ .isEqualTo(CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_CANCEL);
+ }
+
+ @Test
+ public void cancelAfTrigger_singleRequestSent() {
+ camera2CameraControl.cancelAfAeTrigger(true, false);
+
+ ArgumentCaptor<CaptureRequestConfiguration> argumentCaptor =
+ ArgumentCaptor.forClass(CaptureRequestConfiguration.class);
+ verify(camera2RequestRunner).submitSingleRequest(argumentCaptor.capture());
+ CaptureRequestConfiguration resultCaptureConfig = argumentCaptor.getValue();
+ assertThat(
+ resultCaptureConfig
+ .getCameraCharacteristics()
+ .get(CaptureRequest.CONTROL_AF_TRIGGER)
+ .getValue())
+ .isEqualTo(CaptureRequest.CONTROL_AF_TRIGGER_CANCEL);
+ assertThat(
+ resultCaptureConfig
+ .getCameraCharacteristics()
+ .get(CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER))
+ .isNull();
+ }
+
+ @Test
+ public void cancelAeTrigger_singleRequestSent() {
+ camera2CameraControl.cancelAfAeTrigger(false, true);
+
+ ArgumentCaptor<CaptureRequestConfiguration> argumentCaptor =
+ ArgumentCaptor.forClass(CaptureRequestConfiguration.class);
+ verify(camera2RequestRunner).submitSingleRequest(argumentCaptor.capture());
+ CaptureRequestConfiguration resultCaptureConfig = argumentCaptor.getValue();
+ assertThat(
+ resultCaptureConfig
+ .getCameraCharacteristics()
+ .get(CaptureRequest.CONTROL_AF_TRIGGER))
+ .isNull();
+ assertThat(
+ resultCaptureConfig
+ .getCameraCharacteristics()
+ .get(CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER)
+ .getValue())
+ .isEqualTo(CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_CANCEL);
+ }
+}
diff --git a/camera/camera2/src/androidTest/java/androidx/camera/camera2/Camera2CaptureSessionCaptureCallbacksAndroidTest.java b/camera/camera2/src/androidTest/java/androidx/camera/camera2/Camera2CaptureSessionCaptureCallbacksAndroidTest.java
new file mode 100644
index 0000000..e943452
--- /dev/null
+++ b/camera/camera2/src/androidTest/java/androidx/camera/camera2/Camera2CaptureSessionCaptureCallbacksAndroidTest.java
@@ -0,0 +1,86 @@
+/*
+ * Copyright (C) 2019 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.camera2;
+
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import android.hardware.camera2.CameraCaptureSession;
+import android.hardware.camera2.CaptureFailure;
+import android.hardware.camera2.CaptureRequest;
+import android.hardware.camera2.CaptureResult;
+import android.hardware.camera2.TotalCaptureResult;
+import android.os.Build;
+import android.view.Surface;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.Mockito;
+
+@RunWith(JUnit4.class)
+public final class Camera2CaptureSessionCaptureCallbacksAndroidTest {
+
+ @Test
+ public void comboCallbackInvokesConstituentCallbacks() {
+ CameraCaptureSession.CaptureCallback callback0 =
+ Mockito.mock(CameraCaptureSession.CaptureCallback.class);
+ CameraCaptureSession.CaptureCallback callback1 =
+ Mockito.mock(CameraCaptureSession.CaptureCallback.class);
+ CameraCaptureSession.CaptureCallback comboCallback =
+ Camera2CaptureSessionCaptureCallbacks.createComboCallback(callback0, callback1);
+ CameraCaptureSession session = Mockito.mock(CameraCaptureSession.class);
+ CaptureResult result = Mockito.mock(CaptureResult.class);
+ CaptureFailure failure = Mockito.mock(CaptureFailure.class);
+ Surface surface = Mockito.mock(Surface.class);
+ // CaptureRequest, TotalCaptureResult are final classes which cannot be mocked, and it is
+ // difficult to create fake instances without an actual Camera2 pipeline. Use null as a
+ // placeholder.
+ CaptureRequest request = null;
+ TotalCaptureResult totalResult = null;
+
+ if (Build.VERSION.SDK_INT >= 24) {
+ comboCallback.onCaptureBufferLost(session, request, surface, 1L);
+ verify(callback0, times(1)).onCaptureBufferLost(session, request, surface, 1L);
+ verify(callback1, times(1)).onCaptureBufferLost(session, request, surface, 1L);
+ }
+
+ comboCallback.onCaptureCompleted(session, request, totalResult);
+ verify(callback0, times(1)).onCaptureCompleted(session, request, totalResult);
+ verify(callback1, times(1)).onCaptureCompleted(session, request, totalResult);
+
+ comboCallback.onCaptureFailed(session, request, failure);
+ verify(callback0, times(1)).onCaptureFailed(session, request, failure);
+ verify(callback1, times(1)).onCaptureFailed(session, request, failure);
+
+ comboCallback.onCaptureProgressed(session, request, result);
+ verify(callback0, times(1)).onCaptureProgressed(session, request, result);
+ verify(callback1, times(1)).onCaptureProgressed(session, request, result);
+
+ comboCallback.onCaptureSequenceAborted(session, 1);
+ verify(callback0, times(1)).onCaptureSequenceAborted(session, 1);
+ verify(callback1, times(1)).onCaptureSequenceAborted(session, 1);
+
+ comboCallback.onCaptureSequenceCompleted(session, 1, 123L);
+ verify(callback0, times(1)).onCaptureSequenceCompleted(session, 1, 123L);
+ verify(callback1, times(1)).onCaptureSequenceCompleted(session, 1, 123L);
+
+ comboCallback.onCaptureStarted(session, request, 123L, 1L);
+ verify(callback0, times(1)).onCaptureStarted(session, request, 123L, 1L);
+ verify(callback1, times(1)).onCaptureStarted(session, request, 123L, 1L);
+ }
+}
diff --git a/camera/camera2/src/androidTest/java/androidx/camera/camera2/Camera2ConfigurationAndroidTest.java b/camera/camera2/src/androidTest/java/androidx/camera/camera2/Camera2ConfigurationAndroidTest.java
new file mode 100644
index 0000000..397f1cd
--- /dev/null
+++ b/camera/camera2/src/androidTest/java/androidx/camera/camera2/Camera2ConfigurationAndroidTest.java
@@ -0,0 +1,225 @@
+/*
+ * Copyright (C) 2019 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.camera2;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.hardware.camera2.CameraCaptureSession;
+import android.hardware.camera2.CameraDevice;
+import android.hardware.camera2.CameraMetadata;
+import android.hardware.camera2.CaptureRequest;
+import android.util.Range;
+
+import androidx.camera.core.CameraCaptureSessionStateCallbacks;
+import androidx.camera.core.CameraDeviceStateCallbacks;
+import androidx.camera.testing.fakes.FakeConfiguration;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public final class Camera2ConfigurationAndroidTest {
+ private static final int INVALID_TEMPLATE_TYPE = -1;
+ private static final int INVALID_COLOR_CORRECTION_MODE = -1;
+ private static final CameraCaptureSession.CaptureCallback SESSION_CAPTURE_CALLBACK =
+ Camera2CaptureSessionCaptureCallbacks.createComboCallback();
+ private static final CameraCaptureSession.StateCallback SESSION_STATE_CALLBACK =
+ CameraCaptureSessionStateCallbacks.createNoOpCallback();
+ private static final CameraDevice.StateCallback DEVICE_STATE_CALLBACK =
+ CameraDeviceStateCallbacks.createNoOpCallback();
+
+ @Test
+ public void emptyConfigurationDoesNotContainTemplateType() {
+ FakeConfiguration.Builder builder = new FakeConfiguration.Builder();
+ Camera2Configuration config = new Camera2Configuration(builder.build());
+
+ assertThat(config.getCaptureRequestTemplate(INVALID_TEMPLATE_TYPE))
+ .isEqualTo(INVALID_TEMPLATE_TYPE);
+ }
+
+ @Test
+ public void canExtendWithTemplateType() {
+ FakeConfiguration.Builder builder = new FakeConfiguration.Builder();
+
+ new Camera2Configuration.Extender(builder)
+ .setCaptureRequestTemplate(CameraDevice.TEMPLATE_PREVIEW);
+
+ Camera2Configuration config = new Camera2Configuration(builder.build());
+
+ assertThat(config.getCaptureRequestTemplate(INVALID_TEMPLATE_TYPE))
+ .isEqualTo(CameraDevice.TEMPLATE_PREVIEW);
+ }
+
+ @Test
+ public void canExtendWithSessionCaptureCallback() {
+ FakeConfiguration.Builder builder = new FakeConfiguration.Builder();
+
+ new Camera2Configuration.Extender(builder)
+ .setSessionCaptureCallback(SESSION_CAPTURE_CALLBACK);
+
+ Camera2Configuration config = new Camera2Configuration(builder.build());
+
+ assertThat(config.getSessionCaptureCallback(/*valueIfMissing=*/ null))
+ .isSameAs(SESSION_CAPTURE_CALLBACK);
+ }
+
+ @Test
+ public void canExtendWithSessionStateCallback() {
+ FakeConfiguration.Builder builder = new FakeConfiguration.Builder();
+
+ new Camera2Configuration.Extender(builder).setSessionStateCallback(SESSION_STATE_CALLBACK);
+
+ Camera2Configuration config = new Camera2Configuration(builder.build());
+
+ assertThat(config.getSessionStateCallback(/*valueIfMissing=*/ null))
+ .isSameAs(SESSION_STATE_CALLBACK);
+ }
+
+ @Test
+ public void canExtendWithDeviceStateCallback() {
+ FakeConfiguration.Builder builder = new FakeConfiguration.Builder();
+
+ new Camera2Configuration.Extender(builder).setDeviceStateCallback(DEVICE_STATE_CALLBACK);
+
+ Camera2Configuration config = new Camera2Configuration(builder.build());
+
+ assertThat(config.getDeviceStateCallback(/*valueIfMissing=*/ null))
+ .isSameAs(DEVICE_STATE_CALLBACK);
+ }
+
+ @Test
+ public void canSetAndRetrieveCaptureRequestKeys() {
+ FakeConfiguration.Builder builder = new FakeConfiguration.Builder();
+
+ Range<Integer> fakeRange = new Range<>(0, 30);
+ new Camera2Configuration.Extender(builder)
+ .setCaptureRequestOption(CaptureRequest.CONTROL_AE_TARGET_FPS_RANGE, fakeRange)
+ .setCaptureRequestOption(
+ CaptureRequest.COLOR_CORRECTION_MODE,
+ CameraMetadata.COLOR_CORRECTION_MODE_FAST);
+
+ Camera2Configuration config = new Camera2Configuration(builder.build());
+
+ assertThat(
+ config.getCaptureRequestOption(
+ CaptureRequest.CONTROL_AE_TARGET_FPS_RANGE,
+ /*valueIfMissing=*/ null))
+ .isSameAs(fakeRange);
+ assertThat(
+ config.getCaptureRequestOption(
+ CaptureRequest.COLOR_CORRECTION_MODE,
+ INVALID_COLOR_CORRECTION_MODE))
+ .isEqualTo(CameraMetadata.COLOR_CORRECTION_MODE_FAST);
+ }
+
+ @Test
+ public void canSetAndRetrieveCaptureRequestKeys_fromOptionIds() {
+ FakeConfiguration.Builder builder = new FakeConfiguration.Builder();
+
+ Range<Integer> fakeRange = new Range<>(0, 30);
+ new Camera2Configuration.Extender(builder)
+ .setCaptureRequestOption(CaptureRequest.CONTROL_AE_TARGET_FPS_RANGE, fakeRange)
+ .setCaptureRequestOption(
+ CaptureRequest.COLOR_CORRECTION_MODE,
+ CameraMetadata.COLOR_CORRECTION_MODE_FAST)
+ // Insert one non capture request option to ensure it gets filtered out
+ .setCaptureRequestTemplate(CameraDevice.TEMPLATE_PREVIEW);
+
+ Camera2Configuration config = new Camera2Configuration(builder.build());
+
+ config.findOptions(
+ "camera2.captureRequest.option",
+ option -> {
+ // The token should be the capture request key
+ assertThat(option.getToken())
+ .isAnyOf(
+ CaptureRequest.CONTROL_AE_TARGET_FPS_RANGE,
+ CaptureRequest.COLOR_CORRECTION_MODE);
+ return true;
+ });
+
+ assertThat(config.listOptions()).hasSize(3);
+ }
+
+ @Test
+ public void canSetAndRetrieveCaptureRequestKeys_byBuilder() {
+ Range<Integer> fakeRange = new Range<>(0, 30);
+ Camera2Configuration.Builder builder =
+ new Camera2Configuration.Builder()
+ .setCaptureRequestOption(
+ CaptureRequest.CONTROL_AE_TARGET_FPS_RANGE, fakeRange)
+ .setCaptureRequestOption(
+ CaptureRequest.COLOR_CORRECTION_MODE,
+ CameraMetadata.COLOR_CORRECTION_MODE_FAST);
+
+ Camera2Configuration config = new Camera2Configuration(builder.build());
+
+ assertThat(
+ config.getCaptureRequestOption(
+ CaptureRequest.CONTROL_AE_TARGET_FPS_RANGE,
+ /*valueIfMissing=*/ null))
+ .isSameAs(fakeRange);
+ assertThat(
+ config.getCaptureRequestOption(
+ CaptureRequest.COLOR_CORRECTION_MODE,
+ INVALID_COLOR_CORRECTION_MODE))
+ .isEqualTo(CameraMetadata.COLOR_CORRECTION_MODE_FAST);
+ }
+
+ @Test
+ public void canInsertAllOptions_byBuilder() {
+ Range<Integer> fakeRange = new Range<>(0, 30);
+ Camera2Configuration.Builder builder =
+ new Camera2Configuration.Builder()
+ .setCaptureRequestOption(
+ CaptureRequest.CONTROL_AE_TARGET_FPS_RANGE, fakeRange)
+ .setCaptureRequestOption(
+ CaptureRequest.COLOR_CORRECTION_MODE,
+ CameraMetadata.COLOR_CORRECTION_MODE_FAST);
+
+ Camera2Configuration config1 = new Camera2Configuration(builder.build());
+
+ Camera2Configuration.Builder builder2 =
+ new Camera2Configuration.Builder()
+ .setCaptureRequestOption(
+ CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON)
+ .setCaptureRequestOption(
+ CaptureRequest.CONTROL_AWB_MODE,
+ CaptureRequest.CONTROL_AWB_MODE_AUTO)
+ .insertAllOptions(config1);
+
+ Camera2Configuration config2 = new Camera2Configuration(builder2.build());
+
+ assertThat(
+ config2.getCaptureRequestOption(
+ CaptureRequest.CONTROL_AE_TARGET_FPS_RANGE,
+ /*valueIfMissing=*/ null))
+ .isSameAs(fakeRange);
+ assertThat(
+ config2.getCaptureRequestOption(
+ CaptureRequest.COLOR_CORRECTION_MODE,
+ INVALID_COLOR_CORRECTION_MODE))
+ .isEqualTo(CameraMetadata.COLOR_CORRECTION_MODE_FAST);
+ assertThat(
+ config2.getCaptureRequestOption(
+ CaptureRequest.CONTROL_AE_MODE, /*valueIfMissing=*/ 0))
+ .isEqualTo(CaptureRequest.CONTROL_AE_MODE_ON);
+ assertThat(config2.getCaptureRequestOption(CaptureRequest.CONTROL_AWB_MODE, 0))
+ .isEqualTo(CaptureRequest.CONTROL_AWB_MODE_AUTO);
+ }
+}
diff --git a/camera/camera2/src/androidTest/java/androidx/camera/camera2/Camera2ImplCameraRepositoryAndroidTest.java b/camera/camera2/src/androidTest/java/androidx/camera/camera2/Camera2ImplCameraRepositoryAndroidTest.java
new file mode 100644
index 0000000..c7cd03d
--- /dev/null
+++ b/camera/camera2/src/androidTest/java/androidx/camera/camera2/Camera2ImplCameraRepositoryAndroidTest.java
@@ -0,0 +1,136 @@
+/*
+ * Copyright (C) 2019 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.camera2;
+
+import android.graphics.SurfaceTexture;
+import android.hardware.camera2.CameraDevice;
+import android.util.Size;
+import android.view.Surface;
+
+import androidx.camera.camera2.SemaphoreReleasingCamera2Callbacks.DeviceStateCallback;
+import androidx.camera.camera2.SemaphoreReleasingCamera2Callbacks.SessionStateCallback;
+import androidx.camera.core.CameraFactory;
+import androidx.camera.core.CameraRepository;
+import androidx.camera.core.CameraX.LensFacing;
+import androidx.camera.core.FakeUseCase;
+import androidx.camera.core.FakeUseCaseConfiguration;
+import androidx.camera.core.ImmediateSurface;
+import androidx.camera.core.SessionConfiguration;
+import androidx.camera.core.UseCaseGroup;
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.Map;
+
+/**
+ * Contains tests for {@link androidx.camera.core.CameraRepository} which require an actual
+ * implementation to run.
+ */
+@RunWith(AndroidJUnit4.class)
+public final class Camera2ImplCameraRepositoryAndroidTest {
+ private CameraRepository cameraRepository;
+ private UseCaseGroup useCaseGroup;
+ private FakeUseCaseConfiguration configuration;
+ private CallbackAttachingFakeUseCase useCase;
+ private CameraFactory cameraFactory;
+
+ private String getCameraIdForLensFacingUnchecked(LensFacing lensFacing) {
+ try {
+ return cameraFactory.cameraIdForLensFacing(lensFacing);
+ } catch (Exception e) {
+ throw new IllegalArgumentException(
+ "Unable to attach to camera with LensFacing " + lensFacing, e);
+ }
+ }
+
+ @Before
+ public void setUp() {
+ cameraRepository = new CameraRepository();
+ cameraFactory = new Camera2CameraFactory(ApplicationProvider.getApplicationContext());
+ cameraRepository.init(cameraFactory);
+ useCaseGroup = new UseCaseGroup();
+ configuration =
+ new FakeUseCaseConfiguration.Builder().setLensFacing(LensFacing.BACK).build();
+ String cameraId = getCameraIdForLensFacingUnchecked(configuration.getLensFacing());
+ useCase = new CallbackAttachingFakeUseCase(configuration, cameraId);
+ useCaseGroup.addUseCase(useCase);
+ }
+
+ @Test(timeout = 5000)
+ public void cameraDeviceCallsAreForwardedToCallback() throws InterruptedException {
+ cameraRepository.onGroupActive(useCaseGroup);
+
+ // Wait for the CameraDevice.onOpened callback.
+ useCase.deviceStateCallback.waitForOnOpened(1);
+
+ cameraRepository.onGroupInactive(useCaseGroup);
+
+ // Wait for the CameraDevice.onClosed callback.
+ useCase.deviceStateCallback.waitForOnClosed(1);
+ }
+
+ @Test(timeout = 5000)
+ public void cameraSessionCallsAreForwardedToCallback() throws InterruptedException {
+ useCase.addStateChangeListener(
+ cameraRepository.getCamera(
+ getCameraIdForLensFacingUnchecked(configuration.getLensFacing())));
+ useCase.doNotifyActive();
+ cameraRepository.onGroupActive(useCaseGroup);
+
+ // Wait for the CameraCaptureSession.onConfigured callback.
+ useCase.sessionStateCallback.waitForOnConfigured(1);
+
+ // Camera doesn't currently call CaptureSession.release(), because it is recommended that
+ // we don't explicitly call CameraCaptureSession.close(). Rather, we rely on another
+ // CameraCaptureSession to get opened. See
+ // https://developer.android.com/reference/android/hardware/camera2/CameraCaptureSession
+ // .html#close()
+ }
+
+ /** A fake use case which attaches to a camera with various callbacks. */
+ private static class CallbackAttachingFakeUseCase extends FakeUseCase {
+ private final DeviceStateCallback deviceStateCallback = new DeviceStateCallback();
+ private final SessionStateCallback sessionStateCallback = new SessionStateCallback();
+ private final SurfaceTexture surfaceTexture = new SurfaceTexture(0);
+
+ CallbackAttachingFakeUseCase(FakeUseCaseConfiguration configuration, String cameraId) {
+ super(configuration);
+
+ SessionConfiguration.Builder builder = new SessionConfiguration.Builder();
+ builder.setTemplateType(CameraDevice.TEMPLATE_PREVIEW);
+ builder.addSurface(new ImmediateSurface(new Surface(surfaceTexture)));
+ builder.setDeviceStateCallback(deviceStateCallback);
+ builder.setSessionStateCallback(sessionStateCallback);
+
+ attachToCamera(cameraId, builder.build());
+ }
+
+ @Override
+ protected Map<String, Size> onSuggestedResolutionUpdated(
+ Map<String, Size> suggestedResolutionMap) {
+ return suggestedResolutionMap;
+ }
+
+ void doNotifyActive() {
+ super.notifyActive();
+ }
+ }
+}
diff --git a/camera/camera2/src/androidTest/java/androidx/camera/camera2/Camera2ImplCameraXAndroidTest.java b/camera/camera2/src/androidTest/java/androidx/camera/camera2/Camera2ImplCameraXAndroidTest.java
new file mode 100644
index 0000000..5569401
--- /dev/null
+++ b/camera/camera2/src/androidTest/java/androidx/camera/camera2/Camera2ImplCameraXAndroidTest.java
@@ -0,0 +1,264 @@
+/*
+ * Copyright (C) 2019 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.camera2;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Matchers.any;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.timeout;
+import static org.mockito.Mockito.verify;
+
+import android.content.Context;
+import android.hardware.camera2.CameraDevice;
+import android.os.HandlerThread;
+
+import androidx.camera.camera2.SemaphoreReleasingCamera2Callbacks.DeviceStateCallback;
+import androidx.camera.camera2.SemaphoreReleasingCamera2Callbacks.SessionCaptureCallback;
+import androidx.camera.core.CameraX;
+import androidx.camera.core.CameraX.LensFacing;
+import androidx.camera.core.ImageAnalysisUseCase;
+import androidx.camera.core.ImageAnalysisUseCaseConfiguration;
+import androidx.camera.testing.fakes.FakeLifecycleOwner;
+import androidx.lifecycle.MutableLiveData;
+import androidx.lifecycle.Observer;
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mockito;
+
+import java.util.concurrent.atomic.AtomicLong;
+
+/**
+ * Contains tests for {@link androidx.camera.core.CameraX} which require an actual implementation to
+ * run.
+ */
+@RunWith(AndroidJUnit4.class)
+public final class Camera2ImplCameraXAndroidTest {
+ private static final LensFacing DEFAULT_LENS_FACING = LensFacing.BACK;
+ private final MutableLiveData<Long> analysisResult = new MutableLiveData<>();
+ private final ImageAnalysisUseCase.Analyzer imageAnalyzer =
+ (image, rotationDegrees) -> {
+ analysisResult.postValue(image.getTimestamp());
+ };
+ private FakeLifecycleOwner lifecycle;
+ private HandlerThread handlerThread;
+
+ private CameraDevice.StateCallback mockStateCallback;
+
+ private static Observer<Long> createCountIncrementingObserver(final AtomicLong counter) {
+ return value -> {
+ counter.incrementAndGet();
+ };
+ }
+
+ @Before
+ public void setUp() {
+ Context context = ApplicationProvider.getApplicationContext();
+ CameraX.init(context, Camera2AppConfiguration.create(context));
+ lifecycle = new FakeLifecycleOwner();
+ handlerThread = new HandlerThread("ErrorHandlerThread");
+ handlerThread.start();
+ mockStateCallback = Mockito.mock(CameraDevice.StateCallback.class);
+ }
+
+ @After
+ public void tearDown() throws InterruptedException {
+ CameraX.unbindAll();
+ handlerThread.quitSafely();
+
+ // Wait some time for the cameras to close. We need the cameras to close to bring CameraX
+ // back
+ // to the initial state.
+ Thread.sleep(3000);
+ }
+
+ @Test
+ public void lifecycleResume_opensCameraAndStreamsFrames() throws InterruptedException {
+ ImageAnalysisUseCaseConfiguration configuration =
+ new ImageAnalysisUseCaseConfiguration.Builder()
+ .setLensFacing(DEFAULT_LENS_FACING)
+ .build();
+ ImageAnalysisUseCase useCase = new ImageAnalysisUseCase(configuration);
+ CameraX.bindToLifecycle(lifecycle, useCase);
+ final AtomicLong observedCount = new AtomicLong(0);
+ useCase.setAnalyzer(imageAnalyzer);
+ analysisResult.observe(lifecycle, createCountIncrementingObserver(observedCount));
+
+ lifecycle.startAndResume();
+
+ // Wait a little bit for the camera to open and stream frames.
+ Thread.sleep(5000);
+
+ // Some frames should have been observed.
+ assertThat(observedCount.get()).isAtLeast(10L);
+ }
+
+ @Test
+ public void removedUseCase_doesNotStreamWhenLifecycleResumes() throws InterruptedException {
+ ImageAnalysisUseCaseConfiguration configuration =
+ new ImageAnalysisUseCaseConfiguration.Builder()
+ .setLensFacing(DEFAULT_LENS_FACING)
+ .build();
+ ImageAnalysisUseCase useCase = new ImageAnalysisUseCase(configuration);
+ CameraX.bindToLifecycle(lifecycle, useCase);
+ final AtomicLong observedCount = new AtomicLong(0);
+ useCase.setAnalyzer(imageAnalyzer);
+ analysisResult.observe(lifecycle, createCountIncrementingObserver(observedCount));
+ assertThat(observedCount.get()).isEqualTo(0);
+
+ CameraX.unbind(useCase);
+
+ lifecycle.startAndResume();
+
+ // Wait a little bit for the camera to open and stream frames.
+ Thread.sleep(5000);
+
+ // No frames should have been observed.
+ assertThat(observedCount.get()).isEqualTo(0);
+ }
+
+ @Test
+ public void lifecyclePause_closesCameraAndStopsStreamingFrames() throws InterruptedException {
+ ImageAnalysisUseCaseConfiguration.Builder configurationBuilder =
+ new ImageAnalysisUseCaseConfiguration.Builder().setLensFacing(DEFAULT_LENS_FACING);
+ DeviceStateCallback deviceStateCallback = new DeviceStateCallback();
+ SessionCaptureCallback sessionCaptureCallback = new SessionCaptureCallback();
+ new Camera2Configuration.Extender(configurationBuilder)
+ .setDeviceStateCallback(deviceStateCallback)
+ .setSessionCaptureCallback(sessionCaptureCallback);
+ ImageAnalysisUseCase useCase = new ImageAnalysisUseCase(configurationBuilder.build());
+ CameraX.bindToLifecycle(lifecycle, useCase);
+ final AtomicLong observedCount = new AtomicLong(0);
+ useCase.setAnalyzer(imageAnalyzer);
+ analysisResult.observe(lifecycle, createCountIncrementingObserver(observedCount));
+
+ lifecycle.startAndResume();
+
+ // Wait a little bit for the camera to open and stream frames.
+ sessionCaptureCallback.waitForOnCaptureCompleted(5);
+
+ lifecycle.pauseAndStop();
+
+ // Wait a little bit for the camera to close.
+ deviceStateCallback.waitForOnClosed(1);
+
+ final Long firstObservedCount = observedCount.get();
+ assertThat(firstObservedCount).isGreaterThan(1L);
+
+ // Stay in idle state for a while.
+ Thread.sleep(5000);
+
+ // Additional frames should not be observed.
+ final Long secondObservedCount = observedCount.get();
+ assertThat(secondObservedCount).isEqualTo(firstObservedCount);
+ }
+
+ @Test
+ public void bind_opensCamera() {
+ ImageAnalysisUseCaseConfiguration.Builder builder =
+ new ImageAnalysisUseCaseConfiguration.Builder().setLensFacing(DEFAULT_LENS_FACING);
+ new Camera2Configuration.Extender(builder).setDeviceStateCallback(mockStateCallback);
+ ImageAnalysisUseCaseConfiguration configuration = builder.build();
+ ImageAnalysisUseCase useCase = new ImageAnalysisUseCase(configuration);
+ CameraX.bindToLifecycle(lifecycle, useCase);
+ lifecycle.startAndResume();
+
+ verify(mockStateCallback, timeout(3000)).onOpened(any(CameraDevice.class));
+ }
+
+ @Test
+ public void unbindAll_closesAllCameras() {
+ ImageAnalysisUseCaseConfiguration.Builder builder =
+ new ImageAnalysisUseCaseConfiguration.Builder().setLensFacing(DEFAULT_LENS_FACING);
+ new Camera2Configuration.Extender(builder).setDeviceStateCallback(mockStateCallback);
+ ImageAnalysisUseCaseConfiguration configuration = builder.build();
+ ImageAnalysisUseCase useCase = new ImageAnalysisUseCase(configuration);
+ CameraX.bindToLifecycle(lifecycle, useCase);
+ lifecycle.startAndResume();
+
+ CameraX.unbindAll();
+
+ verify(mockStateCallback, timeout(3000)).onClosed(any(CameraDevice.class));
+ }
+
+ @Test
+ public void unbindAllAssociatedUseCase_closesCamera() {
+ ImageAnalysisUseCaseConfiguration.Builder builder =
+ new ImageAnalysisUseCaseConfiguration.Builder().setLensFacing(DEFAULT_LENS_FACING);
+ new Camera2Configuration.Extender(builder).setDeviceStateCallback(mockStateCallback);
+ ImageAnalysisUseCaseConfiguration configuration = builder.build();
+ ImageAnalysisUseCase useCase = new ImageAnalysisUseCase(configuration);
+ CameraX.bindToLifecycle(lifecycle, useCase);
+ lifecycle.startAndResume();
+
+ CameraX.unbind(useCase);
+
+ verify(mockStateCallback, timeout(3000)).onClosed(any(CameraDevice.class));
+ }
+
+ @Test
+ public void unbindPartialAssociatedUseCase_doesNotCloseCamera() throws InterruptedException {
+ ImageAnalysisUseCaseConfiguration.Builder builder =
+ new ImageAnalysisUseCaseConfiguration.Builder().setLensFacing(DEFAULT_LENS_FACING);
+ new Camera2Configuration.Extender(builder).setDeviceStateCallback(mockStateCallback);
+ ImageAnalysisUseCaseConfiguration configuration0 = builder.build();
+ ImageAnalysisUseCase useCase0 = new ImageAnalysisUseCase(configuration0);
+
+ ImageAnalysisUseCaseConfiguration configuration1 =
+ new ImageAnalysisUseCaseConfiguration.Builder()
+ .setLensFacing(DEFAULT_LENS_FACING)
+ .build();
+ ImageAnalysisUseCase useCase1 = new ImageAnalysisUseCase(configuration1);
+
+ CameraX.bindToLifecycle(lifecycle, useCase0, useCase1);
+ lifecycle.startAndResume();
+
+ CameraX.unbind(useCase1);
+
+ Thread.sleep(3000);
+
+ verify(mockStateCallback, never()).onClosed(any(CameraDevice.class));
+ }
+
+ @Test
+ public void unbindAllAssociatedUseCaseInParts_ClosesCamera() {
+ ImageAnalysisUseCaseConfiguration.Builder builder =
+ new ImageAnalysisUseCaseConfiguration.Builder().setLensFacing(DEFAULT_LENS_FACING);
+ new Camera2Configuration.Extender(builder).setDeviceStateCallback(mockStateCallback);
+ ImageAnalysisUseCaseConfiguration configuration0 = builder.build();
+ ImageAnalysisUseCase useCase0 = new ImageAnalysisUseCase(configuration0);
+
+ ImageAnalysisUseCaseConfiguration configuration1 =
+ new ImageAnalysisUseCaseConfiguration.Builder()
+ .setLensFacing(DEFAULT_LENS_FACING)
+ .build();
+ ImageAnalysisUseCase useCase1 = new ImageAnalysisUseCase(configuration1);
+
+ CameraX.bindToLifecycle(lifecycle, useCase0, useCase1);
+ lifecycle.startAndResume();
+
+ CameraX.unbind(useCase0);
+ CameraX.unbind(useCase1);
+
+ verify(mockStateCallback, timeout(3000).times(1)).onClosed(any(CameraDevice.class));
+ }
+}
diff --git a/camera/camera2/src/androidTest/java/androidx/camera/camera2/Camera2InitializerAndroidTest.java b/camera/camera2/src/androidTest/java/androidx/camera/camera2/Camera2InitializerAndroidTest.java
new file mode 100644
index 0000000..fe6ab47
--- /dev/null
+++ b/camera/camera2/src/androidTest/java/androidx/camera/camera2/Camera2InitializerAndroidTest.java
@@ -0,0 +1,97 @@
+/*
+ * Copyright (C) 2019 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.camera2;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.ProviderInfo;
+import android.test.IsolatedContext;
+import android.test.RenamingDelegatingContext;
+import android.test.mock.MockContentResolver;
+import android.test.mock.MockContext;
+
+import androidx.camera.core.FakeActivity;
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.rule.ActivityTestRule;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/**
+ * Unit tests for {@link Camera2Initializer}.
+ *
+ * <p>The default ProviderTestCase2 cannot be used, because its mock Context returns null when we
+ * call Context.getSystemService(Context.CAMERA_SERVICE). We need to be able to get the camera
+ * service to test CameraX initialization. Therefore, we copy the test strategy employed in
+ * CalendarProvider2Test, where we override a method of the mock Context.
+ */
+@RunWith(AndroidJUnit4.class)
+public final class Camera2InitializerAndroidTest {
+ private static final String TEST_AUTHORITY = "androidx.camera.core";
+ @Rule
+ public ActivityTestRule<FakeActivity> activityRule =
+ new ActivityTestRule<>(
+ FakeActivity.class, /*initialTouchMode=*/ false, /*launchActivity=*/ false);
+ private Context appContext;
+ private Context testContext;
+ private ProviderInfo providerInfo;
+ private Camera2Initializer provider;
+
+ @Before
+ public void setUp() {
+ appContext = ApplicationProvider.getApplicationContext();
+ Context targetContextWrapper =
+ new RenamingDelegatingContext(new MockContext(), appContext, "test.");
+ MockContentResolver resolver = new MockContentResolver();
+ testContext =
+ new IsolatedContext(resolver, targetContextWrapper) {
+ @Override
+ public Object getSystemService(String name) {
+ if (Context.CAMERA_SERVICE.equals(name)
+ || Context.WINDOW_SERVICE.equals(name)) {
+ return appContext.getSystemService(name);
+ }
+ return super.getSystemService(name);
+ }
+ };
+
+ providerInfo = new ProviderInfo();
+ providerInfo.authority = TEST_AUTHORITY;
+ provider = new Camera2Initializer();
+ provider.attachInfo(testContext, providerInfo);
+
+ resolver.addProvider(TEST_AUTHORITY, provider);
+ }
+
+ @Test
+ public void initializerIsConnectedToContext() {
+ assertThat(provider.getContext()).isSameAs(testContext);
+ }
+
+ @Test
+ public void cameraXIsInitialized_beforeActivityIsCreated() {
+ activityRule.launchActivity(new Intent(appContext, FakeActivity.class));
+ FakeActivity activity = activityRule.getActivity();
+
+ assertThat(activity.isCameraXInitializedAtOnCreate()).isTrue();
+ }
+}
diff --git a/camera/camera2/src/androidTest/java/androidx/camera/camera2/CameraAndroidTest.java b/camera/camera2/src/androidTest/java/androidx/camera/camera2/CameraAndroidTest.java
new file mode 100644
index 0000000..94ecbd3
--- /dev/null
+++ b/camera/camera2/src/androidTest/java/androidx/camera/camera2/CameraAndroidTest.java
@@ -0,0 +1,252 @@
+/*
+ * Copyright (C) 2019 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.camera2;
+
+import static org.mockito.Matchers.any;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.timeout;
+import static org.mockito.Mockito.verify;
+
+import android.graphics.ImageFormat;
+import android.hardware.camera2.CameraDevice;
+import android.media.ImageReader;
+import android.media.ImageReader.OnImageAvailableListener;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.util.Size;
+
+import androidx.camera.core.BaseCamera;
+import androidx.camera.core.CameraDeviceConfiguration;
+import androidx.camera.core.CameraFactory;
+import androidx.camera.core.CameraX.LensFacing;
+import androidx.camera.core.FakeUseCase;
+import androidx.camera.core.FakeUseCaseConfiguration;
+import androidx.camera.core.ImmediateSurface;
+import androidx.camera.core.SessionConfiguration;
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mockito;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+@RunWith(AndroidJUnit4.class)
+public class CameraAndroidTest {
+ private static final LensFacing DEFAULT_LENS_FACING = LensFacing.BACK;
+ static CameraFactory cameraFactory;
+
+ BaseCamera camera;
+
+ UseCase fakeUseCase;
+ OnImageAvailableListener mockOnImageAvailableListener;
+ String cameraId;
+
+ private static String getCameraIdForLensFacingUnchecked(LensFacing lensFacing) {
+ try {
+ return cameraFactory.cameraIdForLensFacing(lensFacing);
+ } catch (Exception e) {
+ throw new IllegalArgumentException(
+ "Unable to attach to camera with LensFacing " + lensFacing, e);
+ }
+ }
+
+ @BeforeClass
+ public static void classSetup() {
+ cameraFactory = new Camera2CameraFactory(ApplicationProvider.getApplicationContext());
+ }
+
+ @Before
+ public void setup() {
+ mockOnImageAvailableListener = Mockito.mock(ImageReader.OnImageAvailableListener.class);
+ FakeUseCaseConfiguration configuration =
+ new FakeUseCaseConfiguration.Builder()
+ .setTargetName("UseCase")
+ .setLensFacing(DEFAULT_LENS_FACING)
+ .build();
+ cameraId = getCameraIdForLensFacingUnchecked(DEFAULT_LENS_FACING);
+ fakeUseCase = new UseCase(configuration, mockOnImageAvailableListener);
+ Map<String, Size> suggestedResolutionMap = new HashMap<>();
+ suggestedResolutionMap.put(cameraId, new Size(640, 480));
+ fakeUseCase.updateSuggestedResolution(suggestedResolutionMap);
+
+ camera = cameraFactory.getCamera(cameraId);
+ }
+
+ @After
+ public void teardown() throws InterruptedException {
+ // Need to release the camera no matter what is done, otherwise the CameraDevice is not
+ // closed.
+ // When the CameraDevice is not closed, then it can cause problems with interferes with
+ // other
+ // test cases.
+ if (camera != null) {
+ camera.release();
+ camera = null;
+ }
+
+ // Wait a little bit for the camera device to close.
+ // TODO(b/111991758): Listen for the close signal when it becomes available.
+ Thread.sleep(2000);
+
+ if (fakeUseCase != null) {
+ fakeUseCase.close();
+ fakeUseCase = null;
+ }
+ }
+
+ @Test
+ public void onlineUseCase() {
+ camera.open();
+
+ camera.addOnlineUseCase(Collections.singletonList(fakeUseCase));
+
+ verify(mockOnImageAvailableListener, never()).onImageAvailable(any(ImageReader.class));
+
+ camera.release();
+ }
+
+ @Test
+ public void activeUseCase() {
+ camera.open();
+
+ camera.onUseCaseActive(fakeUseCase);
+
+ verify(mockOnImageAvailableListener, never()).onImageAvailable(any(ImageReader.class));
+
+ camera.release();
+ }
+
+ @Test
+ public void onlineAndActiveUseCase() throws InterruptedException {
+ camera.open();
+
+ camera.addOnlineUseCase(Collections.singletonList(fakeUseCase));
+ camera.onUseCaseActive(fakeUseCase);
+
+ verify(mockOnImageAvailableListener, timeout(4000).atLeastOnce())
+ .onImageAvailable(any(ImageReader.class));
+ }
+
+ @Test
+ public void removeOnlineUseCase() {
+ camera.open();
+
+ camera.addOnlineUseCase(Collections.singletonList(fakeUseCase));
+ camera.removeOnlineUseCase(Collections.singletonList(fakeUseCase));
+ camera.onUseCaseActive(fakeUseCase);
+
+ verify(mockOnImageAvailableListener, never()).onImageAvailable(any(ImageReader.class));
+ }
+
+ @Test
+ public void unopenedCamera() {
+ camera.addOnlineUseCase(Collections.singletonList(fakeUseCase));
+ camera.removeOnlineUseCase(Collections.singletonList(fakeUseCase));
+
+ verify(mockOnImageAvailableListener, never()).onImageAvailable(any(ImageReader.class));
+ }
+
+ @Test
+ public void closedCamera() {
+ camera.open();
+
+ camera.close();
+ camera.addOnlineUseCase(Collections.singletonList(fakeUseCase));
+ camera.removeOnlineUseCase(Collections.singletonList(fakeUseCase));
+
+ verify(mockOnImageAvailableListener, never()).onImageAvailable(any(ImageReader.class));
+ }
+
+ @Test
+ public void releaseUnopenedCamera() {
+ camera.release();
+ camera.open();
+
+ camera.addOnlineUseCase(Collections.singletonList(fakeUseCase));
+ camera.onUseCaseActive(fakeUseCase);
+
+ verify(mockOnImageAvailableListener, never()).onImageAvailable(any(ImageReader.class));
+ }
+
+ @Test
+ public void releasedOpenedCamera() {
+ camera.release();
+ camera.open();
+
+ camera.addOnlineUseCase(Collections.singletonList(fakeUseCase));
+ camera.onUseCaseActive(fakeUseCase);
+
+ verify(mockOnImageAvailableListener, never()).onImageAvailable(any(ImageReader.class));
+ }
+
+ private static class UseCase extends FakeUseCase {
+ private final ImageReader.OnImageAvailableListener imageAvailableListener;
+ HandlerThread handlerThread = new HandlerThread("HandlerThread");
+ Handler handler;
+ ImageReader imageReader;
+
+ UseCase(
+ FakeUseCaseConfiguration configuration,
+ ImageReader.OnImageAvailableListener listener) {
+ super(configuration);
+ imageAvailableListener = listener;
+ handlerThread.start();
+ handler = new Handler(handlerThread.getLooper());
+ Map<String, Size> suggestedResolutionMap = new HashMap<>();
+ String cameraId = getCameraIdForLensFacingUnchecked(configuration.getLensFacing());
+ suggestedResolutionMap.put(cameraId, new Size(640, 480));
+ updateSuggestedResolution(suggestedResolutionMap);
+ }
+
+ void close() {
+ handler.removeCallbacksAndMessages(null);
+ handlerThread.quitSafely();
+ if (imageReader != null) {
+ imageReader.close();
+ }
+ }
+
+ @Override
+ protected Map<String, Size> onSuggestedResolutionUpdated(
+ Map<String, Size> suggestedResolutionMap) {
+ LensFacing lensFacing =
+ ((CameraDeviceConfiguration) getUseCaseConfiguration()).getLensFacing();
+ String cameraId = getCameraIdForLensFacingUnchecked(lensFacing);
+ Size resolution = suggestedResolutionMap.get(cameraId);
+ SessionConfiguration.Builder builder = new SessionConfiguration.Builder();
+ builder.setTemplateType(CameraDevice.TEMPLATE_PREVIEW);
+ imageReader =
+ ImageReader.newInstance(
+ resolution.getWidth(),
+ resolution.getHeight(),
+ ImageFormat.YUV_420_888, /*maxImages*/
+ 2);
+ imageReader.setOnImageAvailableListener(imageAvailableListener, handler);
+ builder.addSurface(new ImmediateSurface(imageReader.getSurface()));
+
+ attachToCamera(cameraId, builder.build());
+ return suggestedResolutionMap;
+ }
+ }
+}
diff --git a/camera/camera2/src/androidTest/java/androidx/camera/camera2/CameraCaptureCallbackAdapterAndroidTest.java b/camera/camera2/src/androidTest/java/androidx/camera/camera2/CameraCaptureCallbackAdapterAndroidTest.java
new file mode 100644
index 0000000..ca03f51
--- /dev/null
+++ b/camera/camera2/src/androidTest/java/androidx/camera/camera2/CameraCaptureCallbackAdapterAndroidTest.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright (C) 2019 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.camera2;
+
+import static org.mockito.Matchers.any;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import android.hardware.camera2.CameraCaptureSession;
+import android.hardware.camera2.CaptureRequest;
+import android.view.Surface;
+
+import androidx.camera.core.CameraCaptureCallback;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.Mockito;
+
+@RunWith(JUnit4.class)
+public final class CameraCaptureCallbackAdapterAndroidTest {
+
+ private CameraCaptureCallback cameraCaptureCallback;
+ private CameraCaptureSession cameraCaptureSession;
+ private CaptureRequest captureRequest;
+ private CameraCaptureCallbackAdapter cameraCaptureCallbackAdapter;
+
+ @Before
+ public void setUp() {
+ cameraCaptureCallback = Mockito.mock(CameraCaptureCallback.class);
+ cameraCaptureSession = Mockito.mock(CameraCaptureSession.class);
+ // Mockito can't mock final class
+ captureRequest = null;
+ Mockito.mock(Surface.class);
+ cameraCaptureCallbackAdapter = new CameraCaptureCallbackAdapter(cameraCaptureCallback);
+ }
+
+ @Test(expected = NullPointerException.class)
+ public void createCameraCaptureCallbackAdapterWithNullArgument() {
+ new CameraCaptureCallbackAdapter(null);
+ }
+
+ @Test
+ public void onCaptureCompleted() {
+ cameraCaptureCallbackAdapter.onCaptureCompleted(
+ cameraCaptureSession, captureRequest, any());
+ verify(cameraCaptureCallback, times(1)).onCaptureCompleted(any());
+ }
+
+ @Test
+ public void onCaptureFailed() {
+ cameraCaptureCallbackAdapter.onCaptureFailed(cameraCaptureSession, captureRequest, any());
+ verify(cameraCaptureCallback, times(1)).onCaptureFailed(any());
+ }
+}
diff --git a/camera/camera2/src/androidTest/java/androidx/camera/camera2/CaptureCallbackContainerAndroidTest.java b/camera/camera2/src/androidTest/java/androidx/camera/camera2/CaptureCallbackContainerAndroidTest.java
new file mode 100644
index 0000000..cfa4aad
--- /dev/null
+++ b/camera/camera2/src/androidTest/java/androidx/camera/camera2/CaptureCallbackContainerAndroidTest.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2019 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.camera2;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.hardware.camera2.CameraCaptureSession.CaptureCallback;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.Mockito;
+
+@RunWith(JUnit4.class)
+public final class CaptureCallbackContainerAndroidTest {
+
+ @Test(expected = NullPointerException.class)
+ public void createCaptureCallbackContainer_withNullArgument() {
+ CaptureCallbackContainer.create(null);
+ }
+
+ @Test
+ public void getCaptureCallback() {
+ CaptureCallback captureCallback = Mockito.mock(CaptureCallback.class);
+ CaptureCallbackContainer callbackContainer =
+ CaptureCallbackContainer.create(captureCallback);
+ assertThat(callbackContainer.getCaptureCallback()).isEqualTo(captureCallback);
+ }
+}
diff --git a/camera/camera2/src/androidTest/java/androidx/camera/camera2/CaptureCallbackConverterAndroidTest.java b/camera/camera2/src/androidTest/java/androidx/camera/camera2/CaptureCallbackConverterAndroidTest.java
new file mode 100644
index 0000000..f5c26be
--- /dev/null
+++ b/camera/camera2/src/androidTest/java/androidx/camera/camera2/CaptureCallbackConverterAndroidTest.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright (C) 2019 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.camera2;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Matchers.any;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import android.hardware.camera2.CameraCaptureSession.CaptureCallback;
+
+import androidx.camera.core.CameraCaptureCallback;
+import androidx.camera.core.CameraCaptureCallbacks;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.Mockito;
+
+@RunWith(JUnit4.class)
+public final class CaptureCallbackConverterAndroidTest {
+
+ @Test
+ public void toCaptureCallback() {
+ CameraCaptureCallback cameraCallback = Mockito.mock(CameraCaptureCallback.class);
+ CaptureCallback callback = CaptureCallbackConverter.toCaptureCallback(cameraCallback);
+ callback.onCaptureCompleted(null, null, null);
+ verify(cameraCallback, times(1)).onCaptureCompleted(any());
+ }
+
+ @Test
+ public void toCaptureCallback_withNullArgument() {
+ CaptureCallback callback = CaptureCallbackConverter.toCaptureCallback(null);
+ assertThat(callback).isNull();
+ }
+
+ @Test
+ public void toCaptureCallback_withCaptureCallbackContainer() {
+ CaptureCallback actualCallback = Mockito.mock(CaptureCallback.class);
+ CaptureCallbackContainer callbackContainer =
+ CaptureCallbackContainer.create(actualCallback);
+ CaptureCallback callback = CaptureCallbackConverter.toCaptureCallback(callbackContainer);
+ callback.onCaptureCompleted(null, null, null);
+ verify(actualCallback, times(1)).onCaptureCompleted(any(), any(), any());
+ }
+
+ @Test
+ public void toCaptureCallback_withComboCameraCallback() {
+ CameraCaptureCallback cameraCallback1 = Mockito.mock(CameraCaptureCallback.class);
+ CameraCaptureCallback cameraCallback2 = Mockito.mock(CameraCaptureCallback.class);
+ CaptureCallback cameraCallback3 = Mockito.mock(CaptureCallback.class);
+
+ CaptureCallback callback =
+ CaptureCallbackConverter.toCaptureCallback(
+ CameraCaptureCallbacks.createComboCallback(
+ cameraCallback1,
+ CameraCaptureCallbacks.createComboCallback(
+ cameraCallback2,
+ CaptureCallbackContainer.create(cameraCallback3))));
+
+ callback.onCaptureCompleted(null, null, null);
+ verify(cameraCallback1, times(1)).onCaptureCompleted(any());
+ verify(cameraCallback2, times(1)).onCaptureCompleted(any());
+ verify(cameraCallback3, times(1)).onCaptureCompleted(any(), any(), any());
+ }
+}
diff --git a/camera/camera2/src/androidTest/java/androidx/camera/camera2/CaptureSessionAndroidTest.java b/camera/camera2/src/androidTest/java/androidx/camera/camera2/CaptureSessionAndroidTest.java
new file mode 100644
index 0000000..4f8e7d0d
--- /dev/null
+++ b/camera/camera2/src/androidTest/java/androidx/camera/camera2/CaptureSessionAndroidTest.java
@@ -0,0 +1,352 @@
+/*
+ * Copyright (C) 2019 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.camera2;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+import static org.mockito.Matchers.any;
+import static org.mockito.Mockito.timeout;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import android.graphics.ImageFormat;
+import android.hardware.camera2.CameraAccessException;
+import android.hardware.camera2.CameraCaptureSession;
+import android.hardware.camera2.CameraDevice;
+import android.media.Image;
+import android.media.ImageReader;
+import android.media.ImageReader.OnImageAvailableListener;
+import android.os.Handler;
+import android.os.HandlerThread;
+
+import androidx.annotation.NonNull;
+import androidx.camera.camera2.CaptureSession.State;
+import androidx.camera.core.CameraCaptureCallback;
+import androidx.camera.core.CameraCaptureCallbacks;
+import androidx.camera.core.CameraCaptureResult;
+import androidx.camera.core.CameraUtil;
+import androidx.camera.core.CaptureRequestConfiguration;
+import androidx.camera.core.ImmediateSurface;
+import androidx.camera.core.SessionConfiguration;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mockito;
+
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Tests for {@link CaptureSession}. This requires an environment where a valid {@link
+ * android.hardware.camera2.CameraDevice} can be opened since it is used to open a {@link
+ * android.hardware.camera2.CaptureRequest}.
+ */
+@RunWith(AndroidJUnit4.class)
+public class CaptureSessionAndroidTest {
+ private CaptureSessionTestParameters testParameters0;
+ private CaptureSessionTestParameters testParameters1;
+
+ private CameraDevice cameraDevice;
+
+ @Before
+ public void setup() throws CameraAccessException, InterruptedException {
+ testParameters0 = new CaptureSessionTestParameters("testParameters0");
+ testParameters1 = new CaptureSessionTestParameters("testParameters1");
+ cameraDevice = CameraUtil.getCameraDevice();
+ }
+
+ @After
+ public void tearDown() {
+ testParameters0.tearDown();
+ testParameters1.tearDown();
+ CameraUtil.releaseCameraDevice(cameraDevice);
+ }
+
+ @Test
+ public void setCaptureSessionSucceed() {
+ CaptureSession captureSession = new CaptureSession(testParameters0.handler);
+
+ captureSession.setSessionConfiguration(testParameters0.sessionConfiguration);
+
+ assertThat(captureSession.getSessionConfiguration())
+ .isEqualTo(testParameters0.sessionConfiguration);
+ }
+
+ @Test
+ public void setCaptureSessionOnClosedSession_throwsException() {
+ CaptureSession captureSession = new CaptureSession(testParameters0.handler);
+ SessionConfiguration newSessionConfiguration = testParameters0.sessionConfiguration;
+
+ captureSession.close();
+
+ assertThrows(
+ IllegalStateException.class,
+ () -> captureSession.setSessionConfiguration(newSessionConfiguration));
+ }
+
+ @Test
+ public void openCaptureSessionSucceed() throws CameraAccessException, InterruptedException {
+ CaptureSession captureSession = new CaptureSession(testParameters0.handler);
+ captureSession.setSessionConfiguration(testParameters0.sessionConfiguration);
+
+ captureSession.open(testParameters0.sessionConfiguration, cameraDevice);
+
+ testParameters0.waitForData();
+
+ assertThat(captureSession.getState()).isEqualTo(State.OPENED);
+
+ // StateCallback.onConfigured() should be called to signal the session is configured.
+ verify(testParameters0.sessionStateCallback, times(1))
+ .onConfigured(any(CameraCaptureSession.class));
+
+ // CameraCaptureCallback.onCaptureCompleted() should be called to signal a capture attempt.
+ verify(testParameters0.sessionCameraCaptureCallback, timeout(3000).atLeast(1))
+ .onCaptureCompleted(any());
+ }
+
+ @Test
+ public void closeUnopenedSession() {
+ CaptureSession captureSession = new CaptureSession(testParameters0.handler);
+ captureSession.setSessionConfiguration(testParameters0.sessionConfiguration);
+
+ captureSession.close();
+
+ assertThat(captureSession.getState()).isEqualTo(State.RELEASED);
+ }
+
+ @Test
+ public void releaseUnopenedSession() {
+ CaptureSession captureSession = new CaptureSession(testParameters0.handler);
+ captureSession.setSessionConfiguration(testParameters0.sessionConfiguration);
+
+ captureSession.release();
+
+ assertThat(captureSession.getState()).isEqualTo(State.RELEASED);
+ }
+
+ @Test
+ public void closeOpenedSession() throws CameraAccessException, InterruptedException {
+ CaptureSession captureSession = new CaptureSession(testParameters0.handler);
+ captureSession.setSessionConfiguration(testParameters0.sessionConfiguration);
+ captureSession.open(testParameters0.sessionConfiguration, cameraDevice);
+
+ captureSession.close();
+
+ Thread.sleep(3000);
+ // Session should not get released until triggered by another session opening
+ assertThat(captureSession.getState()).isEqualTo(State.CLOSED);
+ }
+
+ @Test
+ public void releaseOpenedSession() throws CameraAccessException, InterruptedException {
+ CaptureSession captureSession = new CaptureSession(testParameters0.handler);
+ captureSession.setSessionConfiguration(testParameters0.sessionConfiguration);
+ captureSession.open(testParameters0.sessionConfiguration, cameraDevice);
+ captureSession.release();
+
+ Thread.sleep(3000);
+ assertThat(captureSession.getState()).isEqualTo(State.RELEASED);
+
+ // StateCallback.onClosed() should be called to signal the session is closed.
+ verify(testParameters0.sessionStateCallback, times(1))
+ .onClosed(any(CameraCaptureSession.class));
+ }
+
+ @Test
+ public void openSecondSession() throws CameraAccessException, InterruptedException {
+ CaptureSession captureSession = new CaptureSession(testParameters0.handler);
+ captureSession.setSessionConfiguration(testParameters0.sessionConfiguration);
+
+ // First session is opened
+ captureSession.open(testParameters0.sessionConfiguration, cameraDevice);
+ captureSession.close();
+
+ // Open second session, which should cause first one to be released
+ CaptureSession captureSession1 = new CaptureSession(testParameters1.handler);
+ captureSession1.setSessionConfiguration(testParameters1.sessionConfiguration);
+ captureSession1.open(testParameters1.sessionConfiguration, cameraDevice);
+
+ testParameters1.waitForData();
+
+ assertThat(captureSession1.getState()).isEqualTo(State.OPENED);
+ assertThat(captureSession.getState()).isEqualTo(State.RELEASED);
+
+ // First session should have StateCallback.onConfigured(), onClosed() calls.
+ verify(testParameters0.sessionStateCallback, times(1))
+ .onConfigured(any(CameraCaptureSession.class));
+ verify(testParameters0.sessionStateCallback, times(1))
+ .onClosed(any(CameraCaptureSession.class));
+
+ // Second session should have StateCallback.onConfigured() call.
+ verify(testParameters1.sessionStateCallback, times(1))
+ .onConfigured(any(CameraCaptureSession.class));
+
+ // Second session should have CameraCaptureCallback.onCaptureCompleted() call.
+ verify(testParameters1.sessionCameraCaptureCallback, timeout(3000).atLeast(1))
+ .onCaptureCompleted(any());
+ }
+
+ @Test
+ public void issueSingleCaptureRequest() throws CameraAccessException, InterruptedException {
+ CaptureSession captureSession = new CaptureSession(testParameters0.handler);
+ captureSession.setSessionConfiguration(testParameters0.sessionConfiguration);
+ captureSession.open(testParameters0.sessionConfiguration, cameraDevice);
+
+ testParameters0.waitForData();
+
+ assertThat(captureSession.getState()).isEqualTo(State.OPENED);
+
+ captureSession.issueSingleCaptureRequest(testParameters0.captureRequestConfiguration);
+
+ testParameters0.waitForCameraCaptureCallback();
+
+ // CameraCaptureCallback.onCaptureCompleted() should be called to signal a capture attempt.
+ verify(testParameters0.cameraCaptureCallback, timeout(3000).times(1))
+ .onCaptureCompleted(any());
+ }
+
+ @Test
+ public void issueSingleCaptureRequestBeforeCaptureSessionOpened()
+ throws CameraAccessException, InterruptedException {
+ CaptureSession captureSession = new CaptureSession(testParameters0.handler);
+ captureSession.setSessionConfiguration(testParameters0.sessionConfiguration);
+
+ captureSession.issueSingleCaptureRequest(testParameters0.captureRequestConfiguration);
+ captureSession.open(testParameters0.sessionConfiguration, cameraDevice);
+
+ testParameters0.waitForCameraCaptureCallback();
+
+ // CameraCaptureCallback.onCaptureCompleted() should be called to signal a capture attempt.
+ verify(testParameters0.cameraCaptureCallback, timeout(3000).times(1))
+ .onCaptureCompleted(any());
+ }
+
+ @Test
+ public void issueSingleCaptureRequestOnClosedSession_throwsException() {
+ CaptureSession captureSession = new CaptureSession(testParameters0.handler);
+
+ captureSession.close();
+
+ assertThrows(
+ IllegalStateException.class,
+ () ->
+ captureSession.issueSingleCaptureRequest(
+ testParameters0.captureRequestConfiguration));
+ }
+
+ /**
+ * Collection of parameters required for setting a {@link CaptureSession} and wait for it to
+ * produce data.
+ */
+ private static class CaptureSessionTestParameters {
+ private static final int TIME_TO_WAIT_FOR_DATA_SECONDS = 3;
+ /** Thread for all asynchronous calls. */
+ private final HandlerThread handlerThread;
+ /** Handler for all asynchronous calls. */
+ private final Handler handler;
+ /** Latch to wait for first image data to appear. */
+ private final CountDownLatch dataLatch = new CountDownLatch(1);
+
+ /** Latch to wait for camera capture callback to be invoked. */
+ private final CountDownLatch cameraCaptureCallbackLatch = new CountDownLatch(1);
+
+ /** Image reader that unlocks the latch waiting for the first image data to appear. */
+ private final OnImageAvailableListener onImageAvailableListener =
+ reader -> {
+ Image image = reader.acquireNextImage();
+ if (image != null) {
+ image.close();
+ dataLatch.countDown();
+ }
+ };
+
+ private final ImageReader imageReader;
+ private final SessionConfiguration sessionConfiguration;
+ private final CaptureRequestConfiguration captureRequestConfiguration;
+
+ private final CameraCaptureSession.StateCallback sessionStateCallback =
+ Mockito.mock(CameraCaptureSession.StateCallback.class);
+ private final CameraCaptureCallback sessionCameraCaptureCallback =
+ Mockito.mock(CameraCaptureCallback.class);
+ private final CameraCaptureCallback cameraCaptureCallback =
+ Mockito.mock(CameraCaptureCallback.class);
+
+ /**
+ * A composite capture callback that dispatches callbacks to both mock and real callbacks.
+ * The mock callback is used to verify the callback result. The real callback is used to
+ * unlock the latch waiting.
+ */
+ private final CameraCaptureCallback comboCameraCaptureCallback =
+ CameraCaptureCallbacks.createComboCallback(
+ cameraCaptureCallback,
+ new CameraCaptureCallback() {
+ @Override
+ public void onCaptureCompleted(@NonNull CameraCaptureResult result) {
+ cameraCaptureCallbackLatch.countDown();
+ }
+ });
+
+ CaptureSessionTestParameters(String name) {
+ handlerThread = new HandlerThread(name);
+ handlerThread.start();
+ handler = new Handler(handlerThread.getLooper());
+
+ imageReader =
+ ImageReader.newInstance(640, 480, ImageFormat.YUV_420_888, /*maxImages*/ 2);
+ imageReader.setOnImageAvailableListener(onImageAvailableListener, handler);
+
+ SessionConfiguration.Builder builder = new SessionConfiguration.Builder();
+ builder.setTemplateType(CameraDevice.TEMPLATE_PREVIEW);
+ builder.addSurface(new ImmediateSurface(imageReader.getSurface()));
+ builder.setSessionStateCallback(sessionStateCallback);
+ builder.setCameraCaptureCallback(sessionCameraCaptureCallback);
+
+ sessionConfiguration = builder.build();
+
+ CaptureRequestConfiguration.Builder captureRequestConfigBuilder =
+ new CaptureRequestConfiguration.Builder();
+ captureRequestConfigBuilder.setTemplateType(CameraDevice.TEMPLATE_PREVIEW);
+ captureRequestConfigBuilder.addSurface(new ImmediateSurface(imageReader.getSurface()));
+ captureRequestConfigBuilder.setCameraCaptureCallback(comboCameraCaptureCallback);
+
+ captureRequestConfiguration = captureRequestConfigBuilder.build();
+ }
+
+ /**
+ * Wait for data to get produced by the session.
+ *
+ * @throws InterruptedException if data is not produced after a set amount of time
+ */
+ void waitForData() throws InterruptedException {
+ dataLatch.await(TIME_TO_WAIT_FOR_DATA_SECONDS, TimeUnit.SECONDS);
+ }
+
+ void waitForCameraCaptureCallback() throws InterruptedException {
+ cameraCaptureCallbackLatch.await(TIME_TO_WAIT_FOR_DATA_SECONDS, TimeUnit.SECONDS);
+ }
+
+ /** Clean up resources. */
+ void tearDown() {
+ imageReader.close();
+ handlerThread.quitSafely();
+ }
+ }
+}
diff --git a/camera/camera2/src/androidTest/java/androidx/camera/camera2/FakeRepeatingUseCase.java b/camera/camera2/src/androidTest/java/androidx/camera/camera2/FakeRepeatingUseCase.java
new file mode 100644
index 0000000..a7534dd
--- /dev/null
+++ b/camera/camera2/src/androidTest/java/androidx/camera/camera2/FakeRepeatingUseCase.java
@@ -0,0 +1,97 @@
+/*
+ * Copyright (C) 2019 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.camera2;
+
+import android.graphics.ImageFormat;
+import android.hardware.camera2.CameraDevice;
+import android.media.Image;
+import android.media.ImageReader;
+import android.os.Handler;
+import android.os.Looper;
+import android.util.Size;
+
+import androidx.annotation.RestrictTo;
+import androidx.annotation.RestrictTo.Scope;
+import androidx.camera.core.CameraX;
+import androidx.camera.core.CameraX.LensFacing;
+import androidx.camera.core.FakeUseCase;
+import androidx.camera.core.FakeUseCaseConfiguration;
+import androidx.camera.core.ImmediateSurface;
+import androidx.camera.core.SessionConfiguration;
+import androidx.camera.core.UseCaseConfiguration;
+
+import java.util.Map;
+
+/** A fake {@link FakeUseCase} which contain a repeating surface. */
+@RestrictTo(Scope.LIBRARY_GROUP)
+public class FakeRepeatingUseCase extends FakeUseCase {
+
+ /** The repeating surface. */
+ private final ImageReader imageReader =
+ ImageReader.newInstance(640, 480, ImageFormat.YUV_420_888, 2);
+
+ public FakeRepeatingUseCase(FakeUseCaseConfiguration configuration) {
+ super(configuration);
+
+ FakeUseCaseConfiguration configWithDefaults =
+ (FakeUseCaseConfiguration) getUseCaseConfiguration();
+ imageReader.setOnImageAvailableListener(
+ imageReader -> {
+ Image image = imageReader.acquireLatestImage();
+ if (image != null) {
+ image.close();
+ }
+ },
+ new Handler(Looper.getMainLooper()));
+
+ SessionConfiguration.Builder builder =
+ SessionConfiguration.Builder.createFrom(configWithDefaults);
+ builder.addSurface(new ImmediateSurface(imageReader.getSurface()));
+ try {
+ String cameraId = CameraX.getCameraWithLensFacing(configWithDefaults.getLensFacing());
+ attachToCamera(cameraId, builder.build());
+ } catch (Exception e) {
+ throw new IllegalArgumentException(
+ "Unable to attach to camera with LensFacing "
+ + configWithDefaults.getLensFacing(),
+ e);
+ }
+ }
+
+ @Override
+ protected UseCaseConfiguration.Builder<?, ?, ?> getDefaultBuilder() {
+ return new FakeUseCaseConfiguration.Builder()
+ .setLensFacing(LensFacing.BACK)
+ .setOptionUnpacker(
+ (useCaseConfig, sessionConfigBuilder) -> {
+ // Set the template since it is currently required by implementation
+ sessionConfigBuilder.setTemplateType(CameraDevice.TEMPLATE_PREVIEW);
+ });
+ }
+
+ @Override
+ public void clear() {
+ super.clear();
+ imageReader.close();
+ }
+
+ @Override
+ protected Map<String, Size> onSuggestedResolutionUpdated(
+ Map<String, Size> suggestedResolutionMap) {
+ return suggestedResolutionMap;
+ }
+}
diff --git a/camera/camera2/src/androidTest/java/androidx/camera/camera2/ImageAnalysisUseCaseAndroidTest.java b/camera/camera2/src/androidTest/java/androidx/camera/camera2/ImageAnalysisUseCaseAndroidTest.java
new file mode 100644
index 0000000..5852787
--- /dev/null
+++ b/camera/camera2/src/androidTest/java/androidx/camera/camera2/ImageAnalysisUseCaseAndroidTest.java
@@ -0,0 +1,288 @@
+/*
+ * Copyright (C) 2019 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.camera2;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import android.content.Context;
+import android.graphics.ImageFormat;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.util.Size;
+
+import androidx.camera.core.AppConfiguration;
+import androidx.camera.core.BaseCamera;
+import androidx.camera.core.BaseUseCase.StateChangeListener;
+import androidx.camera.core.CameraFactory;
+import androidx.camera.core.CameraInfoUnavailableException;
+import androidx.camera.core.CameraUtil;
+import androidx.camera.core.CameraX;
+import androidx.camera.core.CameraX.LensFacing;
+import androidx.camera.core.ImageAnalysisUseCase;
+import androidx.camera.core.ImageAnalysisUseCase.Analyzer;
+import androidx.camera.core.ImageAnalysisUseCase.ImageReaderMode;
+import androidx.camera.core.ImageAnalysisUseCaseConfiguration;
+import androidx.camera.core.ImageProxy;
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mockito;
+
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.Semaphore;
+import java.util.concurrent.TimeUnit;
+
+@RunWith(AndroidJUnit4.class)
+public final class ImageAnalysisUseCaseAndroidTest {
+ private static final Size DEFAULT_RESOLUTION = new Size(640, 480);
+ private final ImageAnalysisUseCaseConfiguration defaultConfiguration =
+ ImageAnalysisUseCase.DEFAULT_CONFIG.getConfiguration();
+ private final StateChangeListener mockListener = Mockito.mock(StateChangeListener.class);
+ private final Analyzer mockAnalyzer = Mockito.mock(Analyzer.class);
+ private Set<ImageProperties> analysisResults;
+ private Analyzer analyzer;
+ private BaseCamera camera;
+ private HandlerThread handlerThread;
+ private Handler handler;
+ private Semaphore analysisResultsSemaphore;
+ private String cameraId;
+
+ @Before
+ public void setUp() {
+ analysisResults = new HashSet<>();
+ analysisResultsSemaphore = new Semaphore(/*permits=*/ 0);
+ analyzer =
+ (image, rotationDegrees) -> {
+ analysisResults.add(new ImageProperties(image, rotationDegrees));
+ analysisResultsSemaphore.release();
+ };
+ Context context = ApplicationProvider.getApplicationContext();
+ AppConfiguration config = Camera2AppConfiguration.create(context);
+ CameraFactory cameraFactory = config.getCameraFactory(/*valueIfMissing=*/ null);
+ try {
+ cameraId = cameraFactory.cameraIdForLensFacing(LensFacing.BACK);
+ } catch (Exception e) {
+ throw new IllegalArgumentException(
+ "Unable to attach to camera with LensFacing " + LensFacing.BACK, e);
+ }
+ camera = cameraFactory.getCamera(cameraId);
+
+ CameraX.init(context, config);
+
+ handlerThread = new HandlerThread("AnalysisThread");
+ handlerThread.start();
+ handler = new Handler(handlerThread.getLooper());
+ }
+
+ @After
+ public void tearDown() {
+ handlerThread.quitSafely();
+ camera.release();
+ }
+
+ @Test
+ public void analyzerCanBeSetAndRetrieved() {
+ ImageAnalysisUseCase useCase = new ImageAnalysisUseCase(defaultConfiguration);
+ Map<String, Size> suggestedResolutionMap = new HashMap<>();
+ suggestedResolutionMap.put(cameraId, DEFAULT_RESOLUTION);
+ useCase.updateSuggestedResolution(suggestedResolutionMap);
+
+ Analyzer initialAnalyzer = useCase.getAnalyzer();
+
+ useCase.setAnalyzer(mockAnalyzer);
+
+ Analyzer retrievedAnalyzer = useCase.getAnalyzer();
+
+ // The observer is bound to the lifecycle.
+ assertThat(initialAnalyzer).isNull();
+ assertThat(retrievedAnalyzer).isSameAs(mockAnalyzer);
+ }
+
+ @Test
+ public void becomesActive_whenHasAnalyzer() {
+ ImageAnalysisUseCase useCase = new ImageAnalysisUseCase(defaultConfiguration);
+ Map<String, Size> suggestedResolutionMap = new HashMap<>();
+ suggestedResolutionMap.put(cameraId, DEFAULT_RESOLUTION);
+ useCase.updateSuggestedResolution(suggestedResolutionMap);
+ useCase.addStateChangeListener(mockListener);
+
+ useCase.setAnalyzer(mockAnalyzer);
+
+ verify(mockListener, times(1)).onUseCaseActive(useCase);
+ }
+
+ @Test
+ public void becomesInactive_whenNoAnalyzer() {
+ ImageAnalysisUseCase useCase = new ImageAnalysisUseCase(defaultConfiguration);
+ Map<String, Size> suggestedResolutionMap = new HashMap<>();
+ suggestedResolutionMap.put(cameraId, DEFAULT_RESOLUTION);
+ useCase.updateSuggestedResolution(suggestedResolutionMap);
+ useCase.addStateChangeListener(mockListener);
+ useCase.setAnalyzer(mockAnalyzer);
+ useCase.removeAnalyzer();
+
+ verify(mockListener, times(1)).onUseCaseInactive(useCase);
+ }
+
+ @Test(timeout = 5000)
+ public void analyzerAnalyzesImages_whenCameraIsOpen()
+ throws InterruptedException, CameraInfoUnavailableException {
+ final int imageFormat = ImageFormat.YUV_420_888;
+ ImageAnalysisUseCaseConfiguration configuration =
+ new ImageAnalysisUseCaseConfiguration.Builder().setCallbackHandler(handler).build();
+ ImageAnalysisUseCase useCase = new ImageAnalysisUseCase(configuration);
+ Map<String, Size> suggestedResolutionMap = new HashMap<>();
+ suggestedResolutionMap.put(cameraId, DEFAULT_RESOLUTION);
+ useCase.updateSuggestedResolution(suggestedResolutionMap);
+ CameraUtil.openCameraWithUseCase(camera, useCase);
+ useCase.setAnalyzer(analyzer);
+
+ int sensorRotation = CameraX.getCameraInfo(cameraId).getSensorRotationDegrees();
+ // The frames should have properties which match the configuration.
+ for (ImageProperties properties : analysisResults) {
+ assertThat(properties.resolution).isEqualTo(DEFAULT_RESOLUTION);
+ assertThat(properties.format).isEqualTo(imageFormat);
+ assertThat(properties.rotationDegrees).isEqualTo(sensorRotation);
+ }
+ }
+
+ @Test
+ public void analyzerDoesNotAnalyzeImages_whenCameraIsNotOpen() throws InterruptedException {
+ ImageAnalysisUseCaseConfiguration configuration =
+ new ImageAnalysisUseCaseConfiguration.Builder().setCallbackHandler(handler).build();
+ ImageAnalysisUseCase useCase = new ImageAnalysisUseCase(configuration);
+ Map<String, Size> suggestedResolutionMap = new HashMap<>();
+ suggestedResolutionMap.put(cameraId, DEFAULT_RESOLUTION);
+ useCase.updateSuggestedResolution(suggestedResolutionMap);
+ useCase.setAnalyzer(analyzer);
+ // Keep the lifecycle in an inactive state.
+ // Wait a little while for frames to be analyzed.
+ analysisResultsSemaphore.tryAcquire(5, TimeUnit.SECONDS);
+
+ // No frames should have been analyzed.
+ assertThat(analysisResults).isEmpty();
+ }
+
+ @Test(timeout = 5000)
+ public void updateSessionConfigurationWithSuggestedResolution() throws InterruptedException {
+ final int imageFormat = ImageFormat.YUV_420_888;
+ final Size[] sizes = {new Size(1280, 720), new Size(640, 480)};
+
+ ImageAnalysisUseCaseConfiguration configuration =
+ new ImageAnalysisUseCaseConfiguration.Builder().setCallbackHandler(handler).build();
+ ImageAnalysisUseCase useCase = new ImageAnalysisUseCase(configuration);
+ useCase.setAnalyzer(analyzer);
+
+ for (Size size : sizes) {
+ Map<String, Size> suggestedResolutionMap = new HashMap<>();
+ suggestedResolutionMap.put(cameraId, size);
+ useCase.updateSuggestedResolution(suggestedResolutionMap);
+ CameraUtil.openCameraWithUseCase(camera, useCase);
+
+ // Clear previous results
+ analysisResults.clear();
+ // Wait a little while for frames to be analyzed.
+ analysisResultsSemaphore.tryAcquire(5, TimeUnit.SECONDS);
+
+ // The frames should have properties which match the configuration.
+ for (ImageProperties properties : analysisResults) {
+ assertThat(properties.resolution).isEqualTo(size);
+ assertThat(properties.format).isEqualTo(imageFormat);
+ }
+
+ // Detach use case from camera device to run next resolution setting
+ CameraUtil.detachUseCaseFromCamera(camera, useCase);
+ }
+ }
+
+ @Test
+ public void defaultsIncludeImageReaderMode() {
+ ImageAnalysisUseCaseConfiguration defaultConfig =
+ ImageAnalysisUseCase.DEFAULT_CONFIG.getConfiguration();
+
+ // Will throw if mode does not exist
+ ImageReaderMode mode = defaultConfig.getImageReaderMode();
+
+ // Should not be null
+ assertThat(mode).isNotNull();
+ }
+
+ @Test
+ public void defaultsIncludeImageQueueDepth() {
+ ImageAnalysisUseCaseConfiguration defaultConfig =
+ ImageAnalysisUseCase.DEFAULT_CONFIG.getConfiguration();
+
+ // Will throw if depth does not exist
+ int depth = defaultConfig.getImageQueueDepth();
+
+ // Should not be less than 1
+ assertThat(depth).isAtLeast(1);
+ }
+
+ private static class ImageProperties {
+ final Size resolution;
+ final int format;
+ final long timestamp;
+ final int rotationDegrees;
+
+ ImageProperties(ImageProxy image, int rotationDegrees) {
+ this.resolution = new Size(image.getWidth(), image.getHeight());
+ this.format = image.getFormat();
+ this.timestamp = image.getTimestamp();
+ this.rotationDegrees = rotationDegrees;
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (this == other) {
+ return true;
+ }
+ if (other == null) {
+ return false;
+ }
+ if (!(other instanceof ImageProperties)) {
+ return false;
+ }
+ ImageProperties otherProperties = (ImageProperties) other;
+ return resolution.equals(otherProperties.resolution)
+ && format == otherProperties.format
+ && otherProperties.timestamp == timestamp
+ && otherProperties.rotationDegrees == rotationDegrees;
+ }
+
+ @Override
+ public int hashCode() {
+ int hash = 7;
+ hash = 31 * hash + resolution.getWidth();
+ hash = 31 * hash + resolution.getHeight();
+ hash = 31 * hash + format;
+ hash = 31 * hash + (int) timestamp;
+ hash = 31 * hash + rotationDegrees;
+ return hash;
+ }
+ }
+}
diff --git a/camera/camera2/src/androidTest/java/androidx/camera/camera2/ImageCaptureUseCaseAndroidTest.java b/camera/camera2/src/androidTest/java/androidx/camera/camera2/ImageCaptureUseCaseAndroidTest.java
new file mode 100644
index 0000000..b94e9cf
--- /dev/null
+++ b/camera/camera2/src/androidTest/java/androidx/camera/camera2/ImageCaptureUseCaseAndroidTest.java
@@ -0,0 +1,456 @@
+/*
+ * Copyright (C) 2019 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.camera2;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.anyInt;
+import static org.mockito.Matchers.anyObject;
+import static org.mockito.Matchers.anyString;
+import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import android.content.Context;
+import android.location.Location;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.util.Size;
+import android.view.Surface;
+
+import androidx.annotation.Nullable;
+import androidx.camera.core.AppConfiguration;
+import androidx.camera.core.BaseCamera;
+import androidx.camera.core.CameraFactory;
+import androidx.camera.core.CameraInfoUnavailableException;
+import androidx.camera.core.CameraUtil;
+import androidx.camera.core.CameraX;
+import androidx.camera.core.CameraX.LensFacing;
+import androidx.camera.core.Exif;
+import androidx.camera.core.FakeUseCaseConfiguration;
+import androidx.camera.core.ImageCaptureUseCase;
+import androidx.camera.core.ImageCaptureUseCase.Metadata;
+import androidx.camera.core.ImageCaptureUseCase.OnImageCapturedListener;
+import androidx.camera.core.ImageCaptureUseCase.OnImageSavedListener;
+import androidx.camera.core.ImageCaptureUseCase.UseCaseError;
+import androidx.camera.core.ImageCaptureUseCaseConfiguration;
+import androidx.camera.core.ImageProxy;
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mockito;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.Semaphore;
+
+@RunWith(AndroidJUnit4.class)
+public final class ImageCaptureUseCaseAndroidTest {
+ private static final Size DEFAULT_RESOLUTION = new Size(1920, 1080);
+ private static final LensFacing BACK_LENS_FACING = LensFacing.BACK;
+
+ private HandlerThread handlerThread;
+ private Handler handler;
+ private BaseCamera camera;
+ private ImageCaptureUseCaseConfiguration defaultConfiguration;
+ private ImageCaptureUseCase.OnImageCapturedListener onImageCapturedListener;
+ private ImageCaptureUseCase.OnImageCapturedListener mockImageCapturedListener;
+ private ImageCaptureUseCase.OnImageSavedListener onImageSavedListener;
+ private ImageCaptureUseCase.OnImageSavedListener mockImageSavedListener;
+ private ImageProxy capturedImage;
+ private Semaphore semaphore;
+ private FakeRepeatingUseCase repeatingUseCase;
+ private FakeUseCaseConfiguration fakeRepeatingConfiguration;
+ private String cameraId;
+
+ private ImageCaptureUseCaseConfiguration createNonRotatedConfiguration()
+ throws CameraInfoUnavailableException {
+ // Create a configuration with target rotation that matches the sensor rotation.
+ // This assumes a back-facing camera (facing away from screen)
+ String backCameraId = CameraX.getCameraWithLensFacing(BACK_LENS_FACING);
+ int sensorRotation = CameraX.getCameraInfo(backCameraId).getSensorRotationDegrees();
+
+ int surfaceRotation = Surface.ROTATION_0;
+ switch (sensorRotation) {
+ case 0:
+ surfaceRotation = Surface.ROTATION_0;
+ break;
+ case 90:
+ surfaceRotation = Surface.ROTATION_90;
+ break;
+ case 180:
+ surfaceRotation = Surface.ROTATION_180;
+ break;
+ case 270:
+ surfaceRotation = Surface.ROTATION_270;
+ break;
+ default:
+ throw new IllegalStateException("Invalid sensor rotation: " + sensorRotation);
+ }
+
+ return new ImageCaptureUseCaseConfiguration.Builder()
+ .setLensFacing(BACK_LENS_FACING)
+ .setTargetRotation(surfaceRotation)
+ .build();
+ }
+
+ @Before
+ public void setUp() {
+ handlerThread = new HandlerThread("CaptureThread");
+ handlerThread.start();
+ handler = new Handler(handlerThread.getLooper());
+ Context context = ApplicationProvider.getApplicationContext();
+ AppConfiguration appConfig = Camera2AppConfiguration.create(context);
+ CameraFactory cameraFactory = appConfig.getCameraFactory(null);
+ CameraX.init(context, appConfig);
+ try {
+ cameraId = cameraFactory.cameraIdForLensFacing(BACK_LENS_FACING);
+ } catch (Exception e) {
+ throw new IllegalArgumentException(
+ "Unable to attach to camera with LensFacing " + BACK_LENS_FACING, e);
+ }
+ defaultConfiguration = new ImageCaptureUseCaseConfiguration.Builder().build();
+
+ camera = cameraFactory.getCamera(cameraId);
+ capturedImage = null;
+ semaphore = new Semaphore(/*permits=*/ 0);
+ onImageCapturedListener =
+ new OnImageCapturedListener() {
+ @Override
+ public void onCaptureSuccess(ImageProxy image, int rotationDegrees) {
+ capturedImage = image;
+ // Signal that the image has been captured.
+ semaphore.release();
+ }
+ };
+ mockImageCapturedListener = Mockito.mock(OnImageCapturedListener.class);
+ mockImageSavedListener = Mockito.mock(OnImageSavedListener.class);
+ onImageSavedListener =
+ new OnImageSavedListener() {
+ @Override
+ public void onImageSaved(File file) {
+ mockImageSavedListener.onImageSaved(file);
+ // Signal that an image was saved
+ semaphore.release();
+ }
+
+ @Override
+ public void onError(
+ UseCaseError error, String message, @Nullable Throwable cause) {
+ mockImageSavedListener.onError(error, message, cause);
+ // Signal that there was an error
+ semaphore.release();
+ }
+ };
+
+ fakeRepeatingConfiguration = new FakeUseCaseConfiguration.Builder().build();
+ repeatingUseCase = new FakeRepeatingUseCase(fakeRepeatingConfiguration);
+ }
+
+ @After
+ public void tearDown() {
+ handlerThread.quitSafely();
+ camera.release();
+ if (capturedImage != null) {
+ capturedImage.close();
+ }
+ }
+
+ @Test
+ public void capturedImageHasCorrectProperties() throws InterruptedException {
+ ImageCaptureUseCaseConfiguration configuration =
+ new ImageCaptureUseCaseConfiguration.Builder().setCallbackHandler(handler).build();
+ ImageCaptureUseCase useCase = new ImageCaptureUseCase(configuration);
+ Map<String, Size> suggestedResolutionMap = new HashMap<>();
+ suggestedResolutionMap.put(cameraId, DEFAULT_RESOLUTION);
+ useCase.updateSuggestedResolution(suggestedResolutionMap);
+ CameraUtil.openCameraWithUseCase(camera, useCase, repeatingUseCase);
+ useCase.addStateChangeListener(camera);
+
+ useCase.takePicture(onImageCapturedListener);
+ // Wait for the signal that the image has been captured.
+ semaphore.acquire();
+
+ assertThat(new Size(capturedImage.getWidth(), capturedImage.getHeight()))
+ .isEqualTo(DEFAULT_RESOLUTION);
+ assertThat(capturedImage.getFormat()).isEqualTo(useCase.getImageFormat());
+ }
+
+ @Test(timeout = 5000)
+ public void canCaptureMultipleImages() throws InterruptedException {
+ ImageCaptureUseCase useCase = new ImageCaptureUseCase(defaultConfiguration);
+ Map<String, Size> suggestedResolutionMap = new HashMap<>();
+ suggestedResolutionMap.put(cameraId, DEFAULT_RESOLUTION);
+ useCase.updateSuggestedResolution(suggestedResolutionMap);
+ CameraUtil.openCameraWithUseCase(camera, useCase, repeatingUseCase);
+ useCase.addStateChangeListener(camera);
+
+ int numImages = 5;
+ for (int i = 0; i < numImages; ++i) {
+ useCase.takePicture(
+ new ImageCaptureUseCase.OnImageCapturedListener() {
+ @Override
+ public void onCaptureSuccess(ImageProxy image, int rotationDegrees) {
+ mockImageCapturedListener.onCaptureSuccess(image, rotationDegrees);
+ image.close();
+
+ // Signal that an image has been captured.
+ semaphore.release();
+ }
+ });
+ }
+
+ // Wait for the signal that all images have been captured.
+ semaphore.acquire(numImages);
+
+ verify(mockImageCapturedListener, times(numImages)).onCaptureSuccess(any(), anyInt());
+ }
+
+ @Test(timeout = 10000)
+ public void canCaptureMultipleImagesWithMaxQuality() throws InterruptedException {
+ ImageCaptureUseCaseConfiguration configuration =
+ new ImageCaptureUseCaseConfiguration.Builder()
+ .setCaptureMode(ImageCaptureUseCase.CaptureMode.MAX_QUALITY)
+ .build();
+ ImageCaptureUseCase useCase = new ImageCaptureUseCase(configuration);
+ Map<String, Size> suggestedResolutionMap = new HashMap<>();
+ suggestedResolutionMap.put(cameraId, DEFAULT_RESOLUTION);
+ useCase.updateSuggestedResolution(suggestedResolutionMap);
+ CameraUtil.openCameraWithUseCase(camera, useCase, repeatingUseCase);
+ useCase.addStateChangeListener(camera);
+
+ int numImages = 5;
+ for (int i = 0; i < numImages; ++i) {
+ useCase.takePicture(
+ new ImageCaptureUseCase.OnImageCapturedListener() {
+ @Override
+ public void onCaptureSuccess(ImageProxy image, int rotationDegrees) {
+ mockImageCapturedListener.onCaptureSuccess(image, rotationDegrees);
+ image.close();
+
+ // Signal that an image has been captured.
+ semaphore.release();
+ }
+ });
+ }
+
+ // Wait for the signal that all images have been captured.
+ semaphore.acquire(numImages);
+
+ verify(mockImageCapturedListener, times(numImages)).onCaptureSuccess(any(), anyInt());
+ }
+
+ @Test(timeout = 5000)
+ public void saveCanSucceed() throws InterruptedException, IOException {
+ ImageCaptureUseCase useCase = new ImageCaptureUseCase(defaultConfiguration);
+ Map<String, Size> suggestedResolutionMap = new HashMap<>();
+ suggestedResolutionMap.put(cameraId, DEFAULT_RESOLUTION);
+ useCase.updateSuggestedResolution(suggestedResolutionMap);
+ CameraUtil.openCameraWithUseCase(camera, useCase, repeatingUseCase);
+ useCase.addStateChangeListener(camera);
+
+ File saveLocation = File.createTempFile("test", ".jpg");
+ saveLocation.deleteOnExit();
+ useCase.takePicture(saveLocation, onImageSavedListener);
+
+ // Wait for the signal that the image has been saved.
+ semaphore.acquire();
+
+ verify(mockImageSavedListener).onImageSaved(eq(saveLocation));
+ }
+
+ @Test(timeout = 5000)
+ public void canSaveFile_withRotation()
+ throws InterruptedException, IOException, CameraInfoUnavailableException {
+ ImageCaptureUseCase useCase = new ImageCaptureUseCase(defaultConfiguration);
+ Map<String, Size> suggestedResolutionMap = new HashMap<>();
+ suggestedResolutionMap.put(cameraId, DEFAULT_RESOLUTION);
+ useCase.updateSuggestedResolution(suggestedResolutionMap);
+ CameraUtil.openCameraWithUseCase(camera, useCase, repeatingUseCase);
+ useCase.addStateChangeListener(camera);
+
+ File saveLocation = File.createTempFile("test", ".jpg");
+ saveLocation.deleteOnExit();
+ Metadata metadata = new Metadata();
+ useCase.takePicture(saveLocation, onImageSavedListener, metadata);
+
+ // Wait for the signal that the image has been saved.
+ semaphore.acquire();
+
+ // Retrieve the sensor orientation
+ int rotationDegrees = CameraX.getCameraInfo(cameraId).getSensorRotationDegrees();
+
+ // Retrieve the exif from the image
+ Exif exif = Exif.createFromFile(saveLocation);
+ assertThat(exif.getRotation()).isEqualTo(rotationDegrees);
+ }
+
+ @Test(timeout = 5000)
+ public void canSaveFile_flippedHorizontal()
+ throws InterruptedException, IOException, CameraInfoUnavailableException {
+ // Use a non-rotated configuration since some combinations of rotation + flipping vertically
+ // can
+ // be equivalent to flipping horizontally
+ ImageCaptureUseCase useCase = new ImageCaptureUseCase(createNonRotatedConfiguration());
+ Map<String, Size> suggestedResolutionMap = new HashMap<>();
+ suggestedResolutionMap.put(cameraId, DEFAULT_RESOLUTION);
+ useCase.updateSuggestedResolution(suggestedResolutionMap);
+ CameraUtil.openCameraWithUseCase(camera, useCase, repeatingUseCase);
+ useCase.addStateChangeListener(camera);
+
+ File saveLocation = File.createTempFile("test", ".jpg");
+ saveLocation.deleteOnExit();
+ Metadata metadata = new Metadata();
+ metadata.isReversedHorizontal = true;
+ useCase.takePicture(saveLocation, onImageSavedListener, metadata);
+
+ // Wait for the signal that the image has been saved.
+ semaphore.acquire();
+
+ // Retrieve the exif from the image
+ Exif exif = Exif.createFromFile(saveLocation);
+ assertThat(exif.isFlippedHorizontally()).isTrue();
+ }
+
+ @Test(timeout = 5000)
+ public void canSaveFile_flippedVertical()
+ throws InterruptedException, IOException, CameraInfoUnavailableException {
+ // Use a non-rotated configuration since some combinations of rotation + flipping
+ // horizontally
+ // can be equivalent to flipping vertically
+ ImageCaptureUseCase useCase = new ImageCaptureUseCase(createNonRotatedConfiguration());
+ Map<String, Size> suggestedResolutionMap = new HashMap<>();
+ suggestedResolutionMap.put(cameraId, DEFAULT_RESOLUTION);
+ useCase.updateSuggestedResolution(suggestedResolutionMap);
+ CameraUtil.openCameraWithUseCase(camera, useCase, repeatingUseCase);
+ useCase.addStateChangeListener(camera);
+
+ File saveLocation = File.createTempFile("test", ".jpg");
+ saveLocation.deleteOnExit();
+ Metadata metadata = new Metadata();
+ metadata.isReversedVertical = true;
+ useCase.takePicture(saveLocation, onImageSavedListener, metadata);
+
+ // Wait for the signal that the image has been saved.
+ semaphore.acquire();
+
+ // Retrieve the exif from the image
+ Exif exif = Exif.createFromFile(saveLocation);
+ assertThat(exif.isFlippedVertically()).isTrue();
+ }
+
+ @Test(timeout = 5000)
+ public void canSaveFile_withAttachedLocation() throws InterruptedException, IOException {
+ ImageCaptureUseCase useCase = new ImageCaptureUseCase(defaultConfiguration);
+ Map<String, Size> suggestedResolutionMap = new HashMap<>();
+ suggestedResolutionMap.put(cameraId, DEFAULT_RESOLUTION);
+ useCase.updateSuggestedResolution(suggestedResolutionMap);
+ CameraUtil.openCameraWithUseCase(camera, useCase, repeatingUseCase);
+ useCase.addStateChangeListener(camera);
+
+ File saveLocation = File.createTempFile("test", ".jpg");
+ saveLocation.deleteOnExit();
+ Location location = new Location("ImageCaptureUseCaseAndroidTest");
+ Metadata metadata = new Metadata();
+ metadata.location = location;
+ useCase.takePicture(saveLocation, onImageSavedListener, metadata);
+
+ // Wait for the signal that the image has been saved.
+ semaphore.acquire();
+
+ // Retrieve the exif from the image
+ Exif exif = Exif.createFromFile(saveLocation);
+ assertThat(exif.getLocation().getProvider()).isEqualTo(location.getProvider());
+ }
+
+ @Test(timeout = 5000)
+ public void canSaveMultipleFiles() throws InterruptedException, IOException {
+ ImageCaptureUseCase useCase = new ImageCaptureUseCase(defaultConfiguration);
+ Map<String, Size> suggestedResolutionMap = new HashMap<>();
+ suggestedResolutionMap.put(cameraId, DEFAULT_RESOLUTION);
+ useCase.updateSuggestedResolution(suggestedResolutionMap);
+ CameraUtil.openCameraWithUseCase(camera, useCase, repeatingUseCase);
+ useCase.addStateChangeListener(camera);
+
+ int numImages = 5;
+ for (int i = 0; i < numImages; ++i) {
+ File saveLocation = File.createTempFile("test" + i, ".jpg");
+ saveLocation.deleteOnExit();
+
+ useCase.takePicture(saveLocation, onImageSavedListener);
+ }
+
+ // Wait for the signal that all images have been saved.
+ semaphore.acquire(numImages);
+
+ verify(mockImageSavedListener, times(numImages)).onImageSaved(anyObject());
+ }
+
+ @Test(timeout = 5000)
+ public void saveWillFail_whenInvalidFilePathIsUsed() throws InterruptedException {
+ ImageCaptureUseCase useCase = new ImageCaptureUseCase(defaultConfiguration);
+ Map<String, Size> suggestedResolutionMap = new HashMap<>();
+ suggestedResolutionMap.put(cameraId, DEFAULT_RESOLUTION);
+ useCase.updateSuggestedResolution(suggestedResolutionMap);
+ CameraUtil.openCameraWithUseCase(camera, useCase, repeatingUseCase);
+ useCase.addStateChangeListener(camera);
+
+ // Note the invalid path
+ File saveLocation = new File("/not/a/real/path.jpg");
+ useCase.takePicture(saveLocation, onImageSavedListener);
+
+ // Wait for the signal that an error occurred.
+ semaphore.acquire();
+
+ verify(mockImageSavedListener)
+ .onError(eq(UseCaseError.FILE_IO_ERROR), anyString(), anyObject());
+ }
+
+ @Test(timeout = 5000)
+ public void updateSessionConfigurationWithSuggestedResolution() throws InterruptedException {
+ ImageCaptureUseCaseConfiguration configuration =
+ new ImageCaptureUseCaseConfiguration.Builder().setCallbackHandler(handler).build();
+ ImageCaptureUseCase useCase = new ImageCaptureUseCase(configuration);
+ useCase.addStateChangeListener(camera);
+ final Size[] sizes = {new Size(1920, 1080), new Size(640, 480)};
+
+ for (Size size : sizes) {
+ Map<String, Size> suggestedResolutionMap = new HashMap<>();
+ suggestedResolutionMap.put(cameraId, size);
+ // Update SessionConfiguration with resolution setting
+ useCase.updateSuggestedResolution(suggestedResolutionMap);
+ CameraUtil.openCameraWithUseCase(camera, useCase, repeatingUseCase);
+
+ useCase.takePicture(onImageCapturedListener);
+ // Wait for the signal that the image has been captured.
+ semaphore.acquire();
+
+ assertThat(new Size(capturedImage.getWidth(), capturedImage.getHeight()))
+ .isEqualTo(size);
+
+ // Detach use case from camera device to run next resolution setting
+ CameraUtil.detachUseCaseFromCamera(camera, useCase);
+ }
+ }
+}
diff --git a/camera/camera2/src/androidTest/java/androidx/camera/camera2/ImageReaderProxysAndroidTest.java b/camera/camera2/src/androidTest/java/androidx/camera/camera2/ImageReaderProxysAndroidTest.java
new file mode 100644
index 0000000..499e0e3
--- /dev/null
+++ b/camera/camera2/src/androidTest/java/androidx/camera/camera2/ImageReaderProxysAndroidTest.java
@@ -0,0 +1,164 @@
+/*
+ * Copyright (C) 2019 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.camera2;
+
+import android.content.Context;
+import android.graphics.ImageFormat;
+import android.hardware.camera2.CameraDevice;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.util.Size;
+
+import androidx.camera.core.AppConfiguration;
+import androidx.camera.core.BaseCamera;
+import androidx.camera.core.CameraFactory;
+import androidx.camera.core.CameraUtil;
+import androidx.camera.core.CameraX;
+import androidx.camera.core.FakeUseCase;
+import androidx.camera.core.FakeUseCaseConfiguration;
+import androidx.camera.core.ImageProxy;
+import androidx.camera.core.ImageReaderProxy;
+import androidx.camera.core.ImageReaderProxys;
+import androidx.camera.core.ImmediateSurface;
+import androidx.camera.core.SessionConfiguration;
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.Semaphore;
+
+@RunWith(AndroidJUnit4.class)
+public final class ImageReaderProxysAndroidTest {
+ private static final String CAMERA_ID = "0";
+
+ private BaseCamera camera;
+ private HandlerThread handlerThread;
+ private Handler handler;
+
+ private static ImageReaderProxy.OnImageAvailableListener createSemaphoreReleasingListener(
+ Semaphore semaphore) {
+ return reader -> {
+ ImageProxy image = reader.acquireLatestImage();
+ if (image != null) {
+ semaphore.release();
+ image.close();
+ }
+ };
+ }
+
+ @Before
+ public void setUp() {
+ Context context = ApplicationProvider.getApplicationContext();
+ AppConfiguration appConfig = Camera2AppConfiguration.create(context);
+ CameraFactory cameraFactory = appConfig.getCameraFactory(null);
+ CameraX.init(context, appConfig);
+ camera = cameraFactory.getCamera(CAMERA_ID);
+ handlerThread = new HandlerThread("Background");
+ handlerThread.start();
+ handler = new Handler(handlerThread.getLooper());
+ }
+
+ @After
+ public void tearDown() {
+ camera.release();
+ handlerThread.quitSafely();
+ }
+
+ @Test(timeout = 5000)
+ public void sharedReadersGetFramesFromCamera() throws InterruptedException {
+ List<ImageReaderProxy> readers = new ArrayList<>();
+ List<Semaphore> semaphores = new ArrayList<>();
+ for (int i = 0; i < 2; ++i) {
+ ImageReaderProxy reader =
+ ImageReaderProxys.createSharedReader(
+ CAMERA_ID, 640, 480, ImageFormat.YUV_420_888, 2, handler);
+ Semaphore semaphore = new Semaphore(/*permits=*/ 0);
+ reader.setOnImageAvailableListener(
+ createSemaphoreReleasingListener(semaphore), handler);
+ readers.add(reader);
+ semaphores.add(semaphore);
+ }
+
+ FakeUseCaseConfiguration configuration =
+ new FakeUseCaseConfiguration.Builder().setTargetName("UseCase").build();
+ UseCase useCase = new UseCase(configuration, readers);
+ CameraUtil.openCameraWithUseCase(camera, useCase);
+
+ // Wait for a few frames to be observed.
+ for (Semaphore semaphore : semaphores) {
+ semaphore.acquire(/*permits=*/ 5);
+ }
+ }
+
+ @Test(timeout = 5000)
+ public void isolatedReadersGetFramesFromCamera() throws InterruptedException {
+ List<ImageReaderProxy> readers = new ArrayList<>();
+ List<Semaphore> semaphores = new ArrayList<>();
+ for (int i = 0; i < 2; ++i) {
+ ImageReaderProxy reader =
+ ImageReaderProxys.createIsolatedReader(
+ 640, 480, ImageFormat.YUV_420_888, 2, handler);
+ Semaphore semaphore = new Semaphore(/*permits=*/ 0);
+ reader.setOnImageAvailableListener(
+ createSemaphoreReleasingListener(semaphore), handler);
+ readers.add(reader);
+ semaphores.add(semaphore);
+ }
+
+ FakeUseCaseConfiguration configuration =
+ new FakeUseCaseConfiguration.Builder().setTargetName("UseCase").build();
+ UseCase useCase = new UseCase(configuration, readers);
+ CameraUtil.openCameraWithUseCase(camera, useCase);
+
+ // Wait for a few frames to be observed.
+ for (Semaphore semaphore : semaphores) {
+ semaphore.acquire(/*permits=*/ 5);
+ }
+ }
+
+ private static final class UseCase extends FakeUseCase {
+ private final List<ImageReaderProxy> imageReaders;
+
+ private UseCase(FakeUseCaseConfiguration configuration, List<ImageReaderProxy> readers) {
+ super(configuration);
+ imageReaders = readers;
+ Map<String, Size> suggestedResolutionMap = new HashMap<>();
+ suggestedResolutionMap.put(CAMERA_ID, new Size(640, 480));
+ updateSuggestedResolution(suggestedResolutionMap);
+ }
+
+ @Override
+ protected Map<String, Size> onSuggestedResolutionUpdated(
+ Map<String, Size> suggestedResolutionMap) {
+ SessionConfiguration.Builder sessionConfigBuilder = new SessionConfiguration.Builder();
+ sessionConfigBuilder.setTemplateType(CameraDevice.TEMPLATE_PREVIEW);
+ for (ImageReaderProxy reader : imageReaders) {
+ sessionConfigBuilder.addSurface(new ImmediateSurface(reader.getSurface()));
+ }
+ attachToCamera(CAMERA_ID, sessionConfigBuilder.build());
+ return suggestedResolutionMap;
+ }
+ }
+}
diff --git a/camera/camera2/src/androidTest/java/androidx/camera/camera2/SemaphoreReleasingCamera2Callbacks.java b/camera/camera2/src/androidTest/java/androidx/camera/camera2/SemaphoreReleasingCamera2Callbacks.java
new file mode 100644
index 0000000..71e4b84
--- /dev/null
+++ b/camera/camera2/src/androidTest/java/androidx/camera/camera2/SemaphoreReleasingCamera2Callbacks.java
@@ -0,0 +1,238 @@
+/*
+ * Copyright (C) 2019 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.camera2;
+
+import android.hardware.camera2.CameraCaptureSession;
+import android.hardware.camera2.CameraDevice;
+import android.hardware.camera2.CaptureFailure;
+import android.hardware.camera2.CaptureRequest;
+import android.hardware.camera2.CaptureResult;
+import android.hardware.camera2.TotalCaptureResult;
+import android.view.Surface;
+
+import java.util.concurrent.Semaphore;
+
+/** Camera2 callbacks which release specific semaphores on each event. */
+final class SemaphoreReleasingCamera2Callbacks {
+
+ private SemaphoreReleasingCamera2Callbacks() {
+ }
+
+ /** A device state callback which releases a different semaphore for each method. */
+ static final class DeviceStateCallback extends CameraDevice.StateCallback {
+ private static final String TAG = DeviceStateCallback.class.getSimpleName();
+
+ private final Semaphore onOpenedSemaphore = new Semaphore(0);
+ private final Semaphore onClosedSemaphore = new Semaphore(0);
+ private final Semaphore onDisconnectedSemaphore = new Semaphore(0);
+ private final Semaphore onErrorSemaphore = new Semaphore(0);
+
+ @Override
+ public void onOpened(CameraDevice cameraDevice) {
+ onOpenedSemaphore.release();
+ }
+
+ @Override
+ public void onClosed(CameraDevice cameraDevice) {
+ onClosedSemaphore.release();
+ }
+
+ @Override
+ public void onDisconnected(CameraDevice cameraDevice) {
+ onDisconnectedSemaphore.release();
+ }
+
+ @Override
+ public void onError(CameraDevice cameraDevice, int error) {
+ onErrorSemaphore.release();
+ }
+
+ void waitForOnOpened(int count) throws InterruptedException {
+ onOpenedSemaphore.acquire(count);
+ }
+
+ void waitForOnClosed(int count) throws InterruptedException {
+ onClosedSemaphore.acquire(count);
+ }
+
+ void waitForOnDisconnected(int count) throws InterruptedException {
+ onDisconnectedSemaphore.acquire(count);
+ }
+
+ void waitForOnError(int count) throws InterruptedException {
+ onErrorSemaphore.acquire(count);
+ }
+ }
+
+ /** A session state callback which releases a different semaphore for each method. */
+ static final class SessionStateCallback extends CameraCaptureSession.StateCallback {
+ private static final String TAG = SessionStateCallback.class.getSimpleName();
+
+ private final Semaphore onConfiguredSemaphore = new Semaphore(0);
+ private final Semaphore onActiveSemaphore = new Semaphore(0);
+ private final Semaphore onClosedSemaphore = new Semaphore(0);
+ private final Semaphore onReadySemaphore = new Semaphore(0);
+ private final Semaphore onCaptureQueueEmptySemaphore = new Semaphore(0);
+ private final Semaphore onSurfacePreparedSemaphore = new Semaphore(0);
+ private final Semaphore onConfigureFailedSemaphore = new Semaphore(0);
+
+ @Override
+ public void onConfigured(CameraCaptureSession session) {
+ onConfiguredSemaphore.release();
+ }
+
+ @Override
+ public void onActive(CameraCaptureSession session) {
+ onActiveSemaphore.release();
+ }
+
+ @Override
+ public void onClosed(CameraCaptureSession session) {
+ onClosedSemaphore.release();
+ }
+
+ @Override
+ public void onReady(CameraCaptureSession session) {
+ onReadySemaphore.release();
+ }
+
+ @Override
+ public void onCaptureQueueEmpty(CameraCaptureSession session) {
+ onCaptureQueueEmptySemaphore.release();
+ }
+
+ @Override
+ public void onSurfacePrepared(CameraCaptureSession session, Surface surface) {
+ onSurfacePreparedSemaphore.release();
+ }
+
+ @Override
+ public void onConfigureFailed(CameraCaptureSession session) {
+ onConfigureFailedSemaphore.release();
+ }
+
+ void waitForOnConfigured(int count) throws InterruptedException {
+ onConfiguredSemaphore.acquire(count);
+ }
+
+ void waitForOnActive(int count) throws InterruptedException {
+ onActiveSemaphore.acquire(count);
+ }
+
+ void waitForOnClosed(int count) throws InterruptedException {
+ onClosedSemaphore.acquire(count);
+ }
+
+ void waitForOnReady(int count) throws InterruptedException {
+ onReadySemaphore.acquire(count);
+ }
+
+ void waitForOnCaptureQueueEmpty(int count) throws InterruptedException {
+ onCaptureQueueEmptySemaphore.acquire(count);
+ }
+
+ void waitForOnSurfacePrepared(int count) throws InterruptedException {
+ onSurfacePreparedSemaphore.acquire(count);
+ }
+
+ void waitForOnConfigureFailed(int count) throws InterruptedException {
+ onConfigureFailedSemaphore.acquire(count);
+ }
+ }
+
+ /** A session capture callback which releases a different semaphore for each method. */
+ static final class SessionCaptureCallback extends CameraCaptureSession.CaptureCallback {
+ private static final String TAG = SessionCaptureCallback.class.getSimpleName();
+
+ private final Semaphore onCaptureBufferLostSemaphore = new Semaphore(0);
+ private final Semaphore onCaptureCompletedSemaphore = new Semaphore(0);
+ private final Semaphore onCaptureFailedSemaphore = new Semaphore(0);
+ private final Semaphore onCaptureProgressedSemaphore = new Semaphore(0);
+ private final Semaphore onCaptureSequenceAbortedSemaphore = new Semaphore(0);
+ private final Semaphore onCaptureSequenceCompletedSemaphore = new Semaphore(0);
+ private final Semaphore onCaptureStartedSemaphore = new Semaphore(0);
+
+ @Override
+ public void onCaptureBufferLost(
+ CameraCaptureSession session, CaptureRequest request, Surface surface, long frame) {
+ onCaptureBufferLostSemaphore.release();
+ }
+
+ @Override
+ public void onCaptureCompleted(
+ CameraCaptureSession session, CaptureRequest request, TotalCaptureResult result) {
+ onCaptureCompletedSemaphore.release();
+ }
+
+ @Override
+ public void onCaptureFailed(
+ CameraCaptureSession session, CaptureRequest request, CaptureFailure failure) {
+ onCaptureFailedSemaphore.release();
+ }
+
+ @Override
+ public void onCaptureProgressed(
+ CameraCaptureSession session, CaptureRequest request, CaptureResult partialResult) {
+ onCaptureProgressedSemaphore.release();
+ }
+
+ @Override
+ public void onCaptureSequenceAborted(CameraCaptureSession session, int sequenceId) {
+ onCaptureSequenceAbortedSemaphore.release();
+ }
+
+ @Override
+ public void onCaptureSequenceCompleted(
+ CameraCaptureSession session, int sequenceId, long frame) {
+ onCaptureSequenceCompletedSemaphore.release();
+ }
+
+ @Override
+ public void onCaptureStarted(
+ CameraCaptureSession session, CaptureRequest request, long timestamp, long frame) {
+ onCaptureStartedSemaphore.release();
+ }
+
+ void waitForOnCaptureBufferLost(int count) throws InterruptedException {
+ onCaptureBufferLostSemaphore.acquire(count);
+ }
+
+ void waitForOnCaptureCompleted(int count) throws InterruptedException {
+ onCaptureCompletedSemaphore.acquire(count);
+ }
+
+ void waitForOnCaptureFailed(int count) throws InterruptedException {
+ onCaptureFailedSemaphore.acquire(count);
+ }
+
+ void waitForOnCaptureProgressed(int count) throws InterruptedException {
+ onCaptureProgressedSemaphore.acquire(count);
+ }
+
+ void waitForOnCaptureSequenceAborted(int count) throws InterruptedException {
+ onCaptureSequenceAbortedSemaphore.acquire(count);
+ }
+
+ void waitForOnCaptureSequenceCompleted(int count) throws InterruptedException {
+ onCaptureSequenceCompletedSemaphore.acquire(count);
+ }
+
+ void waitForOnCaptureStarted(int count) throws InterruptedException {
+ onCaptureStartedSemaphore.acquire(count);
+ }
+ }
+}
diff --git a/camera/camera2/src/androidTest/java/androidx/camera/camera2/UseCaseSurfaceOccupancyManagerAndroidTest.java b/camera/camera2/src/androidTest/java/androidx/camera/camera2/UseCaseSurfaceOccupancyManagerAndroidTest.java
new file mode 100644
index 0000000..0ec2537
--- /dev/null
+++ b/camera/camera2/src/androidTest/java/androidx/camera/camera2/UseCaseSurfaceOccupancyManagerAndroidTest.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright (C) 2019 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.camera2;
+
+import static org.junit.Assert.assertThrows;
+
+import android.content.Context;
+
+import androidx.camera.core.AppConfiguration;
+import androidx.camera.core.CameraX;
+import androidx.camera.core.ImageCaptureUseCase;
+import androidx.camera.core.ImageCaptureUseCaseConfiguration;
+import androidx.camera.core.VideoCaptureUseCase;
+import androidx.camera.core.VideoCaptureUseCaseConfiguration;
+import androidx.test.core.app.ApplicationProvider;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.util.Arrays;
+
+/** JUnit test cases for UseCaseSurfaceOccupancyManager class. */
+@RunWith(JUnit4.class)
+public final class UseCaseSurfaceOccupancyManagerAndroidTest {
+
+ @Before
+ public void setUp() {
+ Context context = ApplicationProvider.getApplicationContext();
+ AppConfiguration appConfig = Camera2AppConfiguration.create(context);
+ CameraX.init(context, appConfig);
+ }
+
+ @Test
+ public void failedWhenBindTooManyImageCaptureUseCase() {
+ ImageCaptureUseCaseConfiguration configuration =
+ new ImageCaptureUseCaseConfiguration.Builder().build();
+ ImageCaptureUseCase useCase1 = new ImageCaptureUseCase(configuration);
+ ImageCaptureUseCase useCase2 = new ImageCaptureUseCase(configuration);
+
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> {
+ UseCaseSurfaceOccupancyManager.checkUseCaseLimitNotExceeded(
+ Arrays.asList(useCase1), Arrays.asList(useCase2));
+ });
+ }
+
+ @Test
+ public void failedWhenBindTooManyVideoCaptureUseCase() {
+ VideoCaptureUseCaseConfiguration configuration =
+ new VideoCaptureUseCaseConfiguration.Builder().build();
+ VideoCaptureUseCase useCase1 = new VideoCaptureUseCase(configuration);
+ VideoCaptureUseCase useCase2 = new VideoCaptureUseCase(configuration);
+
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> {
+ UseCaseSurfaceOccupancyManager.checkUseCaseLimitNotExceeded(
+ Arrays.asList(useCase1), Arrays.asList(useCase2));
+ });
+ }
+
+ @Test
+ public void passWhenNotBindTooManyImageVideoCaptureUseCase() {
+ ImageCaptureUseCaseConfiguration imageCaptureConfiguration =
+ new ImageCaptureUseCaseConfiguration.Builder().build();
+ ImageCaptureUseCase imageCaptureUseCase =
+ new ImageCaptureUseCase(imageCaptureConfiguration);
+
+ VideoCaptureUseCaseConfiguration videoCaptureConfiguration =
+ new VideoCaptureUseCaseConfiguration.Builder().build();
+ VideoCaptureUseCase videoCaptureUseCase =
+ new VideoCaptureUseCase(videoCaptureConfiguration);
+
+ UseCaseSurfaceOccupancyManager.checkUseCaseLimitNotExceeded(
+ Arrays.asList(imageCaptureUseCase), Arrays.asList(videoCaptureUseCase));
+ }
+}
diff --git a/camera/camera2/src/androidTest/java/androidx/camera/camera2/VideoCaptureUseCaseAndroidTest.java b/camera/camera2/src/androidTest/java/androidx/camera/camera2/VideoCaptureUseCaseAndroidTest.java
new file mode 100644
index 0000000..440ea21
--- /dev/null
+++ b/camera/camera2/src/androidTest/java/androidx/camera/camera2/VideoCaptureUseCaseAndroidTest.java
@@ -0,0 +1,155 @@
+/*
+ * Copyright (C) 2019 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.camera2;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import android.content.Context;
+import android.util.Size;
+
+import androidx.camera.core.AppConfiguration;
+import androidx.camera.core.BaseUseCase;
+import androidx.camera.core.BaseUseCase.StateChangeListener;
+import androidx.camera.core.CameraFactory;
+import androidx.camera.core.CameraX;
+import androidx.camera.core.CameraX.LensFacing;
+import androidx.camera.core.VideoCaptureUseCase;
+import androidx.camera.core.VideoCaptureUseCase.OnVideoSavedListener;
+import androidx.camera.core.VideoCaptureUseCaseConfiguration;
+import androidx.test.InstrumentationRegistry;
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mockito;
+
+import java.io.File;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Minimal unit test for the VideoCaptureUseCase because the {@link android.media.MediaRecorder}
+ * class requires a valid preview surface in order to correctly function.
+ *
+ * <p>TODO(b/112325215): The VideoCaptureUseCase will be more thoroughly tested via integration
+ * tests
+ */
+@RunWith(AndroidJUnit4.class)
+public final class VideoCaptureUseCaseAndroidTest {
+ private static final Size DEFAULT_RESOLUTION = new Size(1920, 1080);
+
+ private final Context context = InstrumentationRegistry.getTargetContext();
+ private final StateChangeListener listener = Mockito.mock(StateChangeListener.class);
+ private final ArgumentCaptor<BaseUseCase> baseUseCaseCaptor =
+ ArgumentCaptor.forClass(BaseUseCase.class);
+ private final OnVideoSavedListener mockVideoSavedListener =
+ Mockito.mock(OnVideoSavedListener.class);
+ private VideoCaptureUseCaseConfiguration defaultConfiguration;
+ private String cameraId;
+
+ @Before
+ public void setUp() {
+ defaultConfiguration = VideoCaptureUseCase.DEFAULT_CONFIG.getConfiguration();
+ Context context = ApplicationProvider.getApplicationContext();
+ AppConfiguration appConfiguration = Camera2AppConfiguration.create(context);
+ CameraFactory cameraFactory = appConfiguration.getCameraFactory(/*valueIfMissing=*/ null);
+ try {
+ cameraId = cameraFactory.cameraIdForLensFacing(LensFacing.BACK);
+ } catch (Exception e) {
+ throw new IllegalArgumentException(
+ "Unable to attach to camera with LensFacing " + LensFacing.BACK, e);
+ }
+ CameraX.init(context, appConfiguration);
+ }
+
+ @Test
+ public void useCaseBecomesActive_whenStartingVideoRecording() {
+ VideoCaptureUseCase useCase = new VideoCaptureUseCase(defaultConfiguration);
+ Map<String, Size> suggestedResolutionMap = new HashMap<>();
+ suggestedResolutionMap.put(cameraId, DEFAULT_RESOLUTION);
+ useCase.updateSuggestedResolution(suggestedResolutionMap);
+ useCase.addStateChangeListener(listener);
+
+ useCase.startRecording(
+ new File(
+ context.getFilesDir()
+ + "/useCaseBecomesActive_whenStartingVideoRecording.mp4"),
+ mockVideoSavedListener);
+
+ verify(listener, times(1)).onUseCaseActive(baseUseCaseCaptor.capture());
+ assertThat(baseUseCaseCaptor.getValue()).isSameAs(useCase);
+ }
+
+ @Test
+ public void useCaseBecomesInactive_whenStoppingVideoRecording() {
+ VideoCaptureUseCase useCase = new VideoCaptureUseCase(defaultConfiguration);
+ Map<String, Size> suggestedResolutionMap = new HashMap<>();
+ suggestedResolutionMap.put(cameraId, DEFAULT_RESOLUTION);
+ useCase.updateSuggestedResolution(suggestedResolutionMap);
+ useCase.addStateChangeListener(listener);
+
+ useCase.startRecording(
+ new File(
+ context.getFilesDir()
+ + "/useCaseBecomesInactive_whenStoppingVideoRecording.mp4"),
+ mockVideoSavedListener);
+
+ try {
+ useCase.stopRecording();
+ } catch (RuntimeException e) {
+ // Need to catch the RuntimeException because for certain devices MediaRecorder
+ // contained
+ // within the VideoCaptureUseCase requires a valid preview in order to run. This unit
+ // test is
+ // just to exercise the inactive state change that the use case should trigger
+ // TODO(b/112324530): The try-catch should be removed after the bug fix
+ }
+
+ verify(listener, times(1)).onUseCaseInactive(baseUseCaseCaptor.capture());
+ assertThat(baseUseCaseCaptor.getValue()).isSameAs(useCase);
+ }
+
+ @Test
+ public void updateSessionConfigurationWithSuggestedResolution() {
+ VideoCaptureUseCase useCase = new VideoCaptureUseCase(defaultConfiguration);
+ // Create video encoder with default 1920x1080 resolution
+ Map<String, Size> suggestedResolutionMap = new HashMap<>();
+ suggestedResolutionMap.put(cameraId, DEFAULT_RESOLUTION);
+ useCase.updateSuggestedResolution(suggestedResolutionMap);
+ useCase.addStateChangeListener(listener);
+
+ // Recreate video encoder with new 640x480 resolution
+ suggestedResolutionMap.put(cameraId, new Size(640, 480));
+ useCase.updateSuggestedResolution(suggestedResolutionMap);
+
+ // Check it could be started to record and become active
+ useCase.startRecording(
+ new File(
+ context.getFilesDir()
+ + "/useCaseBecomesInactive_whenStoppingVideoRecording.mp4"),
+ mockVideoSavedListener);
+
+ verify(listener, times(1)).onUseCaseActive(baseUseCaseCaptor.capture());
+ assertThat(baseUseCaseCaptor.getValue()).isSameAs(useCase);
+ }
+}
diff --git a/camera/camera2/src/androidTest/java/androidx/camera/camera2/ViewFinderUseCaseAndroidTest.java b/camera/camera2/src/androidTest/java/androidx/camera/camera2/ViewFinderUseCaseAndroidTest.java
new file mode 100644
index 0000000..dbf9d6c
--- /dev/null
+++ b/camera/camera2/src/androidTest/java/androidx/camera/camera2/ViewFinderUseCaseAndroidTest.java
@@ -0,0 +1,473 @@
+/*
+ * Copyright (C) 2019 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.camera2;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import android.content.Context;
+import android.graphics.Rect;
+import android.graphics.SurfaceTexture;
+import android.hardware.camera2.CaptureRequest;
+import android.hardware.camera2.params.MeteringRectangle;
+import android.os.Build;
+import android.os.Handler;
+import android.os.Looper;
+import android.util.Size;
+import android.view.Surface;
+
+import androidx.camera.core.AppConfiguration;
+import androidx.camera.core.CameraControl;
+import androidx.camera.core.CameraFactory;
+import androidx.camera.core.CameraX;
+import androidx.camera.core.CameraX.LensFacing;
+import androidx.camera.core.CaptureRequestConfiguration;
+import androidx.camera.core.DeferrableSurfaces;
+import androidx.camera.core.ViewFinderUseCase;
+import androidx.camera.core.ViewFinderUseCase.OnViewFinderOutputUpdateListener;
+import androidx.camera.core.ViewFinderUseCase.ViewFinderOutput;
+import androidx.camera.core.ViewFinderUseCaseConfiguration;
+import androidx.test.annotation.UiThreadTest;
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.concurrent.Callable;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.FutureTask;
+import java.util.concurrent.Semaphore;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicReference;
+
+@RunWith(AndroidJUnit4.class)
+public final class ViewFinderUseCaseAndroidTest {
+ private static final Size DEFAULT_RESOLUTION = new Size(1920, 1080);
+ private static final Size SECONDARY_RESOLUTION = new Size(1280, 720);
+
+ private ViewFinderUseCaseConfiguration defaultConfiguration;
+ @Mock
+ private OnViewFinderOutputUpdateListener mockListener;
+ private String cameraId;
+
+ @Before
+ public void setUp() {
+ // Instantiates OnViewFinderOutputUpdateListener before each test run.
+ mockListener = Mockito.mock(OnViewFinderOutputUpdateListener.class);
+ Context context = ApplicationProvider.getApplicationContext();
+ AppConfiguration appConfig = Camera2AppConfiguration.create(context);
+ CameraFactory cameraFactory = appConfig.getCameraFactory(/*valueIfMissing=*/ null);
+ try {
+ cameraId = cameraFactory.cameraIdForLensFacing(LensFacing.BACK);
+ } catch (Exception e) {
+ throw new IllegalArgumentException(
+ "Unable to attach to camera with LensFacing " + LensFacing.BACK, e);
+ }
+ CameraX.init(context, appConfig);
+
+ // init CameraX before creating ViewFinderUseCase to get preview size with CameraX's context
+ defaultConfiguration = ViewFinderUseCase.DEFAULT_CONFIG.getConfiguration();
+ }
+
+ @Test
+ @UiThreadTest
+ public void useCaseIsConstructedWithDefaultConfiguration() {
+ ViewFinderUseCase useCase = new ViewFinderUseCase(defaultConfiguration);
+ useCase.updateSuggestedResolution(Collections.singletonMap(cameraId, DEFAULT_RESOLUTION));
+
+ List<Surface> surfaces =
+ DeferrableSurfaces.surfaceList(
+ useCase.getSessionConfiguration(cameraId).getSurfaces());
+
+ assertThat(surfaces.size()).isEqualTo(1);
+ assertThat(surfaces.get(0).isValid()).isTrue();
+ }
+
+ @Test
+ @UiThreadTest
+ public void useCaseIsConstructedWithCustomConfiguration() {
+ ViewFinderUseCaseConfiguration configuration =
+ new ViewFinderUseCaseConfiguration.Builder().setLensFacing(LensFacing.BACK).build();
+ ViewFinderUseCase useCase = new ViewFinderUseCase(configuration);
+ useCase.updateSuggestedResolution(Collections.singletonMap(cameraId, DEFAULT_RESOLUTION));
+
+ List<Surface> surfaces =
+ DeferrableSurfaces.surfaceList(
+ useCase.getSessionConfiguration(cameraId).getSurfaces());
+
+ assertThat(surfaces.size()).isEqualTo(1);
+ assertThat(surfaces.get(0).isValid()).isTrue();
+ }
+
+ @Test
+ @UiThreadTest
+ public void focusRegionCanBeSet() {
+ ViewFinderUseCase useCase = new ViewFinderUseCase(defaultConfiguration);
+ useCase.updateSuggestedResolution(Collections.singletonMap(cameraId, DEFAULT_RESOLUTION));
+
+ CameraControl cameraControl = getFakeCameraControl();
+ useCase.attachCameraControl(cameraId, cameraControl);
+
+ Rect rect = new Rect(/*left=*/ 200, /*top=*/ 200, /*right=*/ 800, /*bottom=*/ 800);
+ useCase.focus(rect, rect);
+
+ Camera2Configuration configuration =
+ new Camera2Configuration(cameraControl.getSingleRequestImplOptions());
+ MeteringRectangle[] aeMeteringRects =
+ configuration.getCaptureRequestOption(CaptureRequest.CONTROL_AE_REGIONS, null);
+ MeteringRectangle[] afMeteringRects =
+ configuration.getCaptureRequestOption(CaptureRequest.CONTROL_AF_REGIONS, null);
+ MeteringRectangle[] awbMeteringRects =
+ configuration.getCaptureRequestOption(CaptureRequest.CONTROL_AWB_REGIONS, null);
+ assertThat(aeMeteringRects).hasLength(1);
+ assertThat(afMeteringRects).hasLength(1);
+ assertThat(awbMeteringRects).hasLength(1);
+
+ assertThat(aeMeteringRects[0].getRect()).isEqualTo(rect);
+ assertThat(afMeteringRects[0].getRect()).isEqualTo(rect);
+ assertThat(awbMeteringRects[0].getRect()).isEqualTo(rect);
+ }
+
+ @Test
+ @UiThreadTest
+ public void zoomRegionCanBeSet() {
+ ViewFinderUseCase useCase = new ViewFinderUseCase(defaultConfiguration);
+ useCase.updateSuggestedResolution(Collections.singletonMap(cameraId, DEFAULT_RESOLUTION));
+
+ CameraControl cameraControl = getFakeCameraControl();
+ useCase.attachCameraControl(cameraId, cameraControl);
+
+ Rect rect = new Rect(/*left=*/ 200, /*top=*/ 200, /*right=*/ 800, /*bottom=*/ 800);
+ useCase.zoom(rect);
+
+ Camera2Configuration configuration =
+ new Camera2Configuration(cameraControl.getSingleRequestImplOptions());
+ Rect cropRect =
+ configuration.getCaptureRequestOption(CaptureRequest.SCALER_CROP_REGION, null);
+ assertThat(cropRect).isEqualTo(rect);
+ }
+
+ @Test
+ @UiThreadTest
+ public void torchModeCanBeSet() {
+ ViewFinderUseCase useCase = new ViewFinderUseCase(defaultConfiguration);
+ CameraControl cameraControl = getFakeCameraControl();
+ useCase.attachCameraControl(cameraId, cameraControl);
+
+ useCase.enableTorch(true);
+
+ assertThat(useCase.isTorchOn()).isTrue();
+ }
+
+ @Test(timeout = 5000)
+ @UiThreadTest
+ public void surfaceTextureIsNotReleased()
+ throws InterruptedException, ExecutionException, TimeoutException {
+ // This test only target SDK >= 26
+ if (Build.VERSION.SDK_INT < 26) {
+ return;
+ }
+ ViewFinderUseCase useCase = new ViewFinderUseCase(defaultConfiguration);
+
+ SurfaceTextureCallable surfaceTextureCallable0 = new SurfaceTextureCallable();
+ FutureTask<SurfaceTexture> future0 = new FutureTask<>(surfaceTextureCallable0);
+ useCase.setOnViewFinderOutputUpdateListener(
+ viewFinderOutput -> {
+ surfaceTextureCallable0.setSurfaceTexture(viewFinderOutput.getSurfaceTexture());
+ future0.run();
+ });
+ useCase.updateSuggestedResolution(Collections.singletonMap(cameraId, DEFAULT_RESOLUTION));
+
+ SurfaceTexture surfaceTexture0 = future0.get(1, TimeUnit.SECONDS);
+ surfaceTexture0.release();
+
+ SurfaceTextureCallable surfaceTextureCallable1 = new SurfaceTextureCallable();
+ FutureTask<SurfaceTexture> future1 = new FutureTask<>(surfaceTextureCallable1);
+ useCase.setOnViewFinderOutputUpdateListener(
+ viewFinderOutput -> {
+ surfaceTextureCallable1.setSurfaceTexture(viewFinderOutput.getSurfaceTexture());
+ future1.run();
+ });
+ useCase.updateSuggestedResolution(Collections.singletonMap(cameraId, DEFAULT_RESOLUTION));
+ SurfaceTexture surfaceTexture1 = future1.get(1, TimeUnit.SECONDS);
+
+ assertThat(surfaceTexture1.isReleased()).isFalse();
+ }
+
+ @Test(timeout = 5000)
+ @UiThreadTest
+ public void listenedSurfaceTextureIsNotReleased_whenCleared()
+ throws InterruptedException, ExecutionException, TimeoutException {
+ // This test only target SDK >= 26
+ if (Build.VERSION.SDK_INT <= 26) {
+ return;
+ }
+ ViewFinderUseCase useCase = new ViewFinderUseCase(defaultConfiguration);
+
+ SurfaceTextureCallable surfaceTextureCallable = new SurfaceTextureCallable();
+ FutureTask<SurfaceTexture> future = new FutureTask<>(surfaceTextureCallable);
+
+ useCase.setOnViewFinderOutputUpdateListener(
+ viewFinderOutput -> {
+ surfaceTextureCallable.setSurfaceTexture(viewFinderOutput.getSurfaceTexture());
+ future.run();
+ });
+
+ useCase.updateSuggestedResolution(Collections.singletonMap(cameraId, DEFAULT_RESOLUTION));
+ SurfaceTexture surfaceTexture = future.get(1, TimeUnit.SECONDS);
+
+ useCase.clear();
+
+ assertThat(surfaceTexture.isReleased()).isFalse();
+ }
+
+ @Test(timeout = 5000)
+ @UiThreadTest
+ public void surfaceTexture_isListenedOnlyOnce()
+ throws InterruptedException, ExecutionException, TimeoutException {
+
+ ViewFinderUseCase useCase = new ViewFinderUseCase(defaultConfiguration);
+
+ SurfaceTextureCallable surfaceTextureCallable0 = new SurfaceTextureCallable();
+ FutureTask<SurfaceTexture> future0 = new FutureTask<>(surfaceTextureCallable0);
+ useCase.setOnViewFinderOutputUpdateListener(
+ viewFinderOutput -> {
+ surfaceTextureCallable0.setSurfaceTexture(viewFinderOutput.getSurfaceTexture());
+ future0.run();
+ });
+
+ useCase.updateSuggestedResolution(Collections.singletonMap(cameraId, DEFAULT_RESOLUTION));
+ SurfaceTexture surfaceTexture0 = future0.get();
+
+ SurfaceTextureCallable surfaceTextureCallable1 = new SurfaceTextureCallable();
+ FutureTask<SurfaceTexture> future1 = new FutureTask<>(surfaceTextureCallable1);
+ useCase.setOnViewFinderOutputUpdateListener(
+ viewFinderOutput -> {
+ surfaceTextureCallable1.setSurfaceTexture(viewFinderOutput.getSurfaceTexture());
+ future1.run();
+ });
+
+ SurfaceTexture surfaceTexture1 = future1.get(1, TimeUnit.SECONDS);
+
+ assertThat(surfaceTexture0).isNotSameAs(surfaceTexture1);
+ }
+
+ @Test
+ @UiThreadTest
+ public void updateSessionConfigurationWithSuggestedResolution() {
+ ViewFinderUseCaseConfiguration configuration =
+ new ViewFinderUseCaseConfiguration.Builder().setLensFacing(LensFacing.BACK).build();
+ ViewFinderUseCase useCase = new ViewFinderUseCase(configuration);
+
+ final Size[] sizes = {new Size(1920, 1080), new Size(640, 480)};
+
+ for (Size size : sizes) {
+ useCase.updateSuggestedResolution(Collections.singletonMap(cameraId, size));
+
+ List<Surface> surfaces =
+ DeferrableSurfaces.surfaceList(
+ useCase.getSessionConfiguration(cameraId).getSurfaces());
+
+ assertWithMessage("Failed at Size: " + size).that(surfaces).hasSize(1);
+ assertWithMessage("Failed at Size: " + size).that(surfaces.get(0).isValid()).isTrue();
+ }
+ }
+
+ @Test
+ @UiThreadTest
+ public void viewFinderOutputListenerCanBeSetAndRetrieved() {
+ ViewFinderUseCase useCase = new ViewFinderUseCase(defaultConfiguration);
+ useCase.updateSuggestedResolution(Collections.singletonMap(cameraId, DEFAULT_RESOLUTION));
+ OnViewFinderOutputUpdateListener viewFinderOutputListener =
+ useCase.getOnViewFinderOutputUpdateListener();
+ useCase.setOnViewFinderOutputUpdateListener(mockListener);
+
+ OnViewFinderOutputUpdateListener retrievedViewFinderOutputListener =
+ useCase.getOnViewFinderOutputUpdateListener();
+
+ assertThat(viewFinderOutputListener).isNull();
+ assertThat(retrievedViewFinderOutputListener).isSameAs(mockListener);
+ }
+
+ @Test
+ @UiThreadTest
+ public void clear_removeViewFinderOutputListener() {
+ ViewFinderUseCase useCase = new ViewFinderUseCase(defaultConfiguration);
+ useCase.updateSuggestedResolution(Collections.singletonMap(cameraId, DEFAULT_RESOLUTION));
+
+ useCase.setOnViewFinderOutputUpdateListener(mockListener);
+ useCase.clear();
+
+ assertThat(useCase.getOnViewFinderOutputUpdateListener()).isNull();
+ }
+
+ @Test
+ @UiThreadTest
+ public void viewFinderOutput_isResetOnUpdatedResolution() {
+ ViewFinderUseCase useCase = new ViewFinderUseCase(defaultConfiguration);
+ useCase.updateSuggestedResolution(Collections.singletonMap(cameraId, DEFAULT_RESOLUTION));
+
+ AtomicInteger calledCount = new AtomicInteger(0);
+ useCase.setOnViewFinderOutputUpdateListener(
+ viewFinderOutput -> {
+ calledCount.incrementAndGet();
+ });
+
+ int initialCount = calledCount.get();
+
+ useCase.updateSuggestedResolution(Collections.singletonMap(cameraId, SECONDARY_RESOLUTION));
+
+ int countAfterUpdate = calledCount.get();
+
+ assertThat(initialCount).isEqualTo(1);
+ assertThat(countAfterUpdate).isEqualTo(2);
+ }
+
+ @Test
+ @UiThreadTest
+ public void viewFinderOutput_updatesWithTargetRotation() {
+ ViewFinderUseCase useCase = new ViewFinderUseCase(defaultConfiguration);
+ useCase.setTargetRotation(Surface.ROTATION_0);
+ useCase.updateSuggestedResolution(Collections.singletonMap(cameraId, DEFAULT_RESOLUTION));
+
+ AtomicReference<ViewFinderOutput> latestViewFinderOutput = new AtomicReference<>();
+ useCase.setOnViewFinderOutputUpdateListener(
+ viewFinderOutput -> {
+ latestViewFinderOutput.set(viewFinderOutput);
+ });
+
+ ViewFinderOutput initialOutput = latestViewFinderOutput.get();
+
+ useCase.setTargetRotation(Surface.ROTATION_90);
+
+ assertThat(initialOutput).isNotNull();
+ assertThat(initialOutput.getSurfaceTexture())
+ .isEqualTo(latestViewFinderOutput.get().getSurfaceTexture());
+ assertThat(initialOutput.getRotationDegrees())
+ .isNotEqualTo(latestViewFinderOutput.get().getRotationDegrees());
+ }
+
+ // Must not run on main thread
+ @Test(timeout = 5000)
+ public void viewFinderOutput_isResetByReleasedSurface()
+ throws InterruptedException, ExecutionException {
+ ViewFinderUseCase useCase = new ViewFinderUseCase(defaultConfiguration);
+ Handler mainHandler = new Handler(Looper.getMainLooper());
+ Semaphore semaphore = new Semaphore(0);
+
+ mainHandler.post(
+ () -> {
+ useCase.updateSuggestedResolution(
+ Collections.singletonMap(cameraId, DEFAULT_RESOLUTION));
+
+ useCase.setOnViewFinderOutputUpdateListener(
+ viewFinderOutput -> {
+ // Release the surface texture
+ viewFinderOutput.getSurfaceTexture().release();
+
+ semaphore.release();
+ });
+ });
+
+ // Wait for the surface texture to be released
+ semaphore.acquire();
+
+ // Cause the surface to reset
+ useCase.getSessionConfiguration(cameraId).getSurfaces().get(0).getSurface().get();
+
+ // Wait for the surface to reset
+ semaphore.acquire();
+ }
+
+ @Test(timeout = 5000)
+ @UiThreadTest
+ public void outputIsPublished_whenListenerIsSetBefore()
+ throws InterruptedException, ExecutionException {
+
+ ViewFinderUseCase useCase = new ViewFinderUseCase(defaultConfiguration);
+
+ SurfaceTextureCallable surfaceTextureCallable0 = new SurfaceTextureCallable();
+ FutureTask<SurfaceTexture> future0 = new FutureTask<>(surfaceTextureCallable0);
+ useCase.setOnViewFinderOutputUpdateListener(
+ viewFinderOutput -> {
+ surfaceTextureCallable0.setSurfaceTexture(viewFinderOutput.getSurfaceTexture());
+ future0.run();
+ });
+
+ useCase.updateSuggestedResolution(Collections.singletonMap(cameraId, DEFAULT_RESOLUTION));
+ SurfaceTexture surfaceTexture0 = future0.get();
+
+ assertThat(surfaceTexture0).isNotNull();
+ }
+
+ @Test(timeout = 5000)
+ @UiThreadTest
+ public void outputIsPublished_whenListenerIsSetAfter()
+ throws InterruptedException, ExecutionException {
+
+ ViewFinderUseCase useCase = new ViewFinderUseCase(defaultConfiguration);
+
+ SurfaceTextureCallable surfaceTextureCallable0 = new SurfaceTextureCallable();
+ FutureTask<SurfaceTexture> future0 = new FutureTask<>(surfaceTextureCallable0);
+ useCase.updateSuggestedResolution(Collections.singletonMap(cameraId, DEFAULT_RESOLUTION));
+
+ useCase.setOnViewFinderOutputUpdateListener(
+ viewFinderOutput -> {
+ surfaceTextureCallable0.setSurfaceTexture(viewFinderOutput.getSurfaceTexture());
+ future0.run();
+ });
+ SurfaceTexture surfaceTexture0 = future0.get();
+
+ assertThat(surfaceTexture0).isNotNull();
+ }
+
+ private CameraControl getFakeCameraControl() {
+ return new Camera2CameraControl(
+ new Camera2RequestRunner() {
+ @Override
+ public void submitSingleRequest(
+ CaptureRequestConfiguration singleRequestConfig) {
+ }
+
+ @Override
+ public void updateRepeatingRequest() {
+ }
+ },
+ new Handler());
+ }
+
+ private static final class SurfaceTextureCallable implements Callable<SurfaceTexture> {
+ SurfaceTexture surfaceTexture;
+
+ void setSurfaceTexture(SurfaceTexture surfaceTexture) {
+ this.surfaceTexture = surfaceTexture;
+ }
+
+ @Override
+ public SurfaceTexture call() {
+ return surfaceTexture;
+ }
+ }
+}
diff --git a/camera/camera2/src/main/AndroidManifest.xml b/camera/camera2/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..9430688
--- /dev/null
+++ b/camera/camera2/src/main/AndroidManifest.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2019 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.
+-->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="androidx.camera.camera2">
+
+ <uses-sdk
+ android:minSdkVersion="21"
+ android:targetSdkVersion="27" />
+
+ <application>
+ <provider
+ android:name=".Camera2Initializer"
+ android:authorities="${applicationId}.camerax-init"
+ android:exported="false"
+ android:initOrder="100"
+ android:multiprocess="true" />
+ </application>
+
+</manifest>
diff --git a/camera/camera2/src/main/java/androidx/camera/camera2/CamcorderProfileHelper.java b/camera/camera2/src/main/java/androidx/camera/camera2/CamcorderProfileHelper.java
new file mode 100644
index 0000000..b6c7b37
--- /dev/null
+++ b/camera/camera2/src/main/java/androidx/camera/camera2/CamcorderProfileHelper.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 2019 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.camera2;
+
+/**
+ * This is helper class to use {@link android.media.CamcorderProfile} that may be mocked.
+ *
+ * @hide
+ */
+public interface CamcorderProfileHelper {
+
+ boolean hasProfile(int cameraId, int quality);
+}
diff --git a/camera/camera2/src/main/java/androidx/camera/camera2/Camera.java b/camera/camera2/src/main/java/androidx/camera/camera2/Camera.java
new file mode 100644
index 0000000..d0f848d
--- /dev/null
+++ b/camera/camera2/src/main/java/androidx/camera/camera2/Camera.java
@@ -0,0 +1,706 @@
+/*
+ * Copyright (C) 2019 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.camera2;
+
+import android.annotation.SuppressLint;
+import android.hardware.camera2.CameraAccessException;
+import android.hardware.camera2.CameraDevice;
+import android.hardware.camera2.CameraManager;
+import android.os.Handler;
+import android.os.Looper;
+import android.util.Log;
+
+import androidx.annotation.GuardedBy;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.annotation.RestrictTo.Scope;
+import androidx.camera.core.BaseCamera;
+import androidx.camera.core.BaseUseCase;
+import androidx.camera.core.CameraControl;
+import androidx.camera.core.CameraDeviceStateCallbacks;
+import androidx.camera.core.CameraInfo;
+import androidx.camera.core.CameraInfoUnavailableException;
+import androidx.camera.core.CameraX;
+import androidx.camera.core.CaptureRequestConfiguration;
+import androidx.camera.core.DeferrableSurface;
+import androidx.camera.core.SessionConfiguration;
+import androidx.camera.core.SessionConfiguration.ValidatingBuilder;
+import androidx.camera.core.UseCaseAttachState;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicReference;
+
+/**
+ * A camera which is controlled by the change of state in use cases.
+ *
+ * <p>The camera needs to be in an open state in order for use cases to control the camera. Whenever
+ * there is a non-zero number of use cases in the online state the camera will either have a capture
+ * session open or be in the process of opening up one. If the number of uses cases in the online
+ * state changes then the capture session will be reconfigured.
+ *
+ * <p>Capture requests will be issued only for use cases which are in both the online and active
+ * state.
+ */
+final class Camera implements BaseCamera, Camera2RequestRunner {
+ private static final String TAG = "Camera";
+
+ private final Object attachedUseCaseLock = new Object();
+
+ /** Map of the use cases to the information on their state. */
+ @GuardedBy("attachedUseCaseLock")
+ private final UseCaseAttachState useCaseAttachState;
+
+ /** The identifier for the {@link CameraDevice} */
+ private final String cameraId;
+
+ /** Handle to the camera service. */
+ private final CameraManager cameraManager;
+
+ private final Object cameraInfoLock = new Object();
+ /** The handler for camera callbacks and use case state management calls. */
+ private final Handler handler;
+ /**
+ * State variable for tracking state of the camera.
+ *
+ * <p>Is an atomic reference because it is initialized in the constructor which is not called on
+ * same thread as any of the other methods and callbacks.
+ */
+ final AtomicReference<State> state = new AtomicReference<>(State.UNINITIALIZED);
+ /** The camera control shared across all use cases bound to this Camera. */
+ private final CameraControl cameraControl;
+ private final StateCallback stateCallback = new StateCallback();
+ /** Information about the characteristics of this camera */
+ // Nullable because this is lazily instantiated
+ @GuardedBy("cameraInfoLock")
+ @Nullable
+ private CameraInfo cameraInfo;
+ /** The handle to the opened camera. */
+ @Nullable
+ CameraDevice cameraDevice;
+ /** The configured session which handles issuing capture requests. */
+ private CaptureSession captureSession = new CaptureSession(null);
+
+ /**
+ * Constructor for a camera.
+ *
+ * @param cameraManager the camera service used to retrieve a camera
+ * @param cameraId the name of the camera as defined by the camera service
+ * @param handler the handler for the thread on which all camera operations run
+ */
+ Camera(CameraManager cameraManager, String cameraId, Handler handler) {
+ this.cameraManager = cameraManager;
+ this.cameraId = cameraId;
+ this.handler = handler;
+ useCaseAttachState = new UseCaseAttachState(cameraId);
+ state.set(State.INITIALIZED);
+ cameraControl = new Camera2CameraControl(this, handler);
+ }
+
+ /**
+ * Open the camera asynchronously.
+ *
+ * <p>Once the camera has been opened use case state transitions can be used to control the
+ * camera pipeline.
+ */
+ @Override
+ public void open() {
+ if (Looper.myLooper() != handler.getLooper()) {
+ handler.post(() -> open());
+ return;
+ }
+
+ switch (state.get()) {
+ case INITIALIZED:
+ openCameraDevice();
+ break;
+ case CLOSING:
+ state.set(State.REOPENING);
+ break;
+ default:
+ Log.d(TAG, "open() ignored due to being in state: " + state.get());
+ }
+ }
+
+ /**
+ * Close the camera asynchronously.
+ *
+ * <p>Once the camera is closed the camera will no longer produce data. The camera must be
+ * reopened for it to produce data again.
+ */
+ @Override
+ public void close() {
+ if (Looper.myLooper() != handler.getLooper()) {
+ handler.post(() -> close());
+ return;
+ }
+
+ Log.d(TAG, "Closing camera: " + cameraId);
+ switch (state.get()) {
+ case OPENED:
+ state.set(State.CLOSING);
+ cameraDevice.close();
+ cameraDevice = null;
+ break;
+ case OPENING:
+ case REOPENING:
+ state.set(State.CLOSING);
+ break;
+ default:
+ Log.d(TAG, "close() ignored due to being in state: " + state.get());
+ }
+ }
+
+ /**
+ * Release the camera.
+ *
+ * <p>Once the camera is released it is permanently closed. A new instance must be created to
+ * access the camera.
+ */
+ @Override
+ public void release() {
+ if (Looper.myLooper() != handler.getLooper()) {
+ handler.post(() -> release());
+ return;
+ }
+
+ switch (state.get()) {
+ case INITIALIZED:
+ state.set(State.RELEASED);
+ break;
+ case OPENED:
+ state.set(State.RELEASING);
+ cameraDevice.close();
+ break;
+ case OPENING:
+ case CLOSING:
+ case REOPENING:
+ state.set(State.RELEASING);
+ break;
+ default:
+ Log.d(TAG, "release() ignored due to being in state: " + state.get());
+ }
+ }
+
+ /**
+ * Sets the use case in a state to issue capture requests.
+ *
+ * <p>The use case must also be online in order for it to issue capture requests.
+ */
+ @Override
+ public void onUseCaseActive(BaseUseCase useCase) {
+ if (Looper.myLooper() != handler.getLooper()) {
+ handler.post(() -> onUseCaseActive(useCase));
+ return;
+ }
+
+ Log.d(TAG, "Use case " + useCase + " ACTIVE for camera " + cameraId);
+
+ synchronized (attachedUseCaseLock) {
+ useCaseAttachState.setUseCaseActive(useCase);
+ }
+ updateCaptureSessionConfiguration();
+ }
+
+ /** Removes the use case from a state of issuing capture requests. */
+ @Override
+ public void onUseCaseInactive(BaseUseCase useCase) {
+ if (Looper.myLooper() != handler.getLooper()) {
+ handler.post(() -> onUseCaseInactive(useCase));
+ return;
+ }
+
+ Log.d(TAG, "Use case " + useCase + " INACTIVE for camera " + cameraId);
+ synchronized (attachedUseCaseLock) {
+ useCaseAttachState.setUseCaseInactive(useCase);
+ }
+
+ updateCaptureSessionConfiguration();
+ }
+
+ /** Updates the capture requests based on the latest settings. */
+ @Override
+ public void onUseCaseUpdated(BaseUseCase useCase) {
+ if (Looper.myLooper() != handler.getLooper()) {
+ handler.post(() -> onUseCaseUpdated(useCase));
+ return;
+ }
+
+ Log.d(TAG, "Use case " + useCase + " UPDATED for camera " + cameraId);
+ synchronized (attachedUseCaseLock) {
+ useCaseAttachState.updateUseCase(useCase);
+ }
+
+ updateCaptureSessionConfiguration();
+ }
+
+ @Override
+ public void onUseCaseReset(BaseUseCase useCase) {
+ if (Looper.myLooper() != handler.getLooper()) {
+ handler.post(() -> onUseCaseReset(useCase));
+ return;
+ }
+
+ Log.d(TAG, "Use case " + useCase + " RESET for camera " + cameraId);
+ synchronized (attachedUseCaseLock) {
+ useCaseAttachState.updateUseCase(useCase);
+ }
+
+ updateCaptureSessionConfiguration();
+ openCaptureSession();
+ }
+
+ @Override
+ public void onUseCaseSingleRequest(
+ BaseUseCase useCase, CaptureRequestConfiguration captureRequestConfiguration) {
+ submitSingleRequest(captureRequestConfiguration);
+ }
+
+ /**
+ * Sets the use case to be in the state where the capture session will be configured to handle
+ * capture requests from the use case.
+ */
+ @Override
+ public void addOnlineUseCase(Collection<BaseUseCase> useCases) {
+ if (useCases.isEmpty()) {
+ return;
+ }
+
+ if (Looper.myLooper() != handler.getLooper()) {
+ handler.post(() -> addOnlineUseCase(useCases));
+ return;
+ }
+
+ Log.d(TAG, "Use cases " + useCases + " ONLINE for camera " + cameraId);
+ synchronized (attachedUseCaseLock) {
+ for (BaseUseCase useCase : useCases) {
+ useCaseAttachState.setUseCaseOnline(useCase);
+ }
+ }
+
+ open();
+ updateCaptureSessionConfiguration();
+ openCaptureSession();
+ }
+
+ /**
+ * Removes the use case to be in the state where the capture session will be configured to
+ * handle capture requests from the use case.
+ */
+ @Override
+ public void removeOnlineUseCase(Collection<BaseUseCase> useCases) {
+ if (useCases.isEmpty()) {
+ return;
+ }
+
+ if (Looper.myLooper() != handler.getLooper()) {
+ handler.post(() -> removeOnlineUseCase(useCases));
+ return;
+ }
+
+ Log.d(TAG, "Use cases " + useCases + " OFFLINE for camera " + cameraId);
+ synchronized (attachedUseCaseLock) {
+ for (BaseUseCase useCase : useCases) {
+ useCaseAttachState.setUseCaseOffline(useCase);
+ }
+
+ if (useCaseAttachState.getOnlineUseCases().isEmpty()) {
+ resetCaptureSession();
+ close();
+ return;
+ }
+ }
+
+ updateCaptureSessionConfiguration();
+ }
+
+ /** Returns an interface to retrieve characteristics of the camera. */
+ @Override
+ public CameraInfo getCameraInfo() throws CameraInfoUnavailableException {
+ synchronized (cameraInfoLock) {
+ if (cameraInfo == null) {
+ // Lazily instantiate camera info
+ cameraInfo = new Camera2CameraInfo(cameraManager, cameraId);
+ }
+
+ return cameraInfo;
+ }
+ }
+
+ /** Opens the camera device */
+ // TODO(b/124268878): Handle SecurityException and require permission in manifest.
+ @SuppressLint("MissingPermission")
+ void openCameraDevice() {
+ state.set(State.OPENING);
+
+ Log.d(TAG, "Opening camera: " + cameraId);
+
+ try {
+ cameraManager.openCamera(cameraId, createDeviceStateCallback(), handler);
+ } catch (CameraAccessException e) {
+ Log.e(TAG, "Unable to open camera " + cameraId + " due to " + e.getMessage());
+ state.set(State.INITIALIZED);
+ }
+ }
+
+ /** Updates the capture request configuration for the current capture session. */
+ private void updateCaptureSessionConfiguration() {
+ ValidatingBuilder validatingBuilder;
+ synchronized (attachedUseCaseLock) {
+ validatingBuilder = useCaseAttachState.getActiveAndOnlineBuilder();
+ }
+
+ if (validatingBuilder.isValid()) {
+ // Apply CameraControl's SessionConfiguration to let CameraControl be able to control
+ // Repeating Request and process results.
+ validatingBuilder.add(cameraControl.getControlSessionConfiguration());
+
+ SessionConfiguration sessionConfiguration = validatingBuilder.build();
+ captureSession.setSessionConfiguration(sessionConfiguration);
+ }
+ }
+
+ /**
+ * Opens a new capture session.
+ *
+ * <p>The previously opened session will be safely disposed of before the new session opened.
+ */
+ void openCaptureSession() {
+ ValidatingBuilder validatingBuilder;
+ synchronized (attachedUseCaseLock) {
+ validatingBuilder = useCaseAttachState.getOnlineBuilder();
+ }
+ if (!validatingBuilder.isValid()) {
+ Log.d(TAG, "Unable to create capture session due to conflicting configurations");
+ return;
+ }
+
+ resetCaptureSession();
+
+ if (cameraDevice == null) {
+ Log.d(TAG, "CameraDevice is null");
+ return;
+ }
+
+ try {
+ captureSession.open(validatingBuilder.build(), cameraDevice);
+ } catch (CameraAccessException e) {
+ Log.d(TAG, "Unable to configure camera " + cameraId + " due to " + e.getMessage());
+ }
+ }
+
+ /**
+ * Closes the currently opened capture session, so it can be safely disposed. Replaces the old
+ * session with a new session initialized with the old session's configuration.
+ */
+ void resetCaptureSession() {
+ Log.d(TAG, "Closing Capture Session");
+ captureSession.close();
+
+ // Recreate an initialized (but not opened) capture session from the previous configuration
+ SessionConfiguration previousSessionConfiguration =
+ captureSession.getSessionConfiguration();
+ List<CaptureRequestConfiguration> unissuedCaptureRequestConfigurations =
+ captureSession.getCaptureRequestConfigurations();
+ captureSession = new CaptureSession(handler);
+ captureSession.setSessionConfiguration(previousSessionConfiguration);
+ // When the previous capture session has not reached the open state, the issued single
+ // capture
+ // requests will still be in request queue and will need to be passed to the next capture
+ // session.
+ captureSession.issueSingleCaptureRequests(unissuedCaptureRequestConfigurations);
+ }
+
+ private CameraDevice.StateCallback createDeviceStateCallback() {
+ synchronized (attachedUseCaseLock) {
+ SessionConfiguration configuration = useCaseAttachState.getOnlineBuilder().build();
+ return CameraDeviceStateCallbacks.createComboCallback(
+ stateCallback, configuration.getDeviceStateCallback());
+ }
+ }
+
+ /**
+ * Attach a repeating surface to a {@link CaptureRequestConfiguration} when the configuration
+ * indicate that it needs a repeating surface.
+ *
+ * @param captureRequestConfiguration the configuration to attach a repeating surface
+ */
+ private void checkAndAttachRepeatingSurface(
+ CaptureRequestConfiguration captureRequestConfiguration) {
+ if (!captureRequestConfiguration.getSurfaces().isEmpty()) {
+ return;
+ }
+
+ if (!captureRequestConfiguration.isUseRepeatingSurface()) {
+ return;
+ }
+
+ Collection<BaseUseCase> activeUseCases;
+ synchronized (attachedUseCaseLock) {
+ activeUseCases = useCaseAttachState.getActiveAndOnlineUseCases();
+ }
+
+ DeferrableSurface repeatingSurface = null;
+ for (BaseUseCase useCase : activeUseCases) {
+ SessionConfiguration sessionConfiguration = useCase.getSessionConfiguration(cameraId);
+ List<DeferrableSurface> surfaces =
+ sessionConfiguration.getCaptureRequestConfiguration().getSurfaces();
+ if (!surfaces.isEmpty()) {
+ // When an use case is active, all surfaces in its CaptureRequestConfiguration are
+ // added to
+ // the repeating request. Choose the first one here as the repeating surface.
+ repeatingSurface = surfaces.get(0);
+ break;
+ }
+ }
+
+ if (repeatingSurface == null) {
+ throw new IllegalStateException(
+ "Unable to find a repeating surface to attach to CaptureRequestConfiguration");
+ }
+
+ captureRequestConfiguration.addSurface(repeatingSurface);
+ }
+
+ /** Returns the Camera2CameraControl attached to Camera */
+ @Override
+ public CameraControl getCameraControl() {
+ return cameraControl;
+ }
+
+ /**
+ * Submits single request
+ *
+ * @param captureRequestConfiguration capture configuration used for creating CaptureRequest
+ */
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ @Override
+ public void submitSingleRequest(CaptureRequestConfiguration captureRequestConfiguration) {
+ if (Looper.myLooper() != handler.getLooper()) {
+ handler.post(() -> submitSingleRequest(captureRequestConfiguration));
+ return;
+ }
+ Log.d(TAG, "issue single capture request for camera " + cameraId);
+
+ checkAndAttachRepeatingSurface(captureRequestConfiguration);
+
+ // Recreates the Builder to add implementationOptions from CameraControl
+ CaptureRequestConfiguration.Builder builder =
+ CaptureRequestConfiguration.Builder.from(captureRequestConfiguration);
+ builder.addImplementationOptions(cameraControl.getSingleRequestImplOptions());
+
+ captureSession.issueSingleCaptureRequest(builder.build());
+ }
+
+ /**
+ * Re-sends repeating request based on current SessionConfigurations and CameraControl's Global
+ * SessionConfiguration
+ */
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ @Override
+ public void updateRepeatingRequest() {
+ if (Looper.myLooper() != handler.getLooper()) {
+ handler.post(() -> updateRepeatingRequest());
+ return;
+ }
+
+ updateCaptureSessionConfiguration();
+ }
+
+ enum State {
+ /** The default state of the camera before construction. */
+ UNINITIALIZED,
+ /**
+ * Stable state once the camera has been constructed.
+ *
+ * <p>At this state the {@link CameraDevice} should be invalid, but threads should be still
+ * in a valid state. Whenever a camera device is fully closed the camera should return to
+ * this state.
+ *
+ * <p>After an error occurs the camera returns to this state so that the device can be
+ * cleanly reopened.
+ */
+ INITIALIZED,
+ /**
+ * A transitional state where the camera device is currently opening.
+ *
+ * <p>At the end of this state, the camera should move into either the OPENED or CLOSING
+ * state.
+ */
+ OPENING,
+ /**
+ * A stable state where the camera has been opened.
+ *
+ * <p>During this state the camera device should be valid. It is at this time a valid
+ * capture session can be active. Capture requests should be issued during this state only.
+ */
+ OPENED,
+ /**
+ * A transitional state where the camera device is currently closing.
+ *
+ * <p>At the end of this state, the camera should move into the INITIALIZED state.
+ */
+ CLOSING,
+ /**
+ * A transitional state where the camera was previously closing, but not fully closed before
+ * a call to open was made.
+ *
+ * <p>At the end of this state, the camera should move into one of two states. The OPENING
+ * state if the device becomes fully closed, since it must restart the process of opening a
+ * camera. The OPENED state if the device becomes opened, which can occur if a call to close
+ * had been done during the OPENING state.
+ */
+ REOPENING,
+ /**
+ * A transitional state where the camera will be closing permanently.
+ *
+ * <p>At the end of this state, the camera should move into the RELEASED state.
+ */
+ RELEASING,
+ /**
+ * A stable state where the camera has been permanently closed.
+ *
+ * <p>During this state all resources should be released and all operations on the camera
+ * will do nothing.
+ */
+ RELEASED
+ }
+
+ final class StateCallback extends CameraDevice.StateCallback {
+
+ @Override
+ public void onOpened(CameraDevice cameraDevice) {
+ Log.d(TAG, "CameraDevice.onOpened(): " + cameraDevice.getId());
+ switch (state.get()) {
+ case CLOSING:
+ case RELEASING:
+ cameraDevice.close();
+ Camera.this.cameraDevice = null;
+ break;
+ case OPENING:
+ case REOPENING:
+ state.set(State.OPENED);
+ Camera.this.cameraDevice = cameraDevice;
+ openCaptureSession();
+ break;
+ default:
+ throw new IllegalStateException(
+ "onOpened() should not be possible from state: " + state.get());
+ }
+ }
+
+ @Override
+ public void onClosed(CameraDevice cameraDevice) {
+ Log.d(TAG, "CameraDevice.onClosed(): " + cameraDevice.getId());
+ resetCaptureSession();
+ switch (state.get()) {
+ case CLOSING:
+ state.set(State.INITIALIZED);
+ Camera.this.cameraDevice = null;
+ break;
+ case REOPENING:
+ state.set(State.OPENING);
+ openCameraDevice();
+ break;
+ case RELEASING:
+ state.set(State.RELEASED);
+ Camera.this.cameraDevice = null;
+ break;
+ default:
+ CameraX.postError(
+ CameraX.ErrorCode.CAMERA_STATE_INCONSISTENT,
+ "Camera closed while in state: " + state.get());
+ }
+ }
+
+ @Override
+ public void onDisconnected(CameraDevice cameraDevice) {
+ Log.d(TAG, "CameraDevice.onDisconnected(): " + cameraDevice.getId());
+ resetCaptureSession();
+ switch (state.get()) {
+ case CLOSING:
+ state.set(State.INITIALIZED);
+ Camera.this.cameraDevice = null;
+ break;
+ case REOPENING:
+ case OPENED:
+ case OPENING:
+ state.set(State.CLOSING);
+ cameraDevice.close();
+ Camera.this.cameraDevice = null;
+ break;
+ case RELEASING:
+ state.set(State.RELEASED);
+ cameraDevice.close();
+ Camera.this.cameraDevice = null;
+ break;
+ default:
+ throw new IllegalStateException(
+ "onDisconnected() should not be possible from state: " + state.get());
+ }
+ }
+
+ private String getErrorMessage(int errorCode) {
+ switch (errorCode) {
+ case CameraDevice.StateCallback.ERROR_CAMERA_DEVICE:
+ return "ERROR_CAMERA_DEVICE";
+ case CameraDevice.StateCallback.ERROR_CAMERA_DISABLED:
+ return "ERROR_CAMERA_DISABLED";
+ case CameraDevice.StateCallback.ERROR_CAMERA_IN_USE:
+ return "ERROR_CAMERA_IN_USE";
+ case CameraDevice.StateCallback.ERROR_CAMERA_SERVICE:
+ return "ERROR_CAMERA_SERVICE";
+ case CameraDevice.StateCallback.ERROR_MAX_CAMERAS_IN_USE:
+ return "ERROR_MAX_CAMERAS_IN_USE";
+ default: // fall out
+ }
+ return "UNKNOWN ERROR";
+ }
+
+ @Override
+ public void onError(CameraDevice cameraDevice, int error) {
+ Log.e(
+ TAG,
+ "CameraDevice.onError(): "
+ + cameraDevice.getId()
+ + " with error: "
+ + getErrorMessage(error));
+ resetCaptureSession();
+ switch (state.get()) {
+ case CLOSING:
+ state.set(State.INITIALIZED);
+ Camera.this.cameraDevice = null;
+ break;
+ case REOPENING:
+ case OPENED:
+ case OPENING:
+ state.set(State.CLOSING);
+ cameraDevice.close();
+ Camera.this.cameraDevice = null;
+ break;
+ case RELEASING:
+ state.set(State.RELEASED);
+ cameraDevice.close();
+ Camera.this.cameraDevice = null;
+ break;
+ default:
+ throw new IllegalStateException(
+ "onError() should not be possible from state: " + state.get());
+ }
+ }
+ }
+}
diff --git a/camera/camera2/src/main/java/androidx/camera/camera2/Camera2AppConfiguration.java b/camera/camera2/src/main/java/androidx/camera/camera2/Camera2AppConfiguration.java
new file mode 100644
index 0000000..ca2f6b9
--- /dev/null
+++ b/camera/camera2/src/main/java/androidx/camera/camera2/Camera2AppConfiguration.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright (C) 2019 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.camera2;
+
+import android.content.Context;
+
+import androidx.annotation.RestrictTo;
+import androidx.annotation.RestrictTo.Scope;
+import androidx.camera.core.AppConfiguration;
+import androidx.camera.core.CameraDeviceSurfaceManager;
+import androidx.camera.core.CameraFactory;
+import androidx.camera.core.ExtendableUseCaseConfigFactory;
+import androidx.camera.core.ImageAnalysisUseCaseConfiguration;
+import androidx.camera.core.ImageCaptureUseCaseConfiguration;
+import androidx.camera.core.VideoCaptureUseCaseConfiguration;
+import androidx.camera.core.ViewFinderUseCaseConfiguration;
+
+/**
+ * Convenience class for generating a pre-populated Camera2 {@link AppConfiguration}.
+ *
+ * @hide Until CameraX.init() is made public
+ */
+@RestrictTo(Scope.LIBRARY_GROUP)
+public final class Camera2AppConfiguration {
+
+ private Camera2AppConfiguration() {
+ }
+
+ /**
+ * Creates the {@link AppConfiguration} containing the Camera2 implementation pieces for
+ * CameraX.
+ */
+ public static AppConfiguration create(Context context) {
+ // Create the camera factory for creating Camera2 camera objects
+ CameraFactory cameraFactory = new Camera2CameraFactory(context);
+
+ // Create the DeviceSurfaceManager for Camera2
+ CameraDeviceSurfaceManager surfaceManager = new Camera2DeviceSurfaceManager(context);
+
+ // Create default configuration factory
+ ExtendableUseCaseConfigFactory configFactory = new ExtendableUseCaseConfigFactory();
+ configFactory.installDefaultProvider(
+ ImageAnalysisUseCaseConfiguration.class,
+ new DefaultImageAnalysisConfigurationProvider(cameraFactory));
+ configFactory.installDefaultProvider(
+ ImageCaptureUseCaseConfiguration.class,
+ new DefaultImageCaptureConfigurationProvider(cameraFactory));
+ configFactory.installDefaultProvider(
+ VideoCaptureUseCaseConfiguration.class,
+ new DefaultVideoCaptureConfigurationProvider(cameraFactory));
+ configFactory.installDefaultProvider(
+ ViewFinderUseCaseConfiguration.class,
+ new DefaultViewFinderConfigurationProvider(cameraFactory));
+
+ AppConfiguration.Builder appConfigBuilder =
+ new AppConfiguration.Builder()
+ .setCameraFactory(cameraFactory)
+ .setDeviceSurfaceManager(surfaceManager)
+ .setUseCaseConfigFactory(configFactory);
+
+ return appConfigBuilder.build();
+ }
+}
diff --git a/camera/camera2/src/main/java/androidx/camera/camera2/Camera2CameraCaptureResult.java b/camera/camera2/src/main/java/androidx/camera/camera2/Camera2CameraCaptureResult.java
new file mode 100644
index 0000000..efddf0e
--- /dev/null
+++ b/camera/camera2/src/main/java/androidx/camera/camera2/Camera2CameraCaptureResult.java
@@ -0,0 +1,183 @@
+/*
+ * Copyright (C) 2019 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.camera2;
+
+import android.hardware.camera2.CaptureResult;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+import androidx.camera.core.CameraCaptureMetaData.AeState;
+import androidx.camera.core.CameraCaptureMetaData.AfMode;
+import androidx.camera.core.CameraCaptureMetaData.AfState;
+import androidx.camera.core.CameraCaptureMetaData.AwbState;
+import androidx.camera.core.CameraCaptureMetaData.FlashState;
+import androidx.camera.core.CameraCaptureResult;
+
+/** The camera2 implementation for the capture result of a single image capture. */
+final class Camera2CameraCaptureResult implements CameraCaptureResult {
+ private static final String TAG = "Camera2CameraCaptureResult";
+
+ /** The actual camera2 {@link CaptureResult}. */
+ private final CaptureResult captureResult;
+
+ Camera2CameraCaptureResult(CaptureResult captureResult) {
+ this.captureResult = captureResult;
+ }
+
+ /**
+ * Converts the camera2 {@link CaptureResult#CONTROL_AF_MODE} to {@link AfMode}.
+ *
+ * @return the {@link AfMode}.
+ */
+ @NonNull
+ @Override
+ public AfMode getAfMode() {
+ Integer mode = captureResult.get(CaptureResult.CONTROL_AF_MODE);
+ if (mode == null) {
+ return AfMode.UNKNOWN;
+ }
+ switch (mode) {
+ case CaptureResult.CONTROL_AF_MODE_OFF:
+ case CaptureResult.CONTROL_AF_MODE_EDOF:
+ return AfMode.OFF;
+ case CaptureResult.CONTROL_AF_MODE_AUTO:
+ case CaptureResult.CONTROL_AF_MODE_MACRO:
+ return AfMode.ON_MANUAL_AUTO;
+ case CaptureResult.CONTROL_AF_MODE_CONTINUOUS_PICTURE:
+ case CaptureResult.CONTROL_AF_MODE_CONTINUOUS_VIDEO:
+ return AfMode.ON_CONTINUOUS_AUTO;
+ default: // fall out
+ }
+ Log.e(TAG, "Undefined af mode: " + mode);
+ return AfMode.UNKNOWN;
+ }
+
+ /**
+ * Converts the camera2 {@link CaptureResult#CONTROL_AF_STATE} to {@link AfState}.
+ *
+ * @return the {@link AfState}.
+ */
+ @NonNull
+ @Override
+ public AfState getAfState() {
+ Integer state = captureResult.get(CaptureResult.CONTROL_AF_STATE);
+ if (state == null) {
+ return AfState.UNKNOWN;
+ }
+ switch (state) {
+ case CaptureResult.CONTROL_AF_STATE_INACTIVE:
+ return AfState.INACTIVE;
+ case CaptureResult.CONTROL_AF_STATE_ACTIVE_SCAN:
+ case CaptureResult.CONTROL_AF_STATE_PASSIVE_SCAN:
+ case CaptureResult.CONTROL_AF_STATE_PASSIVE_UNFOCUSED:
+ return AfState.SCANNING;
+ case CaptureResult.CONTROL_AF_STATE_FOCUSED_LOCKED:
+ return AfState.LOCKED_FOCUSED;
+ case CaptureResult.CONTROL_AF_STATE_NOT_FOCUSED_LOCKED:
+ return AfState.LOCKED_NOT_FOCUSED;
+ case CaptureResult.CONTROL_AF_STATE_PASSIVE_FOCUSED:
+ return AfState.FOCUSED;
+ default: // fall out
+ }
+ Log.e(TAG, "Undefined af state: " + state);
+ return AfState.UNKNOWN;
+ }
+
+ /**
+ * Converts the camera2 {@link CaptureResult#CONTROL_AE_STATE} to {@link AeState}.
+ *
+ * @return the {@link AeState}.
+ */
+ @NonNull
+ @Override
+ public AeState getAeState() {
+ Integer state = captureResult.get(CaptureResult.CONTROL_AE_STATE);
+ if (state == null) {
+ return AeState.UNKNOWN;
+ }
+ switch (state) {
+ case CaptureResult.CONTROL_AE_STATE_INACTIVE:
+ return AeState.INACTIVE;
+ case CaptureResult.CONTROL_AE_STATE_SEARCHING:
+ case CaptureResult.CONTROL_AE_STATE_PRECAPTURE:
+ return AeState.SEARCHING;
+ case CaptureResult.CONTROL_AE_STATE_FLASH_REQUIRED:
+ return AeState.FLASH_REQUIRED;
+ case CaptureResult.CONTROL_AE_STATE_CONVERGED:
+ return AeState.CONVERGED;
+ case CaptureResult.CONTROL_AE_STATE_LOCKED:
+ return AeState.LOCKED;
+ default: // fall out
+ }
+ Log.e(TAG, "Undefined ae state: " + state);
+ return AeState.UNKNOWN;
+ }
+
+ /**
+ * Converts the camera2 {@link CaptureResult#CONTROL_AWB_STATE} to {@link AwbState}.
+ *
+ * @return the {@link AwbState}.
+ */
+ @NonNull
+ @Override
+ public AwbState getAwbState() {
+ Integer state = captureResult.get(CaptureResult.CONTROL_AWB_STATE);
+ if (state == null) {
+ return AwbState.UNKNOWN;
+ }
+ switch (state) {
+ case CaptureResult.CONTROL_AWB_STATE_INACTIVE:
+ return AwbState.INACTIVE;
+ case CaptureResult.CONTROL_AWB_STATE_SEARCHING:
+ return AwbState.METERING;
+ case CaptureResult.CONTROL_AWB_STATE_CONVERGED:
+ return AwbState.CONVERGED;
+ case CaptureResult.CONTROL_AWB_STATE_LOCKED:
+ return AwbState.LOCKED;
+ default: // fall out
+ }
+ Log.e(TAG, "Undefined awb state: " + state);
+ return AwbState.UNKNOWN;
+ }
+
+ /**
+ * Converts the camera2 {@link CaptureResult#FLASH_STATE} to {@link FlashState}.
+ *
+ * @return the {@link FlashState}.
+ */
+ @NonNull
+ @Override
+ public FlashState getFlashState() {
+ Integer state = captureResult.get(CaptureResult.FLASH_STATE);
+ if (state == null) {
+ return FlashState.UNKNOWN;
+ }
+ switch (state) {
+ case CaptureResult.FLASH_STATE_UNAVAILABLE:
+ case CaptureResult.FLASH_STATE_CHARGING:
+ return FlashState.NONE;
+ case CaptureResult.FLASH_STATE_READY:
+ return FlashState.READY;
+ case CaptureResult.FLASH_STATE_FIRED:
+ case CaptureResult.FLASH_STATE_PARTIAL:
+ return FlashState.FIRED;
+ default: // fall out
+ }
+ Log.e(TAG, "Undefined flash state: " + state);
+ return FlashState.UNKNOWN;
+ }
+}
diff --git a/camera/camera2/src/main/java/androidx/camera/camera2/Camera2CameraControl.java b/camera/camera2/src/main/java/androidx/camera/camera2/Camera2CameraControl.java
new file mode 100644
index 0000000..a557cf2
--- /dev/null
+++ b/camera/camera2/src/main/java/androidx/camera/camera2/Camera2CameraControl.java
@@ -0,0 +1,479 @@
+/*
+ * Copyright (C) 2019 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.camera2;
+
+import android.graphics.Rect;
+import android.hardware.camera2.CameraCaptureSession;
+import android.hardware.camera2.CameraCaptureSession.CaptureCallback;
+import android.hardware.camera2.CameraDevice;
+import android.hardware.camera2.CaptureRequest;
+import android.hardware.camera2.CaptureResult;
+import android.hardware.camera2.TotalCaptureResult;
+import android.hardware.camera2.params.MeteringRectangle;
+import android.os.Handler;
+import android.os.Looper;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.annotation.VisibleForTesting;
+import androidx.camera.core.CameraControl;
+import androidx.camera.core.CaptureRequestConfiguration;
+import androidx.camera.core.Configuration;
+import androidx.camera.core.FlashMode;
+import androidx.camera.core.OnFocusCompletedListener;
+import androidx.camera.core.SessionConfiguration;
+
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * A Camera2 implementation for CameraControl interface
+ *
+ * <p>It takes a {@link Camera2RequestRunner} for executing capture request and a {@link Handler} in
+ * which methods run.
+ *
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public final class Camera2CameraControl implements CameraControl {
+ @VisibleForTesting
+ static final long FOCUS_TIMEOUT = 5000;
+ private static final String TAG = "Camera2CameraControl";
+ private final Camera2RequestRunner camera2RequestRunner;
+ private final Handler handler;
+ private final CameraControlSessionCallback sessionCallback = new CameraControlSessionCallback();
+ // use volatile modifier to make these variables in sync in all threads.
+ private volatile boolean isTorchOn = false;
+ private volatile boolean isFocusLocked = false;
+ private volatile FlashMode flashMode = FlashMode.OFF;
+ private volatile Rect cropRect = null;
+ private volatile MeteringRectangle afRect;
+ private volatile MeteringRectangle aeRect;
+ private volatile MeteringRectangle awbRect;
+ private volatile Integer currentAfState = CaptureResult.CONTROL_AF_STATE_INACTIVE;
+ private volatile OnFocusCompletedListener focusListener = null;
+ private volatile Handler focusListenerHandler = null;
+ private volatile CaptureResultListener sessionListenerForFocus = null;
+ private final Runnable handleFocusTimeoutRunnable =
+ () -> {
+ cancelFocus();
+
+ sessionCallback.removeListener(sessionListenerForFocus);
+
+ if (focusListener != null
+ && currentAfState == CaptureResult.CONTROL_AF_STATE_ACTIVE_SCAN) {
+ runInFocusListenerHandler(
+ () -> focusListener.onFocusTimedOut(afRect.getRect()));
+ }
+ };
+ public Camera2CameraControl(Camera2RequestRunner camera2RequestRunner, Handler handler) {
+ this.camera2RequestRunner = camera2RequestRunner;
+ this.handler = handler;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public void setCropRegion(Rect crop) {
+ if (Looper.myLooper() != handler.getLooper()) {
+ handler.post(() -> setCropRegion(crop));
+ return;
+ }
+
+ cropRect = crop;
+ camera2RequestRunner.updateRepeatingRequest();
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public void focus(
+ Rect focus,
+ Rect metering,
+ @Nullable OnFocusCompletedListener listener,
+ @Nullable Handler listenerHandler) {
+ if (Looper.myLooper() != handler.getLooper()) {
+ handler.post(() -> focus(focus, metering, listener, listenerHandler));
+ return;
+ }
+
+ sessionCallback.removeListener(sessionListenerForFocus);
+
+ handler.removeCallbacks(handleFocusTimeoutRunnable);
+
+ afRect = new MeteringRectangle(focus, MeteringRectangle.METERING_WEIGHT_MAX);
+ aeRect = new MeteringRectangle(metering, MeteringRectangle.METERING_WEIGHT_MAX);
+ awbRect = new MeteringRectangle(metering, MeteringRectangle.METERING_WEIGHT_MAX);
+ Log.d(TAG, "Setting new AF rectangle: " + afRect);
+ Log.d(TAG, "Setting new AE rectangle: " + aeRect);
+ Log.d(TAG, "Setting new AWB rectangle: " + awbRect);
+
+ focusListener = listener;
+ focusListenerHandler =
+ (listenerHandler != null ? listenerHandler : new Handler(Looper.getMainLooper()));
+ currentAfState = CaptureResult.CONTROL_AF_STATE_INACTIVE;
+ isFocusLocked = true;
+
+ if (listener != null) {
+
+ sessionListenerForFocus =
+ (result) -> {
+ Integer afState = result.get(CaptureResult.CONTROL_AF_STATE);
+ if (afState == null) {
+ return false;
+ }
+
+ if (currentAfState == CaptureResult.CONTROL_AF_STATE_ACTIVE_SCAN) {
+ if (afState == CaptureResult.CONTROL_AF_STATE_FOCUSED_LOCKED) {
+ runInFocusListenerHandler(
+ () -> focusListener.onFocusLocked(afRect.getRect()));
+ return true; // finished
+ } else if (afState
+ == CaptureResult.CONTROL_AF_STATE_NOT_FOCUSED_LOCKED) {
+ runInFocusListenerHandler(
+ () -> focusListener.onFocusUnableToLock(afRect.getRect()));
+ return true; // finished
+ }
+ }
+ if (!currentAfState.equals(afState)) {
+ currentAfState = afState;
+ }
+ return false; // continue checking
+ };
+
+ sessionCallback.addListener(sessionListenerForFocus);
+ }
+ camera2RequestRunner.updateRepeatingRequest();
+
+ triggerAf();
+ if (FOCUS_TIMEOUT != 0) {
+ handler.postDelayed(handleFocusTimeoutRunnable, FOCUS_TIMEOUT);
+ }
+ }
+
+ private void runInFocusListenerHandler(Runnable runnable) {
+ if (focusListenerHandler != null) {
+ focusListenerHandler.post(runnable);
+ }
+ }
+
+ /** Cancels the focus operation. */
+ @VisibleForTesting
+ void cancelFocus() {
+ if (Looper.myLooper() != handler.getLooper()) {
+ handler.post(() -> cancelFocus());
+ return;
+ }
+
+ handler.removeCallbacks(handleFocusTimeoutRunnable);
+
+ MeteringRectangle zeroRegion =
+ new MeteringRectangle(new Rect(), MeteringRectangle.METERING_WEIGHT_DONT_CARE);
+ afRect = zeroRegion;
+ aeRect = zeroRegion;
+ awbRect = zeroRegion;
+
+ // Send a single request to cancel af process
+ CaptureRequestConfiguration.Builder singleRequestBuilder =
+ new CaptureRequestConfiguration.Builder();
+ singleRequestBuilder.setTemplateType(getDefaultTemplate());
+ singleRequestBuilder.setUseRepeatingSurface(true);
+ singleRequestBuilder.addCharacteristic(
+ CaptureRequest.CONTROL_AF_TRIGGER, CaptureRequest.CONTROL_AF_TRIGGER_CANCEL);
+ camera2RequestRunner.submitSingleRequest(singleRequestBuilder.build());
+
+ isFocusLocked = false;
+ camera2RequestRunner.updateRepeatingRequest();
+ }
+
+ private void updateRepeatingRequest() {
+ if (Looper.myLooper() != handler.getLooper()) {
+ handler.post(() -> updateRepeatingRequest());
+ return;
+ }
+
+ camera2RequestRunner.updateRepeatingRequest();
+ }
+
+ @Override
+ public FlashMode getFlashMode() {
+ return flashMode;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public void setFlashMode(FlashMode flashMode) {
+ // update flashMode immediately so that following getFlashMode() returns correct value.
+ this.flashMode = flashMode;
+
+ updateRepeatingRequest();
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public void enableTorch(boolean torch) {
+ // update isTorchOn immediately so that following isTorchOn() returns correct value.
+ isTorchOn = torch;
+ enableTorchInternal(torch);
+ }
+
+ private void enableTorchInternal(boolean torch) {
+ if (Looper.myLooper() != handler.getLooper()) {
+ handler.post(() -> enableTorchInternal(torch));
+ return;
+ }
+
+ if (!torch) {
+ CaptureRequestConfiguration.Builder singleRequestBuilder =
+ new CaptureRequestConfiguration.Builder();
+ singleRequestBuilder.setTemplateType(getDefaultTemplate());
+ singleRequestBuilder.addCharacteristic(
+ CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON);
+ singleRequestBuilder.setUseRepeatingSurface(true);
+
+ camera2RequestRunner.submitSingleRequest(singleRequestBuilder.build());
+ }
+ camera2RequestRunner.updateRepeatingRequest();
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public boolean isTorchOn() {
+ return isTorchOn;
+ }
+
+ @Override
+ public boolean isFocusLocked() {
+ return isFocusLocked;
+ }
+
+ /**
+ * Issues a {@link CaptureRequest#CONTROL_AF_TRIGGER_START} request to start auto focus scan.
+ */
+ @Override
+ public void triggerAf() {
+ if (Looper.myLooper() != handler.getLooper()) {
+ handler.post(() -> triggerAf());
+ return;
+ }
+
+ CaptureRequestConfiguration.Builder builder = new CaptureRequestConfiguration.Builder();
+
+ builder.setTemplateType(getDefaultTemplate());
+ builder.setUseRepeatingSurface(true);
+ builder.addCharacteristic(
+ CaptureRequest.CONTROL_AF_TRIGGER, CaptureRequest.CONTROL_AF_TRIGGER_START);
+ camera2RequestRunner.submitSingleRequest(builder.build());
+ }
+
+ /**
+ * Issues a {@link CaptureRequest#CONTROL_AE_PRECAPTURE_TRIGGER_START} request to start auto
+ * exposure scan.
+ */
+ @Override
+ public void triggerAePrecapture() {
+ if (Looper.myLooper() != handler.getLooper()) {
+ handler.post(() -> triggerAePrecapture());
+ return;
+ }
+
+ CaptureRequestConfiguration.Builder builder = new CaptureRequestConfiguration.Builder();
+ builder.setTemplateType(getDefaultTemplate());
+ builder.setUseRepeatingSurface(true);
+ builder.addCharacteristic(
+ CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER,
+ CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_START);
+ camera2RequestRunner.submitSingleRequest(builder.build());
+ }
+
+ /**
+ * Issues {@link CaptureRequest#CONTROL_AF_TRIGGER_CANCEL} or {@link
+ * CaptureRequest#CONTROL_AE_PRECAPTURE_TRIGGER_CANCEL} request to cancel auto focus or auto
+ * exposure scan.
+ */
+ @Override
+ public void cancelAfAeTrigger(boolean cancelAfTrigger, boolean cancelAePrecaptureTrigger) {
+ if (Looper.myLooper() != handler.getLooper()) {
+ handler.post(() -> cancelAfAeTrigger(cancelAfTrigger, cancelAePrecaptureTrigger));
+ return;
+ }
+ CaptureRequestConfiguration.Builder builder = new CaptureRequestConfiguration.Builder();
+ builder.setUseRepeatingSurface(true);
+ builder.setTemplateType(getDefaultTemplate());
+ if (cancelAfTrigger) {
+ builder.addCharacteristic(
+ CaptureRequest.CONTROL_AF_TRIGGER, CaptureRequest.CONTROL_AF_TRIGGER_CANCEL);
+ }
+ if (cancelAePrecaptureTrigger) {
+ builder.addCharacteristic(
+ CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER,
+ CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_CANCEL);
+ }
+ camera2RequestRunner.submitSingleRequest(builder.build());
+ }
+
+ private int getDefaultTemplate() {
+ return CameraDevice.TEMPLATE_PREVIEW;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public SessionConfiguration getControlSessionConfiguration() {
+ SessionConfiguration.Builder builder = new SessionConfiguration.Builder();
+
+ builder.setTemplateType(getDefaultTemplate());
+ builder.setCameraCaptureCallback(CaptureCallbackContainer.create(sessionCallback));
+
+ Camera2Configuration.Builder requestOptionBuilder = new Camera2Configuration.Builder();
+
+ if (isTorchOn) {
+ requestOptionBuilder.setCaptureRequestOption(
+ CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON);
+ requestOptionBuilder.setCaptureRequestOption(
+ CaptureRequest.FLASH_MODE, CaptureRequest.FLASH_MODE_TORCH);
+ } else {
+ int aeMode = CaptureRequest.CONTROL_AE_MODE_ON;
+ switch (flashMode) {
+ case OFF:
+ aeMode = CaptureRequest.CONTROL_AE_MODE_ON;
+ break;
+ case ON:
+ aeMode = CaptureRequest.CONTROL_AE_MODE_ON_ALWAYS_FLASH;
+ break;
+ case AUTO:
+ aeMode = CaptureRequest.CONTROL_AE_MODE_ON_AUTO_FLASH;
+ break;
+ }
+ requestOptionBuilder.setCaptureRequestOption(CaptureRequest.CONTROL_AE_MODE, aeMode);
+ }
+
+ // also apply the common option for single requests.
+ Configuration singleRequestImpOptions = getSingleRequestImplOptions();
+ requestOptionBuilder.insertAllOptions(singleRequestImpOptions);
+ builder.setImplementationOptions(requestOptionBuilder.build());
+
+ return builder.build();
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public Configuration getSingleRequestImplOptions() {
+ Camera2Configuration.Builder builder = new Camera2Configuration.Builder();
+
+ builder.setCaptureRequestOption(
+ CaptureRequest.CONTROL_MODE, CaptureRequest.CONTROL_MODE_AUTO);
+
+ builder.setCaptureRequestOption(
+ CaptureRequest.CONTROL_AF_MODE,
+ isFocusLocked()
+ ? CaptureRequest.CONTROL_AF_MODE_AUTO
+ : CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE);
+
+ if (isTorchOn) {
+ // In case some random single request turns off the torch by accident, attach FLASH_MODE
+ // and
+ // CONTROL_AE_MODE_ON to all single requests.
+ builder.setCaptureRequestOption(
+ CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON);
+ builder.setCaptureRequestOption(
+ CaptureRequest.FLASH_MODE, CaptureRequest.FLASH_MODE_TORCH);
+ }
+ // Turning off Flash requires a single request of AE mode set to CONTROL_AE_MODE_ON. This is
+ // the reason why we do not specify AE mode by default for single request.
+
+ builder.setCaptureRequestOption(
+ CaptureRequest.CONTROL_AWB_MODE, CaptureRequest.CONTROL_AWB_MODE_AUTO);
+
+ if (afRect != null) {
+ builder.setCaptureRequestOption(
+ CaptureRequest.CONTROL_AF_REGIONS, new MeteringRectangle[]{afRect});
+ }
+ if (aeRect != null) {
+ builder.setCaptureRequestOption(
+ CaptureRequest.CONTROL_AE_REGIONS, new MeteringRectangle[]{aeRect});
+ }
+ if (awbRect != null) {
+ builder.setCaptureRequestOption(
+ CaptureRequest.CONTROL_AWB_REGIONS, new MeteringRectangle[]{awbRect});
+ }
+
+ if (cropRect != null) {
+ builder.setCaptureRequestOption(CaptureRequest.SCALER_CROP_REGION, cropRect);
+ }
+
+ return builder.build();
+ }
+
+ /** An interface to listen to camera capture results. */
+ private interface CaptureResultListener {
+ /**
+ * Callback to handle camera capture results.
+ *
+ * @param captureResult camera capture result.
+ * @return true to finish listening, false to continue listening.
+ */
+ boolean onCaptureResult(TotalCaptureResult captureResult);
+ }
+
+ static final class CameraControlSessionCallback extends CaptureCallback {
+
+ private final Set<CaptureResultListener> resultListeners = new HashSet<>();
+
+ public void addListener(CaptureResultListener listener) {
+ synchronized (resultListeners) {
+ resultListeners.add(listener);
+ }
+ }
+
+ public void removeListener(CaptureResultListener listener) {
+ if (listener == null) {
+ return;
+ }
+ synchronized (resultListeners) {
+ resultListeners.remove(listener);
+ }
+ }
+
+ @Override
+ public void onCaptureCompleted(
+ @NonNull CameraCaptureSession session,
+ @NonNull CaptureRequest request,
+ @NonNull TotalCaptureResult result) {
+ Set<CaptureResultListener> listeners;
+ synchronized (resultListeners) {
+ if (resultListeners.isEmpty()) {
+ return;
+ }
+ listeners = new HashSet<>(resultListeners);
+ }
+
+ Set<CaptureResultListener> removeSet = new HashSet<>();
+ for (CaptureResultListener listener : listeners) {
+ boolean isFinished = listener.onCaptureResult(result);
+ if (isFinished) {
+ removeSet.add(listener);
+ }
+ }
+
+ if (!removeSet.isEmpty()) {
+ synchronized (resultListeners) {
+ resultListeners.removeAll(removeSet);
+ }
+ }
+ }
+ }
+}
diff --git a/camera/camera2/src/main/java/androidx/camera/camera2/Camera2CameraFactory.java b/camera/camera2/src/main/java/androidx/camera/camera2/Camera2CameraFactory.java
new file mode 100644
index 0000000..32d3596
--- /dev/null
+++ b/camera/camera2/src/main/java/androidx/camera/camera2/Camera2CameraFactory.java
@@ -0,0 +1,111 @@
+/*
+ * Copyright (C) 2019 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.camera2;
+
+import android.content.Context;
+import android.hardware.camera2.CameraAccessException;
+import android.hardware.camera2.CameraCharacteristics;
+import android.hardware.camera2.CameraManager;
+import android.hardware.camera2.CameraMetadata;
+import android.os.Handler;
+import android.os.HandlerThread;
+
+import androidx.annotation.Nullable;
+import androidx.camera.core.BaseCamera;
+import androidx.camera.core.CameraFactory;
+import androidx.camera.core.CameraInfoUnavailableException;
+import androidx.camera.core.CameraX.LensFacing;
+import androidx.camera.core.CameraXThreads;
+
+import java.util.Arrays;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Set;
+
+/** The factory class that creates {@link androidx.camera.camera2.Camera} instances. */
+final class Camera2CameraFactory implements CameraFactory {
+ private static final String TAG = "Camera2CameraFactory";
+
+ private static final HandlerThread handlerThread = new HandlerThread(CameraXThreads.TAG);
+ private static final Handler handler;
+
+ static {
+ handlerThread.start();
+ handler = new Handler(handlerThread.getLooper());
+ }
+
+ private final CameraManager cameraManager;
+
+ Camera2CameraFactory(Context context) {
+ cameraManager = (CameraManager) context.getSystemService(Context.CAMERA_SERVICE);
+ }
+
+ @Override
+ public BaseCamera getCamera(String cameraId) {
+ return new Camera(cameraManager, cameraId, handler);
+ }
+
+ @Override
+ public Set<String> getAvailableCameraIds() throws CameraInfoUnavailableException {
+ List<String> camerasList = null;
+ try {
+ camerasList = Arrays.asList(cameraManager.getCameraIdList());
+ } catch (CameraAccessException e) {
+ throw new CameraInfoUnavailableException(
+ "Unable to retrieve list of cameras on device.", e);
+ }
+ // Use a LinkedHashSet to preserve order
+ return new LinkedHashSet<>(camerasList);
+ }
+
+ @Nullable
+ @Override
+ public String cameraIdForLensFacing(LensFacing lensFacing)
+ throws CameraInfoUnavailableException {
+ Set<String> cameraIds = getAvailableCameraIds();
+
+ // Convert to from CameraX enum to Camera2 CameraMetadata
+ Integer lensFacingInteger = -1;
+ switch (lensFacing) {
+ case BACK:
+ lensFacingInteger = CameraMetadata.LENS_FACING_BACK;
+ break;
+ case FRONT:
+ lensFacingInteger = CameraMetadata.LENS_FACING_FRONT;
+ break;
+ }
+
+ for (String cameraId : cameraIds) {
+ CameraCharacteristics characteristics = null;
+ try {
+ characteristics = cameraManager.getCameraCharacteristics(cameraId);
+ } catch (CameraAccessException e) {
+ throw new CameraInfoUnavailableException(
+ "Unable to retrieve info for camera with id " + cameraId + ".", e);
+ }
+ Integer cameraLensFacing = characteristics.get(CameraCharacteristics.LENS_FACING);
+ if (cameraLensFacing == null) {
+ continue;
+ }
+ if (cameraLensFacing.equals(lensFacingInteger)) {
+ return cameraId;
+ }
+ }
+
+ return null;
+ }
+}
diff --git a/camera/camera2/src/main/java/androidx/camera/camera2/Camera2CameraInfo.java b/camera/camera2/src/main/java/androidx/camera/camera2/Camera2CameraInfo.java
new file mode 100644
index 0000000..fd74c37
--- /dev/null
+++ b/camera/camera2/src/main/java/androidx/camera/camera2/Camera2CameraInfo.java
@@ -0,0 +1,84 @@
+/*
+ * Copyright (C) 2019 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.camera2;
+
+import android.hardware.camera2.CameraAccessException;
+import android.hardware.camera2.CameraCharacteristics;
+import android.hardware.camera2.CameraManager;
+
+import androidx.annotation.Nullable;
+import androidx.camera.core.CameraInfo;
+import androidx.camera.core.CameraInfoUnavailableException;
+import androidx.camera.core.CameraOrientationUtil;
+import androidx.camera.core.CameraX.LensFacing;
+import androidx.camera.core.ImageOutputConfiguration.RotationValue;
+
+/** Implementation of the {@link CameraInfo} interface that exposes parameters through camera2. */
+final class Camera2CameraInfo implements CameraInfo {
+
+ private final CameraCharacteristics cameraCharacteristics;
+
+ Camera2CameraInfo(CameraManager cameraManager, String cameraId)
+ throws CameraInfoUnavailableException {
+ try {
+ cameraCharacteristics = cameraManager.getCameraCharacteristics(cameraId);
+ } catch (CameraAccessException e) {
+ throw new CameraInfoUnavailableException(
+ "Unable to retrieve info for camera " + cameraId, e);
+ }
+
+ checkCharacteristicAvailable(
+ CameraCharacteristics.SENSOR_ORIENTATION, "Sensor orientation");
+ checkCharacteristicAvailable(CameraCharacteristics.LENS_FACING, "Lens facing direction");
+ }
+
+ @Nullable
+ @Override
+ public LensFacing getLensFacing() {
+ switch (cameraCharacteristics.get(CameraCharacteristics.LENS_FACING)) {
+ case CameraCharacteristics.LENS_FACING_FRONT:
+ return LensFacing.FRONT;
+ case CameraCharacteristics.LENS_FACING_BACK:
+ return LensFacing.BACK;
+ default:
+ return null;
+ }
+ }
+
+ @Override
+ public int getSensorRotationDegrees(@RotationValue int relativeRotation) {
+ int relativeRotationDegrees =
+ CameraOrientationUtil.surfaceRotationToDegrees(relativeRotation);
+ // Currently this assumes that a back-facing camera is always opposite to the screen.
+ // This may not be the case for all devices, so in the future we may need to handle that
+ // scenario.
+ boolean isOppositeFacingScreen = LensFacing.BACK.equals(getLensFacing());
+ return CameraOrientationUtil.getRelativeImageRotation(
+ relativeRotationDegrees,
+ cameraCharacteristics.get(CameraCharacteristics.SENSOR_ORIENTATION),
+ isOppositeFacingScreen);
+ }
+
+ private void checkCharacteristicAvailable(CameraCharacteristics.Key<?> key, String readableName)
+ throws CameraInfoUnavailableException {
+ if (cameraCharacteristics.get(key) == null) {
+ throw new CameraInfoUnavailableException(
+ "Camera characteristics map is missing value for characteristic: "
+ + readableName);
+ }
+ }
+}
diff --git a/camera/camera2/src/main/java/androidx/camera/camera2/Camera2CaptureSessionCaptureCallbacks.java b/camera/camera2/src/main/java/androidx/camera/camera2/Camera2CaptureSessionCaptureCallbacks.java
new file mode 100644
index 0000000..6f678ed
--- /dev/null
+++ b/camera/camera2/src/main/java/androidx/camera/camera2/Camera2CaptureSessionCaptureCallbacks.java
@@ -0,0 +1,170 @@
+/*
+ * Copyright (C) 2019 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.camera2;
+
+import android.hardware.camera2.CameraCaptureSession;
+import android.hardware.camera2.CaptureFailure;
+import android.hardware.camera2.CaptureRequest;
+import android.hardware.camera2.CaptureResult;
+import android.hardware.camera2.TotalCaptureResult;
+import android.view.Surface;
+
+import androidx.annotation.RestrictTo;
+import androidx.annotation.RestrictTo.Scope;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * Different implementations of {@link CameraCaptureSession.CaptureCallback}.
+ *
+ * @hide
+ */
+@RestrictTo(Scope.LIBRARY_GROUP)
+public final class Camera2CaptureSessionCaptureCallbacks {
+ private Camera2CaptureSessionCaptureCallbacks() {
+ }
+
+ /** Returns a session capture callback which does nothing. */
+ public static CameraCaptureSession.CaptureCallback createNoOpCallback() {
+ return new NoOpSessionCaptureCallback();
+ }
+
+ /** Returns a session capture callback which calls a list of other callbacks. */
+ static CameraCaptureSession.CaptureCallback createComboCallback(
+ List<CameraCaptureSession.CaptureCallback> callbacks) {
+ return new ComboSessionCaptureCallback(callbacks);
+ }
+
+ /** Returns a session capture callback which calls a list of other callbacks. */
+ public static CameraCaptureSession.CaptureCallback createComboCallback(
+ CameraCaptureSession.CaptureCallback... callbacks) {
+ return createComboCallback(Arrays.asList(callbacks));
+ }
+
+ static final class NoOpSessionCaptureCallback
+ extends CameraCaptureSession.CaptureCallback {
+ @Override
+ public void onCaptureBufferLost(
+ CameraCaptureSession session,
+ CaptureRequest request,
+ Surface surface,
+ long frame) {
+ }
+
+ @Override
+ public void onCaptureCompleted(
+ CameraCaptureSession session, CaptureRequest request, TotalCaptureResult result) {
+ }
+
+ @Override
+ public void onCaptureFailed(
+ CameraCaptureSession session, CaptureRequest request, CaptureFailure failure) {
+ }
+
+ @Override
+ public void onCaptureProgressed(
+ CameraCaptureSession session,
+ CaptureRequest request,
+ CaptureResult partialResult) {
+ }
+
+ @Override
+ public void onCaptureSequenceAborted(CameraCaptureSession session, int sequenceId) {
+ }
+
+ @Override
+ public void onCaptureSequenceCompleted(
+ CameraCaptureSession session, int sequenceId, long frame) {
+ }
+
+ @Override
+ public void onCaptureStarted(
+ CameraCaptureSession session, CaptureRequest request, long timestamp, long frame) {
+ }
+ }
+
+ private static final class ComboSessionCaptureCallback
+ extends CameraCaptureSession.CaptureCallback {
+ private final List<CameraCaptureSession.CaptureCallback> callbacks = new ArrayList<>();
+
+ ComboSessionCaptureCallback(List<CameraCaptureSession.CaptureCallback> callbacks) {
+ for (CameraCaptureSession.CaptureCallback callback : callbacks) {
+ // A no-op callback doesn't do anything, so avoid adding it to the final list.
+ if (!(callback instanceof NoOpSessionCaptureCallback)) {
+ this.callbacks.add(callback);
+ }
+ }
+ }
+
+ @Override
+ public void onCaptureBufferLost(
+ CameraCaptureSession session, CaptureRequest request, Surface surface, long frame) {
+ for (CameraCaptureSession.CaptureCallback callback : callbacks) {
+ callback.onCaptureBufferLost(session, request, surface, frame);
+ }
+ }
+
+ @Override
+ public void onCaptureCompleted(
+ CameraCaptureSession session, CaptureRequest request, TotalCaptureResult result) {
+ for (CameraCaptureSession.CaptureCallback callback : callbacks) {
+ callback.onCaptureCompleted(session, request, result);
+ }
+ }
+
+ @Override
+ public void onCaptureFailed(
+ CameraCaptureSession session, CaptureRequest request, CaptureFailure failure) {
+ for (CameraCaptureSession.CaptureCallback callback : callbacks) {
+ callback.onCaptureFailed(session, request, failure);
+ }
+ }
+
+ @Override
+ public void onCaptureProgressed(
+ CameraCaptureSession session, CaptureRequest request, CaptureResult partialResult) {
+ for (CameraCaptureSession.CaptureCallback callback : callbacks) {
+ callback.onCaptureProgressed(session, request, partialResult);
+ }
+ }
+
+ @Override
+ public void onCaptureSequenceAborted(CameraCaptureSession session, int sequenceId) {
+ for (CameraCaptureSession.CaptureCallback callback : callbacks) {
+ callback.onCaptureSequenceAborted(session, sequenceId);
+ }
+ }
+
+ @Override
+ public void onCaptureSequenceCompleted(
+ CameraCaptureSession session, int sequenceId, long frame) {
+ for (CameraCaptureSession.CaptureCallback callback : callbacks) {
+ callback.onCaptureSequenceCompleted(session, sequenceId, frame);
+ }
+ }
+
+ @Override
+ public void onCaptureStarted(
+ CameraCaptureSession session, CaptureRequest request, long timestamp, long frame) {
+ for (CameraCaptureSession.CaptureCallback callback : callbacks) {
+ callback.onCaptureStarted(session, request, timestamp, frame);
+ }
+ }
+ }
+}
diff --git a/camera/camera2/src/main/java/androidx/camera/camera2/Camera2Configuration.java b/camera/camera2/src/main/java/androidx/camera/camera2/Camera2Configuration.java
new file mode 100644
index 0000000..c49cded
--- /dev/null
+++ b/camera/camera2/src/main/java/androidx/camera/camera2/Camera2Configuration.java
@@ -0,0 +1,323 @@
+/*
+ * Copyright (C) 2019 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.camera2;
+
+import android.hardware.camera2.CameraCaptureSession;
+import android.hardware.camera2.CameraCaptureSession.CaptureCallback;
+import android.hardware.camera2.CameraDevice;
+import android.hardware.camera2.CameraDevice.StateCallback;
+import android.hardware.camera2.CaptureRequest;
+import android.hardware.camera2.CaptureResult;
+import android.view.Surface;
+
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.annotation.RestrictTo.Scope;
+import androidx.camera.core.Configuration;
+import androidx.camera.core.MutableConfiguration;
+import androidx.camera.core.MutableOptionsBundle;
+import androidx.camera.core.OptionsBundle;
+
+import java.util.HashSet;
+import java.util.Set;
+
+/** Configuration options related to the {@link android.hardware.camera2} APIs. */
+public final class Camera2Configuration implements Configuration.Reader {
+
+ static final String CAPTURE_REQUEST_ID_STEM = "camera2.captureRequest.option.";
+ static final Option<Integer> TEMPLATE_TYPE_OPTION =
+ Option.create("camera2.captureRequest.templateType", int.class);
+ static final Option<StateCallback> DEVICE_STATE_CALLBACK_OPTION =
+ Option.create("camera2.cameraDevice.stateCallback", StateCallback.class);
+ static final Option<CameraCaptureSession.StateCallback> SESSION_STATE_CALLBACK_OPTION =
+ Option.create(
+ "camera2.cameraCaptureSession.stateCallback",
+ CameraCaptureSession.StateCallback.class);
+ static final Option<CaptureCallback> SESSION_CAPTURE_CALLBACK_OPTION =
+ Option.create("camera2.cameraCaptureSession.captureCallback", CaptureCallback.class);
+ private final Configuration config;
+
+ /**
+ * Creates a Camera2Configuration for reading Camera2 options from the given config.
+ *
+ * @param config The config that potentially contains Camera2 options.
+ */
+ public Camera2Configuration(Configuration config) {
+ this.config = config;
+ }
+
+ // Unforunately, we can't get the Class<T> from the CaptureRequest.Key, so we're forced to erase
+ // the type. This shouldn't be a problem as long as we are only using these options within the
+ // Camera2Configuration and Camera2Configuration.Builder classes.
+ static Option<Object> createCaptureRequestOption(CaptureRequest.Key<?> key) {
+ return Option.create(CAPTURE_REQUEST_ID_STEM + key.getName(), Object.class, key);
+ }
+
+ /**
+ * Returns a value for the given {@link CaptureRequest.Key}.
+ *
+ * @param key The key to retrieve.
+ * @param valueIfMissing The value to return if this configuration option has not been set.
+ * @param <ValueT> The type of the value.
+ * @return The stored value or <code>valueIfMissing</code> if the value does not exist in this
+ * configuration.
+ */
+ public <ValueT> ValueT getCaptureRequestOption(
+ CaptureRequest.Key<ValueT> key, @Nullable ValueT valueIfMissing) {
+ @SuppressWarnings(
+ "unchecked") // Type should have been only set via Builder#setCaptureRequestOption()
+ Option<ValueT> opt =
+ (Option<ValueT>) Camera2Configuration.createCaptureRequestOption(key);
+ return getConfiguration().retrieveOption(opt, valueIfMissing);
+ }
+
+ /**
+ * Returns all capture request options contained in this configuration
+ *
+ * @hide
+ */
+ Set<Option<?>> getCaptureRequestOptions() {
+ Set<Option<?>> optionSet = new HashSet<>();
+ findOptions(
+ Camera2Configuration.CAPTURE_REQUEST_ID_STEM,
+ option -> {
+ optionSet.add(option);
+ return true;
+ });
+ return optionSet;
+ }
+
+ /**
+ * Returns the CameraDevice template from the given configuration.
+ *
+ * <p>See {@link CameraDevice} for valid template types. For example, {@link
+ * CameraDevice#TEMPLATE_PREVIEW}.
+ *
+ * @param valueIfMissing The value to return if this configuration option has not been set.
+ * @return The stored value or <code>valueIfMissing</code> if the value does not exist in this
+ * configuration.
+ */
+ int getCaptureRequestTemplate(int valueIfMissing) {
+ return getConfiguration().retrieveOption(TEMPLATE_TYPE_OPTION, valueIfMissing);
+ }
+
+ /**
+ * Returns the stored {@link CameraDevice.StateCallback}.
+ *
+ * @param valueIfMissing The value to return if this configuration option has not been set.
+ * @return The stored value or <code>valueIfMissing</code> if the value does not exist in this
+ * configuration.
+ */
+ public CameraDevice.StateCallback getDeviceStateCallback(
+ CameraDevice.StateCallback valueIfMissing) {
+ return getConfiguration().retrieveOption(DEVICE_STATE_CALLBACK_OPTION, valueIfMissing);
+ }
+
+ /**
+ * Returns the stored {@link CameraCaptureSession.StateCallback}.
+ *
+ * @param valueIfMissing The value to return if this configuration option has not been set.
+ * @return The stored value or <code>valueIfMissing</code> if the value does not exist in this
+ * configuration.
+ */
+ public CameraCaptureSession.StateCallback getSessionStateCallback(
+ CameraCaptureSession.StateCallback valueIfMissing) {
+ return getConfiguration().retrieveOption(SESSION_STATE_CALLBACK_OPTION, valueIfMissing);
+ }
+
+ // Option Declarations:
+ // ***********************************************************************************************
+
+ /**
+ * Returns the stored {@link CameraCaptureSession.CaptureCallback}.
+ *
+ * @param valueIfMissing The value to return if this configuration option has not been set.
+ * @return The stored value or <code>valueIfMissing</code> if the value does not exist in this
+ * configuration.
+ */
+ public CameraCaptureSession.CaptureCallback getSessionCaptureCallback(
+ CameraCaptureSession.CaptureCallback valueIfMissing) {
+ return getConfiguration().retrieveOption(SESSION_CAPTURE_CALLBACK_OPTION, valueIfMissing);
+ }
+
+ /**
+ * Returns the underlying immutable {@link Configuration} object.
+ *
+ * @return The underlying {@link Configuration} object.
+ * @hide
+ */
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ @Override
+ public Configuration getConfiguration() {
+ return config;
+ }
+
+ /** Extends a {@link Configuration.Builder} to add Camera2 options. */
+ public static final class Extender {
+
+ Configuration.Builder<?, ?> baseBuilder;
+
+ /**
+ * Creates an Extender that can be used to add Camera2 options to another Builder.
+ *
+ * @param baseBuilder The builder being extended.
+ */
+ public Extender(Configuration.Builder<?, ?> baseBuilder) {
+ this.baseBuilder = baseBuilder;
+ }
+
+ /**
+ * Sets a {@link CaptureRequest.Key} and Value on the configuration.
+ *
+ * @param key The {@link CaptureRequest.Key} which will be set.
+ * @param value The value for the key.
+ * @param <ValueT> The type of the value.
+ * @return The current Extender.
+ */
+ public <ValueT> Extender setCaptureRequestOption(
+ CaptureRequest.Key<ValueT> key, ValueT value) {
+ // Reify the type so we can obtain the class
+ Option<Object> opt = Camera2Configuration.createCaptureRequestOption(key);
+ baseBuilder.insertOption(opt, value);
+ return this;
+ }
+
+ /**
+ * Sets a CameraDevice template on the given configuration.
+ *
+ * <p>See {@link CameraDevice} for valid template types. For example, {@link
+ * CameraDevice#TEMPLATE_PREVIEW}.
+ *
+ * @param templateType The template type to set.
+ * @return The current Extender.
+ */
+ Extender setCaptureRequestTemplate(int templateType) {
+ baseBuilder.insertOption(TEMPLATE_TYPE_OPTION, templateType);
+ return this;
+ }
+
+ /**
+ * Sets a {@link CameraDevice.StateCallback}.
+ *
+ * <p>The caller is expected to use the {@link CameraDevice} instance accessed through the
+ * callback methods responsibly. Generally safe usages include: (1) querying the device for
+ * its id, (2) using the callbacks to determine what state the device is currently in.
+ * Generally unsafe usages include: (1) creating a new {@link CameraCaptureSession}, (2)
+ * creating a new {@link CaptureRequest}, (3) closing the device. When the caller uses the
+ * device beyond the safe usage limits, the usage may still work in conjunction with
+ * CameraX, but any strong guarantees provided by CameraX about the validity of the camera
+ * state become void.
+ *
+ * @param stateCallback The {@link CameraDevice.StateCallback}.
+ * @return The current Extender.
+ */
+ public Extender setDeviceStateCallback(CameraDevice.StateCallback stateCallback) {
+ baseBuilder.insertOption(DEVICE_STATE_CALLBACK_OPTION, stateCallback);
+ return this;
+ }
+
+ /**
+ * Sets a {@link CameraCaptureSession.StateCallback}.
+ *
+ * <p>The caller is expected to use the {@link CameraCaptureSession} instance accessed
+ * through the callback methods responsibly. Generally safe usages include: (1) querying the
+ * session for its properties, (2) using the callbacks to determine what state the session
+ * is currently in. Generally unsafe usages include: (1) submitting a new {@link
+ * CaptureRequest}, (2) stopping an existing {@link CaptureRequest}, (3) closing the
+ * session, (4) attaching a new {@link Surface} to the session. When the caller uses the
+ * session beyond the safe usage limits, the usage may still work in conjunction with
+ * CameraX, but any strong gurantees provided by CameraX about the validity of the camera
+ * state become void.
+ *
+ * @param stateCallback The {@link CameraCaptureSession.StateCallback}.
+ * @return The current Extender.
+ */
+ public Extender setSessionStateCallback(CameraCaptureSession.StateCallback stateCallback) {
+ baseBuilder.insertOption(SESSION_STATE_CALLBACK_OPTION, stateCallback);
+ return this;
+ }
+
+ /**
+ * Sets a {@link CameraCaptureSession.CaptureCallback}.
+ *
+ * <p>The caller is expected to use the {@link CameraCaptureSession} instance accessed
+ * through the callback methods responsibly. Generally safe usages include: (1) querying the
+ * session for its properties. Generally unsafe usages include: (1) submitting a new {@link
+ * CaptureRequest}, (2) stopping an existing {@link CaptureRequest}, (3) closing the
+ * session, (4) attaching a new {@link Surface} to the session. When the caller uses the
+ * session beyond the safe usage limits, the usage may still work in conjunction with
+ * CameraX, but any strong gurantees provided by CameraX about the validity of the camera
+ * state become void.
+ *
+ * <p>The caller is generally free to use the {@link CaptureRequest} and {@link
+ * CaptureResult} instances accessed through the callback methods.
+ *
+ * @param captureCallback The {@link CameraCaptureSession.CaptureCallback}.
+ * @return The current Extender.
+ */
+ public Extender setSessionCaptureCallback(
+ CameraCaptureSession.CaptureCallback captureCallback) {
+ baseBuilder.insertOption(SESSION_CAPTURE_CALLBACK_OPTION, captureCallback);
+ return this;
+ }
+ }
+
+ /**
+ * Builder for creating {@link Camera2Configuration} instance.
+ *
+ * <p>Use {@link Builder} for creating {@link Configuration} which contains camera2 options
+ * only. And use {@link Extender} to add Camera2 options on existing other {@link
+ * Configuration.Builder}.
+ *
+ * @hide
+ */
+ static final class Builder implements Configuration.Builder<Camera2Configuration, Builder> {
+
+ private final MutableOptionsBundle mutableOptionsBundle = MutableOptionsBundle.create();
+
+ @Override
+ public MutableConfiguration getMutableConfiguration() {
+ return mutableOptionsBundle;
+ }
+
+ @Override
+ public Builder builder() {
+ return this;
+ }
+
+ public <ValueT> Builder setCaptureRequestOption(
+ CaptureRequest.Key<ValueT> key, ValueT value) {
+ Option<Object> opt = Camera2Configuration.createCaptureRequestOption(key);
+ insertOption(opt, value);
+ return this;
+ }
+
+ public Builder insertAllOptions(Configuration configuration) {
+ for (Option<?> option : configuration.listOptions()) {
+ @SuppressWarnings("unchecked") // Options/values are being copied directly
+ Option<Object> objectOpt = (Option<Object>) option;
+ insertOption(objectOpt, configuration.retrieveOption(objectOpt));
+ }
+ return this;
+ }
+
+ @Override
+ public Camera2Configuration build() {
+ return new Camera2Configuration(OptionsBundle.from(mutableOptionsBundle));
+ }
+ }
+}
diff --git a/camera/camera2/src/main/java/androidx/camera/camera2/Camera2DeviceSurfaceManager.java b/camera/camera2/src/main/java/androidx/camera/camera2/Camera2DeviceSurfaceManager.java
new file mode 100644
index 0000000..8e595b1
--- /dev/null
+++ b/camera/camera2/src/main/java/androidx/camera/camera2/Camera2DeviceSurfaceManager.java
@@ -0,0 +1,262 @@
+/*
+ * Copyright (C) 2019 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.camera2;
+
+import android.content.Context;
+import android.hardware.camera2.CameraAccessException;
+import android.hardware.camera2.CameraManager;
+import android.media.CamcorderProfile;
+import android.util.Size;
+
+import androidx.annotation.VisibleForTesting;
+import androidx.camera.core.BaseUseCase;
+import androidx.camera.core.CameraDeviceConfiguration;
+import androidx.camera.core.CameraDeviceSurfaceManager;
+import androidx.camera.core.CameraX;
+import androidx.camera.core.SurfaceConfiguration;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Camera device manager to provide the guaranteed supported stream capabilities related info for
+ * all camera devices
+ *
+ * <p>{@link android.hardware.camera2.CameraDevice#createCaptureSession} defines the default
+ * guaranteed stream combinations for different hardware level devices. It defines what combination
+ * of surface configuration type and size pairs can be supported for different hardware level camera
+ * devices. This structure is used to store the guaranteed supported stream capabilities related
+ * info.
+ *
+ * @hide Implementation detail
+ */
+final class Camera2DeviceSurfaceManager implements CameraDeviceSurfaceManager {
+ private static final String TAG = "Camera2DeviceSurfaceManager";
+ private static final Size MAXIMUM_PREVIEW_SIZE = new Size(1920, 1080);
+ private final Map<String, SupportedSurfaceCombination> cameraSupportedSurfaceCombinationMap =
+ new HashMap<>();
+ private boolean isInitialized = false;
+
+ public Camera2DeviceSurfaceManager(Context context) {
+ init(context, CamcorderProfile::hasProfile);
+ }
+
+ @VisibleForTesting
+ Camera2DeviceSurfaceManager(Context context, CamcorderProfileHelper camcorderProfileHelper) {
+ init(context, camcorderProfileHelper);
+ }
+
+ /**
+ * Check whether the input surface configuration list is under the capability of any combination
+ * of this object.
+ *
+ * @param cameraId the camera id of the camera device to be compared
+ * @param surfaceConfigurationList the surface configuration list to be compared
+ * @return the check result that whether it could be supported
+ */
+ @Override
+ public boolean checkSupported(
+ String cameraId, List<SurfaceConfiguration> surfaceConfigurationList) {
+ boolean isSupported = false;
+
+ if (!isInitialized) {
+ throw new IllegalStateException("Camera2DeviceSurfaceManager is not initialized.");
+ }
+
+ if (surfaceConfigurationList == null || surfaceConfigurationList.isEmpty()) {
+ return true;
+ }
+
+ SupportedSurfaceCombination supportedSurfaceCombination =
+ cameraSupportedSurfaceCombinationMap.get(cameraId);
+
+ if (supportedSurfaceCombination != null) {
+ isSupported = supportedSurfaceCombination.checkSupported(surfaceConfigurationList);
+ }
+
+ return isSupported;
+ }
+
+ /**
+ * Transform to a SurfaceConfiguration object with cameraId, image format and size info
+ *
+ * @param cameraId the camera id of the camera device to transform the object
+ * @param imageFormat the image format info for the surface configuration object
+ * @param size the size info for the surface configuration object
+ * @return new {@link SurfaceConfiguration} object
+ */
+ @Override
+ public SurfaceConfiguration transformSurfaceConfiguration(
+ String cameraId, int imageFormat, Size size) {
+ SurfaceConfiguration surfaceConfiguration = null;
+
+ if (!isInitialized) {
+ throw new IllegalStateException("Camera2DeviceSurfaceManager is not initialized.");
+ }
+
+ SupportedSurfaceCombination supportedSurfaceCombination =
+ cameraSupportedSurfaceCombinationMap.get(cameraId);
+
+ if (supportedSurfaceCombination != null) {
+ surfaceConfiguration =
+ supportedSurfaceCombination.transformSurfaceConfiguration(imageFormat, size);
+ }
+
+ return surfaceConfiguration;
+ }
+
+ /**
+ * Retrieves a map of suggested resolutions for the given list of use cases.
+ *
+ * @param cameraId the camera id of the camera device used by the use cases
+ * @param originalUseCases list of use cases with existing surfaces
+ * @param newUseCases list of new use cases
+ * @return map of suggested resolutions for given use cases
+ */
+ @Override
+ public Map<BaseUseCase, Size> getSuggestedResolutions(
+ String cameraId, List<BaseUseCase> originalUseCases, List<BaseUseCase> newUseCases) {
+
+ if (newUseCases == null || newUseCases.isEmpty()) {
+ throw new IllegalArgumentException("No new use cases to be bound.");
+ }
+
+ UseCaseSurfaceOccupancyManager.checkUseCaseLimitNotExceeded(originalUseCases, newUseCases);
+
+ // Use the small size (640x480) for new use cases to check whether there is any possible
+ // supported combination first
+ List<SurfaceConfiguration> surfaceConfigurations = new ArrayList<>();
+
+ if (originalUseCases != null) {
+ for (BaseUseCase useCase : originalUseCases) {
+ CameraDeviceConfiguration configuration =
+ (CameraDeviceConfiguration) useCase.getUseCaseConfiguration();
+ String useCaseCameraId;
+ try {
+ useCaseCameraId =
+ CameraX.getCameraWithLensFacing(configuration.getLensFacing());
+ } catch (Exception e) {
+ throw new IllegalArgumentException(
+ "Unable to get camera ID for use case " + useCase.getName(), e);
+ }
+ Size resolution = useCase.getAttachedSurfaceResolution(useCaseCameraId);
+
+ surfaceConfigurations.add(
+ transformSurfaceConfiguration(
+ cameraId, useCase.getImageFormat(), resolution));
+ }
+ }
+
+ for (BaseUseCase useCase : newUseCases) {
+ surfaceConfigurations.add(
+ transformSurfaceConfiguration(
+ cameraId, useCase.getImageFormat(), new Size(640, 480)));
+ }
+
+ SupportedSurfaceCombination supportedSurfaceCombination =
+ cameraSupportedSurfaceCombinationMap.get(cameraId);
+
+ if (supportedSurfaceCombination == null
+ || !supportedSurfaceCombination.checkSupported(surfaceConfigurations)) {
+ throw new IllegalArgumentException(
+ "No supported surface combination is found for camera device - Id : "
+ + cameraId);
+ }
+
+ return supportedSurfaceCombination.getSuggestedResolutions(originalUseCases, newUseCases);
+ }
+
+ private void init(Context context, CamcorderProfileHelper camcorderProfileHelper) {
+ if (!isInitialized) {
+ CameraManager cameraManager =
+ (CameraManager) context.getSystemService(Context.CAMERA_SERVICE);
+
+ try {
+ for (String cameraId : cameraManager.getCameraIdList()) {
+ cameraSupportedSurfaceCombinationMap.put(
+ cameraId,
+ new SupportedSurfaceCombination(
+ context, cameraId, camcorderProfileHelper));
+ }
+ } catch (CameraAccessException e) {
+ throw new IllegalArgumentException("Fail to get camera id list", e);
+ }
+
+ isInitialized = true;
+ }
+ }
+
+ /**
+ * Get max supported output size for specific camera device and image format
+ *
+ * @param cameraId the camera Id
+ * @param imageFormat the image format info
+ * @return the max supported output size for the image format
+ */
+ @Override
+ public Size getMaxOutputSize(String cameraId, int imageFormat) {
+ if (!isInitialized) {
+ throw new IllegalStateException("CameraDeviceSurfaceManager is not initialized.");
+ }
+
+ SupportedSurfaceCombination supportedSurfaceCombination =
+ cameraSupportedSurfaceCombinationMap.get(cameraId);
+
+ if (supportedSurfaceCombination == null) {
+ throw new IllegalArgumentException(
+ "Fail to find supported surface info - CameraId:" + cameraId);
+ }
+
+ return supportedSurfaceCombination.getMaxOutputSizeByFormat(imageFormat);
+ }
+
+ /**
+ * Retrieves the preview size, choosing the smaller of the display size and 1080P.
+ *
+ * @return preview size from {@link androidx.camera.core.SurfaceSizeDefinition}
+ */
+ @Override
+ public Size getPreviewSize() {
+ if (!isInitialized) {
+ throw new IllegalStateException("CameraDeviceSurfaceManager is not initialized.");
+ }
+
+ // 1920x1080 is maximum preview size
+ Size previewSize = MAXIMUM_PREVIEW_SIZE;
+
+ if (!cameraSupportedSurfaceCombinationMap.isEmpty()) {
+ // Preview size depends on the display size and 1080P. Therefore, we can get the first
+ // camera
+ // device's preview size to return it.
+ String cameraId = (String) cameraSupportedSurfaceCombinationMap.keySet().toArray()[0];
+ previewSize =
+ cameraSupportedSurfaceCombinationMap
+ .get(cameraId)
+ .getSurfaceSizeDefinition()
+ .getPreviewSize();
+ }
+
+ return previewSize;
+ }
+
+ enum Operation {
+ ADD_CONFIG,
+ REMOVE_CONFIG
+ }
+}
diff --git a/camera/camera2/src/main/java/androidx/camera/camera2/Camera2Initializer.java b/camera/camera2/src/main/java/androidx/camera/camera2/Camera2Initializer.java
new file mode 100644
index 0000000..dc37f7e
--- /dev/null
+++ b/camera/camera2/src/main/java/androidx/camera/camera2/Camera2Initializer.java
@@ -0,0 +1,83 @@
+/*
+ * Copyright (C) 2019 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.camera2;
+
+import android.content.ContentProvider;
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.Cursor;
+import android.net.Uri;
+import android.util.Log;
+
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.camera.core.CameraX;
+
+/**
+ * A {@link ContentProvider} used to initialize {@link CameraX} from a {@link Context}.
+ *
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public final class Camera2Initializer extends ContentProvider {
+ private static final String TAG = "Camera2Initializer";
+
+ @Override
+ public boolean onCreate() {
+ Log.d(TAG, "CameraX initializing with Camera2 ...");
+
+ CameraX.init(getContext(), Camera2AppConfiguration.create(getContext()));
+ return false;
+ }
+
+ @Nullable
+ @Override
+ public Cursor query(
+ Uri uri,
+ @Nullable String[] strings,
+ @Nullable String s,
+ @Nullable String[] strings1,
+ @Nullable String s1) {
+ return null;
+ }
+
+ @Nullable
+ @Override
+ public String getType(Uri uri) {
+ return null;
+ }
+
+ @Nullable
+ @Override
+ public Uri insert(Uri uri, @Nullable ContentValues contentValues) {
+ return null;
+ }
+
+ @Override
+ public int delete(Uri uri, @Nullable String s, @Nullable String[] strings) {
+ return 0;
+ }
+
+ @Override
+ public int update(
+ Uri uri,
+ @Nullable ContentValues contentValues,
+ @Nullable String s,
+ @Nullable String[] strings) {
+ return 0;
+ }
+}
diff --git a/camera/camera2/src/main/java/androidx/camera/camera2/Camera2OptionUnpacker.java b/camera/camera2/src/main/java/androidx/camera/camera2/Camera2OptionUnpacker.java
new file mode 100644
index 0000000..e8df444
--- /dev/null
+++ b/camera/camera2/src/main/java/androidx/camera/camera2/Camera2OptionUnpacker.java
@@ -0,0 +1,114 @@
+/*
+ * Copyright (C) 2019 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.camera2;
+
+import android.hardware.camera2.CameraCaptureSession;
+import android.hardware.camera2.CameraDevice;
+import android.hardware.camera2.CaptureRequest;
+
+import androidx.camera.core.CameraCaptureCallback;
+import androidx.camera.core.CameraCaptureCallbacks;
+import androidx.camera.core.CameraCaptureSessionStateCallbacks;
+import androidx.camera.core.CameraDeviceStateCallbacks;
+import androidx.camera.core.Configuration;
+import androidx.camera.core.Configuration.Option;
+import androidx.camera.core.OptionsBundle;
+import androidx.camera.core.SessionConfiguration;
+import androidx.camera.core.UseCaseConfiguration;
+
+/**
+ * A {@link SessionConfiguration.OptionUnpacker} implementation for unpacking Camera2 options into a
+ * {@link SessionConfiguration.Builder}.
+ */
+final class Camera2OptionUnpacker implements SessionConfiguration.OptionUnpacker {
+
+ static final Camera2OptionUnpacker INSTANCE = new Camera2OptionUnpacker();
+
+ @Override
+ public void unpack(UseCaseConfiguration<?> config, SessionConfiguration.Builder builder) {
+ SessionConfiguration defaultSessionConfig =
+ config.getDefaultSessionConfiguration(/*valueIfMissing=*/ null);
+
+ CameraDevice.StateCallback deviceStateCallback =
+ CameraDeviceStateCallbacks.createNoOpCallback();
+ CameraCaptureSession.StateCallback sessionStateCallback =
+ CameraCaptureSessionStateCallbacks.createNoOpCallback();
+ CameraCaptureCallback cameraCaptureCallback = CameraCaptureCallbacks.createNoOpCallback();
+ Configuration implOptions = OptionsBundle.emptyBundle();
+ int templateType =
+ SessionConfiguration.defaultEmptySessionConfiguration().getTemplateType();
+
+ // Apply/extract defaults from session config
+ if (defaultSessionConfig != null) {
+ templateType = defaultSessionConfig.getTemplateType();
+ deviceStateCallback = defaultSessionConfig.getDeviceStateCallback();
+ sessionStateCallback = defaultSessionConfig.getSessionStateCallback();
+ cameraCaptureCallback = defaultSessionConfig.getCameraCaptureCallback();
+ implOptions = defaultSessionConfig.getImplementationOptions();
+
+ // Add all default camera characteristics
+ builder.addCharacteristics(defaultSessionConfig.getCameraCharacteristics());
+ }
+
+ // Set the any additional implementation options
+ builder.setImplementationOptions(implOptions);
+
+ // Get Camera2 extended options
+ Camera2Configuration camera2Config = new Camera2Configuration(config);
+
+ // Apply template type
+ builder.setTemplateType(camera2Config.getCaptureRequestTemplate(templateType));
+
+ // Combine default config callbacks with extension callbacks
+ deviceStateCallback =
+ CameraDeviceStateCallbacks.createComboCallback(
+ deviceStateCallback,
+ camera2Config.getDeviceStateCallback(
+ CameraDeviceStateCallbacks.createNoOpCallback()));
+ sessionStateCallback =
+ CameraCaptureSessionStateCallbacks.createComboCallback(
+ sessionStateCallback,
+ camera2Config.getSessionStateCallback(
+ CameraCaptureSessionStateCallbacks.createNoOpCallback()));
+ cameraCaptureCallback =
+ CameraCaptureCallbacks.createComboCallback(
+ cameraCaptureCallback,
+ CaptureCallbackContainer.create(
+ camera2Config.getSessionCaptureCallback(
+ Camera2CaptureSessionCaptureCallbacks
+ .createNoOpCallback())));
+
+ // Apply state callbacks
+ builder.setDeviceStateCallback(deviceStateCallback);
+ builder.setSessionStateCallback(sessionStateCallback);
+ builder.setCameraCaptureCallback(cameraCaptureCallback);
+
+ // Copy extension keys
+ camera2Config.findOptions(
+ Camera2Configuration.CAPTURE_REQUEST_ID_STEM,
+ option -> {
+ @SuppressWarnings(
+ "unchecked") // No way to get actual type info here, so treat as Object
+ Option<Object> typeErasedOption = (Option<Object>) option;
+ @SuppressWarnings("unchecked")
+ CaptureRequest.Key<Object> key = (CaptureRequest.Key<Object>) option.getToken();
+
+ builder.addCharacteristic(key, camera2Config.retrieveOption(typeErasedOption));
+ return true;
+ });
+ }
+}
diff --git a/camera/camera2/src/main/java/androidx/camera/camera2/Camera2RequestRunner.java b/camera/camera2/src/main/java/androidx/camera/camera2/Camera2RequestRunner.java
new file mode 100644
index 0000000..d44d9af
--- /dev/null
+++ b/camera/camera2/src/main/java/androidx/camera/camera2/Camera2RequestRunner.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2019 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.camera2;
+
+import android.graphics.Rect;
+import android.hardware.camera2.CaptureRequest;
+
+import androidx.camera.core.CameraControl;
+import androidx.camera.core.CaptureRequestConfiguration;
+
+/**
+ * A interface for executing camera2 capture requests which is required for {@link
+ * Camera2CameraControl} to achieve its functionality.
+ *
+ * <p>{@link Camera} implements this interface so Camera2CameraControl can issue {@link
+ * CaptureRequest} for manipulating the camera. Camera2CameraControl can use it to execute single
+ * request and re-send the repeating request with updated Control SessionConfiguration. For example,
+ * {@link CameraControl#focus(Rect, Rect)} needs to send a single request to trigger AF as well as
+ * resend the repeating request with updated focus area.
+ *
+ * @hide
+ */
+public interface Camera2RequestRunner {
+
+ /**
+ * Executes a single capture request.
+ *
+ * <p>CameraControl methods like focus, trigger AF need to send single request.
+ */
+ void submitSingleRequest(CaptureRequestConfiguration singleRequestConfig);
+
+ /**
+ * Re-sends the repeating request which contains the latest settings specified by {@link
+ * CameraControl}.
+ *
+ * <p>CameraControl methods like setCropRegion, zoom, focus need to update repeating request.
+ */
+ void updateRepeatingRequest();
+}
diff --git a/camera/camera2/src/main/java/androidx/camera/camera2/CameraCaptureCallbackAdapter.java b/camera/camera2/src/main/java/androidx/camera/camera2/CameraCaptureCallbackAdapter.java
new file mode 100644
index 0000000..8f60884
--- /dev/null
+++ b/camera/camera2/src/main/java/androidx/camera/camera2/CameraCaptureCallbackAdapter.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2019 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.camera2;
+
+import android.hardware.camera2.CameraCaptureSession;
+import android.hardware.camera2.CaptureFailure;
+import android.hardware.camera2.CaptureRequest;
+import android.hardware.camera2.TotalCaptureResult;
+
+import androidx.annotation.NonNull;
+import androidx.camera.core.CameraCaptureCallback;
+import androidx.camera.core.CameraCaptureFailure;
+
+/**
+ * An adapter that passes {@link CameraCaptureSession.CaptureCallback} to {@link
+ * CameraCaptureCallback}.
+ */
+final class CameraCaptureCallbackAdapter extends CameraCaptureSession.CaptureCallback {
+
+ private final CameraCaptureCallback cameraCaptureCallback;
+
+ CameraCaptureCallbackAdapter(CameraCaptureCallback cameraCaptureCallback) {
+ if (cameraCaptureCallback == null) {
+ throw new NullPointerException("cameraCaptureCallback is null");
+ }
+ this.cameraCaptureCallback = cameraCaptureCallback;
+ }
+
+ @Override
+ public void onCaptureCompleted(
+ @NonNull CameraCaptureSession session,
+ @NonNull CaptureRequest request,
+ @NonNull TotalCaptureResult result) {
+ super.onCaptureCompleted(session, request, result);
+
+ cameraCaptureCallback.onCaptureCompleted(new Camera2CameraCaptureResult(result));
+ }
+
+ @Override
+ public void onCaptureFailed(
+ @NonNull CameraCaptureSession session,
+ @NonNull CaptureRequest request,
+ @NonNull CaptureFailure failure) {
+ super.onCaptureFailed(session, request, failure);
+
+ CameraCaptureFailure cameraFailure =
+ new CameraCaptureFailure(CameraCaptureFailure.Reason.ERROR);
+
+ cameraCaptureCallback.onCaptureFailed(cameraFailure);
+ }
+}
diff --git a/camera/camera2/src/main/java/androidx/camera/camera2/CaptureCallbackContainer.java b/camera/camera2/src/main/java/androidx/camera/camera2/CaptureCallbackContainer.java
new file mode 100644
index 0000000..a046d03
--- /dev/null
+++ b/camera/camera2/src/main/java/androidx/camera/camera2/CaptureCallbackContainer.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2019 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.camera2;
+
+import android.hardware.camera2.CameraCaptureSession.CaptureCallback;
+
+import androidx.annotation.NonNull;
+import androidx.camera.core.CameraCaptureCallback;
+
+/**
+ * A {@link CameraCaptureCallback} which contains an {@link CaptureCallback} and doesn't handle the
+ * callback.
+ */
+final class CaptureCallbackContainer extends CameraCaptureCallback {
+
+ private final CaptureCallback captureCallback;
+
+ private CaptureCallbackContainer(CaptureCallback captureCallback) {
+ if (captureCallback == null) {
+ throw new NullPointerException("captureCallback is null");
+ }
+ this.captureCallback = captureCallback;
+ }
+
+ static CaptureCallbackContainer create(CaptureCallback captureCallback) {
+ return new CaptureCallbackContainer(captureCallback);
+ }
+
+ @NonNull
+ CaptureCallback getCaptureCallback() {
+ return captureCallback;
+ }
+}
diff --git a/camera/camera2/src/main/java/androidx/camera/camera2/CaptureCallbackConverter.java b/camera/camera2/src/main/java/androidx/camera/camera2/CaptureCallbackConverter.java
new file mode 100644
index 0000000..fef48118
--- /dev/null
+++ b/camera/camera2/src/main/java/androidx/camera/camera2/CaptureCallbackConverter.java
@@ -0,0 +1,80 @@
+/*
+ * Copyright (C) 2019 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.camera2;
+
+import android.hardware.camera2.CameraCaptureSession.CaptureCallback;
+
+import androidx.camera.core.CameraCaptureCallback;
+import androidx.camera.core.CameraCaptureCallbacks.ComboCameraCaptureCallback;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/** An utility class to convert {@link CameraCaptureCallback} to camera2 {@link CaptureCallback}. */
+final class CaptureCallbackConverter {
+
+ private CaptureCallbackConverter() {
+ }
+
+ /**
+ * Converts {@link CameraCaptureCallback} to {@link CaptureCallback}.
+ *
+ * @param cameraCaptureCallback The camera capture callback.
+ * @return The capture session callback.
+ */
+ static CaptureCallback toCaptureCallback(CameraCaptureCallback cameraCaptureCallback) {
+ if (cameraCaptureCallback == null) {
+ return null;
+ }
+ List<CaptureCallback> list = new ArrayList<>();
+ toCaptureCallback(cameraCaptureCallback, list);
+ return list.size() == 1
+ ? list.get(0)
+ : Camera2CaptureSessionCaptureCallbacks.createComboCallback(list);
+ }
+
+ /**
+ * Converts {@link CameraCaptureCallback} to one or more {@link CaptureCallback} and put them
+ * into the input capture callback list.
+ *
+ * <p>There are several known types of {@link CameraCaptureCallback}s. Convert the callback
+ * according to the corresponding rule.
+ *
+ * @param cameraCaptureCallback The camera capture callback.
+ * @param captureCallbackList The output capture session callback list.
+ */
+ static void toCaptureCallback(
+ CameraCaptureCallback cameraCaptureCallback,
+ List<CaptureCallback> captureCallbackList) {
+ if (cameraCaptureCallback instanceof ComboCameraCaptureCallback) {
+ // Recursively convert callback inside the combo callback.
+ ComboCameraCaptureCallback comboCallback =
+ (ComboCameraCaptureCallback) cameraCaptureCallback;
+ for (CameraCaptureCallback callback : comboCallback.getCallbacks()) {
+ toCaptureCallback(callback, captureCallbackList);
+ }
+ } else if (cameraCaptureCallback instanceof CaptureCallbackContainer) {
+ // Get the actual callback inside the CaptureCallbackContainer.
+ CaptureCallbackContainer callbackContainer =
+ (CaptureCallbackContainer) cameraCaptureCallback;
+ captureCallbackList.add(callbackContainer.getCaptureCallback());
+ } else {
+ // Create a CameraCaptureCallbackAdapter.
+ captureCallbackList.add(new CameraCaptureCallbackAdapter(cameraCaptureCallback));
+ }
+ }
+}
diff --git a/camera/camera2/src/main/java/androidx/camera/camera2/CaptureSession.java b/camera/camera2/src/main/java/androidx/camera/camera2/CaptureSession.java
new file mode 100644
index 0000000..f313319
--- /dev/null
+++ b/camera/camera2/src/main/java/androidx/camera/camera2/CaptureSession.java
@@ -0,0 +1,517 @@
+/*
+ * Copyright (C) 2019 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.camera2;
+
+import android.hardware.camera2.CameraAccessException;
+import android.hardware.camera2.CameraCaptureSession;
+import android.hardware.camera2.CameraCaptureSession.CaptureCallback;
+import android.hardware.camera2.CameraDevice;
+import android.hardware.camera2.CaptureRequest;
+import android.hardware.camera2.TotalCaptureResult;
+import android.os.Handler;
+import android.util.Log;
+import android.view.Surface;
+
+import androidx.annotation.GuardedBy;
+import androidx.annotation.Nullable;
+import androidx.camera.core.CameraCaptureSessionStateCallbacks;
+import androidx.camera.core.CaptureRequestConfiguration;
+import androidx.camera.core.Configuration;
+import androidx.camera.core.Configuration.Option;
+import androidx.camera.core.DeferrableSurfaces;
+import androidx.camera.core.SessionConfiguration;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * A session for capturing images from the camera which is tied to a specific {@link CameraDevice}.
+ *
+ * <p>A session can only be opened a single time. Once has {@link CaptureSession#close()} been
+ * called then it is permanently closed so a new session has to be created for capturing images.
+ */
+final class CaptureSession {
+ private static final String TAG = "CaptureSession";
+
+ /** Handler for all the callbacks from the {@link CameraCaptureSession}. */
+ @Nullable
+ private final Handler handler;
+ /** The configuration for the currently issued single capture requests. */
+ private final List<CaptureRequestConfiguration> captureRequestConfigurations =
+ new ArrayList<>();
+ /** Lock on whether the camera is open or closed. */
+ final Object stateLock = new Object();
+ /** Callback for handling image captures. */
+ private final CameraCaptureSession.CaptureCallback captureCallback =
+ new CaptureCallback() {
+ @Override
+ public void onCaptureCompleted(
+ CameraCaptureSession session,
+ CaptureRequest request,
+ TotalCaptureResult result) {
+ }
+ };
+ private final StateCallback captureSessionStateCallback = new StateCallback();
+ /** The framework camera capture session held by this session. */
+ @Nullable
+ CameraCaptureSession cameraCaptureSession;
+ /** The configuration for the currently issued capture requests. */
+ private volatile SessionConfiguration sessionConfiguration =
+ SessionConfiguration.defaultEmptySessionConfiguration();
+ /** The list of surfaces used to configure the current capture session. */
+ private List<Surface> configuredSurfaces = Collections.emptyList();
+ /** Tracks the current state of the session. */
+ @GuardedBy("stateLock")
+ State state = State.UNINITIALIZED;
+
+ /**
+ * Constructor for CaptureSession.
+ *
+ * @param handler The handler is responsible for queuing up callbacks from capture requests. If
+ * this is null then when asynchronous methods are called on this session they
+ * will attempt
+ * to use the current thread's looper.
+ */
+ CaptureSession(@Nullable Handler handler) {
+ this.handler = handler;
+ state = State.INITIALIZED;
+ }
+
+ /** Returns the configurations of the capture session. */
+ SessionConfiguration getSessionConfiguration() {
+ synchronized (stateLock) {
+ return sessionConfiguration;
+ }
+ }
+
+ /**
+ * Sets the active configurations for the capture session.
+ *
+ * <p>Once both the session configuration has been set and the session has been opened, then the
+ * capture requests will immediately be issued.
+ *
+ * @param sessionConfiguration has the configuration that will currently active in issuing
+ * capture request. The surfaces contained in this must be a
+ * subset of the surfaces that
+ * were used to open this capture session.
+ */
+ void setSessionConfiguration(SessionConfiguration sessionConfiguration) {
+ synchronized (stateLock) {
+ switch (state) {
+ case UNINITIALIZED:
+ throw new IllegalStateException(
+ "setSessionConfiguration() should not be possible in state: " + state);
+ case INITIALIZED:
+ case OPENING:
+ this.sessionConfiguration = sessionConfiguration;
+ break;
+ case OPENED:
+ this.sessionConfiguration = sessionConfiguration;
+
+ if (!configuredSurfaces.containsAll(
+ DeferrableSurfaces.surfaceList(sessionConfiguration.getSurfaces()))) {
+ Log.e(TAG, "Does not have the proper configured lists");
+ return;
+ }
+
+ Log.d(TAG, "Attempting to submit CaptureRequest after setting");
+ issueRepeatingCaptureRequests();
+ break;
+ case CLOSED:
+ case RELEASING:
+ case RELEASED:
+ throw new IllegalStateException(
+ "Session configuration cannot be set on a closed/released session.");
+ }
+ }
+ }
+
+ /**
+ * Opens the capture session synchronously.
+ *
+ * <p>When the session is opened and the configurations have been set then the capture requests
+ * will be issued.
+ *
+ * @param sessionConfiguration which is used to configure the camera capture session. This
+ * contains configurations which may or may not be currently
+ * active in issuing capture
+ * requests.
+ * @param cameraDevice the camera with which to generate the capture session
+ * @throws CameraAccessException if the camera is in an invalid start state
+ */
+ void open(SessionConfiguration sessionConfiguration, CameraDevice cameraDevice)
+ throws CameraAccessException {
+ synchronized (stateLock) {
+ switch (state) {
+ case UNINITIALIZED:
+ throw new IllegalStateException(
+ "open() should not be possible in state: " + state);
+ case INITIALIZED:
+ configuredSurfaces =
+ new ArrayList<>(
+ DeferrableSurfaces.surfaceSet(
+ sessionConfiguration.getSurfaces()));
+ if (configuredSurfaces.isEmpty()) {
+ Log.e(TAG, "Unable to open capture session with no surfaces. ");
+ return;
+ }
+
+ state = State.OPENING;
+ Log.d(TAG, "Opening capture session.");
+ CameraCaptureSession.StateCallback comboCallback =
+ CameraCaptureSessionStateCallbacks.createComboCallback(
+ captureSessionStateCallback,
+ sessionConfiguration.getSessionStateCallback());
+ cameraDevice.createCaptureSession(configuredSurfaces, comboCallback, handler);
+ break;
+ default:
+ Log.e(TAG, "Open not allowed in state: " + state);
+ }
+ }
+ }
+
+ /**
+ * Closes the capture session.
+ *
+ * <p>Close needs be called on a session in order to safely open another session. However, this
+ * stops minimal resources so that another session can be quickly opened.
+ *
+ * <p>Once a session is closed it can no longer be opened again. After the session is closed all
+ * method calls on it do nothing.
+ */
+ void close() {
+ synchronized (stateLock) {
+ switch (state) {
+ case UNINITIALIZED:
+ throw new IllegalStateException(
+ "close() should not be possible in state: " + state);
+ case INITIALIZED:
+ state = State.RELEASED;
+ break;
+ case OPENING:
+ case OPENED:
+ state = State.CLOSED;
+ break;
+ case CLOSED:
+ case RELEASING:
+ case RELEASED:
+ break;
+ }
+ }
+ }
+
+ /**
+ * Releases the capture session.
+ *
+ * <p>This releases all of the sessions resources and should be called when ready to close the
+ * camera.
+ *
+ * <p>Once a session is released it can no longer be opened again. After the session is released
+ * all method calls on it do nothing.
+ */
+ void release() {
+ synchronized (stateLock) {
+ switch (state) {
+ case UNINITIALIZED:
+ throw new IllegalStateException(
+ "release() should not be possible in state: " + state);
+ case INITIALIZED:
+ state = State.RELEASED;
+ break;
+ case OPENING:
+ state = State.RELEASING;
+ break;
+ case OPENED:
+ case CLOSED:
+ cameraCaptureSession.close();
+ state = State.RELEASING;
+ break;
+ case RELEASING:
+ case RELEASED:
+ }
+ }
+ }
+
+ /**
+ * Issues a single capture request.
+ *
+ * @param captureRequestConfiguration which is used to construct a {@link CaptureRequest}.
+ */
+ void issueSingleCaptureRequest(CaptureRequestConfiguration captureRequestConfiguration) {
+ issueSingleCaptureRequests(Collections.singletonList(captureRequestConfiguration));
+ }
+
+ /**
+ * Issues single capture requests.
+ *
+ * @param captureRequestConfigurations which is used to construct {@link CaptureRequest}.
+ */
+ void issueSingleCaptureRequests(
+ List<CaptureRequestConfiguration> captureRequestConfigurations) {
+ synchronized (stateLock) {
+ switch (state) {
+ case UNINITIALIZED:
+ throw new IllegalStateException(
+ "issueSingleCaptureRequests() should not be possible in state: "
+ + state);
+ case INITIALIZED:
+ case OPENING:
+ Log.d(TAG, "issueSingleCaptureRequests() before capture session opened.");
+ this.captureRequestConfigurations.addAll(captureRequestConfigurations);
+ break;
+ case OPENED:
+ this.captureRequestConfigurations.addAll(captureRequestConfigurations);
+ issueCaptureRequests();
+ break;
+ case CLOSED:
+ case RELEASING:
+ case RELEASED:
+ throw new IllegalStateException(
+ "Cannot issue capture request on a closed/released session.");
+ }
+ }
+ }
+
+ /** Returns the configurations of the capture requests. */
+ List<CaptureRequestConfiguration> getCaptureRequestConfigurations() {
+ synchronized (stateLock) {
+ return Collections.unmodifiableList(captureRequestConfigurations);
+ }
+ }
+
+ /** Returns the current state of the session. */
+ State getState() {
+ synchronized (stateLock) {
+ return state;
+ }
+ }
+
+ /**
+ * Sets the {@link CaptureRequest} so that the camera will start producing data.
+ *
+ * <p>Will skip setting requests if there are no surfaces since it is illegal to do so.
+ */
+ void issueRepeatingCaptureRequests() {
+ CaptureRequestConfiguration captureRequestConfiguration =
+ sessionConfiguration.getCaptureRequestConfiguration();
+
+ try {
+ Log.d(TAG, "Issuing request for session.");
+ CaptureRequest.Builder builder =
+ captureRequestConfiguration.buildCaptureRequest(
+ cameraCaptureSession.getDevice());
+ if (builder == null) {
+ Log.d(TAG, "Skipping issuing empty request for session.");
+ return;
+ }
+
+ applyImplementationOptionTCaptureBuilder(
+ builder, captureRequestConfiguration.getImplementationOptions());
+
+ CameraCaptureSession.CaptureCallback comboCaptureCallback =
+ Camera2CaptureSessionCaptureCallbacks.createComboCallback(
+ captureCallback,
+ CaptureCallbackConverter.toCaptureCallback(
+ captureRequestConfiguration.getCameraCaptureCallback()));
+ cameraCaptureSession.setRepeatingRequest(
+ builder.build(), comboCaptureCallback, handler);
+ } catch (CameraAccessException e) {
+ Log.e(TAG, "Unable to access camera: " + e.getMessage());
+ Thread.dumpStack();
+ }
+ }
+
+ private void applyImplementationOptionTCaptureBuilder(
+ CaptureRequest.Builder builder, Configuration configuration) {
+ Camera2Configuration camera2Config = new Camera2Configuration(configuration);
+ for (Option<?> option : camera2Config.getCaptureRequestOptions()) {
+ /* Although type is erased below, it is safe to pass it to CaptureRequest.Builder
+ because
+ these option are created via Camera2Configuration.Extender.setCaptureRequestOption
+ (CaptureRequest.Key<ValueT> key, ValueT value) and hence the type compatibility of
+ key and
+ value are ensured by the compiler. */
+ @SuppressWarnings("unchecked")
+ Option<Object> typeErasedOption = (Option<Object>) option;
+ @SuppressWarnings("unchecked")
+ CaptureRequest.Key<Object> key = (CaptureRequest.Key<Object>) option.getToken();
+ builder.set(key, camera2Config.retrieveOption(typeErasedOption));
+ }
+ }
+
+ /** Issues captureRequestConfigurations to {@link CameraCaptureSession}. */
+ void issueCaptureRequests() {
+ if (captureRequestConfigurations.isEmpty()) {
+ return;
+ }
+
+ for (CaptureRequestConfiguration captureRequestConfiguration :
+ captureRequestConfigurations) {
+ if (captureRequestConfiguration.getSurfaces().isEmpty()) {
+ Log.d(TAG, "Skipping issuing empty capture request.");
+ continue;
+ }
+ try {
+ Log.d(TAG, "Issuing capture request.");
+ CaptureRequest.Builder builder =
+ captureRequestConfiguration.buildCaptureRequest(
+ cameraCaptureSession.getDevice());
+
+ applyImplementationOptionTCaptureBuilder(
+ builder, captureRequestConfiguration.getImplementationOptions());
+
+ cameraCaptureSession.capture(
+ builder.build(),
+ CaptureCallbackConverter.toCaptureCallback(
+ captureRequestConfiguration.getCameraCaptureCallback()),
+ handler);
+ } catch (CameraAccessException e) {
+ Log.e(TAG, "Unable to access camera: " + e.getMessage());
+ Thread.dumpStack();
+ }
+ }
+ captureRequestConfigurations.clear();
+ }
+
+ enum State {
+ /** The default state of the session before construction. */
+ UNINITIALIZED,
+ /**
+ * Stable state once the session has been constructed, but prior to the {@link
+ * CameraCaptureSession} being opened.
+ */
+ INITIALIZED,
+ /**
+ * Transitional state when the {@link CameraCaptureSession} is in the process of being
+ * opened.
+ */
+ OPENING,
+ /**
+ * Stable state where the {@link CameraCaptureSession} has been successfully opened. During
+ * this state if a valid {@link SessionConfiguration} has been set then the {@link
+ * CaptureRequest} will be issued.
+ */
+ OPENED,
+ /**
+ * Stable state where the session has been closed. However the {@link CameraCaptureSession}
+ * is still valid. It will remain valid until a new instance is opened at which point {@link
+ * CameraCaptureSession.StateCallback#onClosed(CameraCaptureSession)} will be called to do
+ * final cleanup.
+ */
+ CLOSED,
+ /** Transitional state where the resources are being cleaned up. */
+ RELEASING,
+ /**
+ * Terminal state where the session has been cleaned up. At this point the session should
+ * not be used as nothing will happen in this state.
+ */
+ RELEASED
+ }
+
+ /**
+ * Callback for handling state changes to the {@link CameraCaptureSession}.
+ *
+ * <p>State changes are ignored once the CaptureSession has been closed.
+ */
+ final class StateCallback extends CameraCaptureSession.StateCallback {
+ /**
+ * {@inheritDoc}
+ *
+ * <p>Once the {@link CameraCaptureSession} has been configured then the capture request
+ * will be immediately issued.
+ */
+ @Override
+ public void onConfigured(CameraCaptureSession session) {
+ synchronized (stateLock) {
+ switch (state) {
+ case UNINITIALIZED:
+ case INITIALIZED:
+ case OPENED:
+ case RELEASED:
+ throw new IllegalStateException(
+ "onConfigured() should not be possible in state: " + state);
+ case OPENING:
+ state = State.OPENED;
+ cameraCaptureSession = session;
+ Log.d(TAG, "Attempting to send capture request onConfigured");
+ issueRepeatingCaptureRequests();
+ issueCaptureRequests();
+ break;
+ case CLOSED:
+ cameraCaptureSession = session;
+ break;
+ case RELEASING:
+ session.close();
+ break;
+ }
+ Log.d(TAG, "CameraCaptureSession.onConfigured()");
+ }
+ }
+
+ @Override
+ public void onReady(CameraCaptureSession session) {
+ synchronized (stateLock) {
+ switch (state) {
+ case UNINITIALIZED:
+ throw new IllegalStateException(
+ "onReady() should not be possible in state: " + state);
+ default:
+ }
+ Log.d(TAG, "CameraCaptureSession.onReady()");
+ }
+ }
+
+ @Override
+ public void onClosed(CameraCaptureSession session) {
+ synchronized (stateLock) {
+ switch (state) {
+ case UNINITIALIZED:
+ throw new IllegalStateException(
+ "onClosed() should not be possible in state: " + state);
+ default:
+ state = State.RELEASED;
+ cameraCaptureSession = null;
+ }
+ Log.d(TAG, "CameraCaptureSession.onClosed()");
+ }
+ }
+
+ @Override
+ public void onConfigureFailed(CameraCaptureSession session) {
+ synchronized (stateLock) {
+ switch (state) {
+ case UNINITIALIZED:
+ case INITIALIZED:
+ case OPENED:
+ case RELEASED:
+ throw new IllegalStateException(
+ "onConfiguredFailed() should not be possible in state: " + state);
+ case OPENING:
+ case CLOSED:
+ state = State.CLOSED;
+ cameraCaptureSession = session;
+ break;
+ case RELEASING:
+ state = State.RELEASING;
+ session.close();
+ }
+ Log.e(TAG, "CameraCaptureSession.onConfiguredFailed()");
+ }
+ }
+ }
+}
diff --git a/camera/camera2/src/main/java/androidx/camera/camera2/DefaultImageAnalysisConfigurationProvider.java b/camera/camera2/src/main/java/androidx/camera/camera2/DefaultImageAnalysisConfigurationProvider.java
new file mode 100644
index 0000000..6ca7660
--- /dev/null
+++ b/camera/camera2/src/main/java/androidx/camera/camera2/DefaultImageAnalysisConfigurationProvider.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright (C) 2019 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.camera2;
+
+import android.hardware.camera2.CameraDevice;
+import android.util.Log;
+
+import androidx.camera.core.CameraFactory;
+import androidx.camera.core.CameraX.LensFacing;
+import androidx.camera.core.ConfigurationProvider;
+import androidx.camera.core.ImageAnalysisUseCase;
+import androidx.camera.core.ImageAnalysisUseCaseConfiguration;
+import androidx.camera.core.SessionConfiguration;
+
+/**
+ * Provides defaults for {@link ImageAnalysisUseCaseConfiguration} in the Camera2 implementation.
+ */
+final class DefaultImageAnalysisConfigurationProvider
+ implements ConfigurationProvider<ImageAnalysisUseCaseConfiguration> {
+ private static final String TAG = "DefaultImageAnalysisConfigurationProvider";
+
+ private final CameraFactory cameraFactory;
+
+ public DefaultImageAnalysisConfigurationProvider(CameraFactory cameraFactory) {
+ this.cameraFactory = cameraFactory;
+ }
+
+ @Override
+ public ImageAnalysisUseCaseConfiguration getConfiguration() {
+ ImageAnalysisUseCaseConfiguration.Builder builder =
+ ImageAnalysisUseCaseConfiguration.Builder.fromConfig(
+ ImageAnalysisUseCase.DEFAULT_CONFIG.getConfiguration());
+
+ // SessionConfiguration containing all intrinsic properties needed for ImageAnalysisUseCase
+ SessionConfiguration.Builder sessionBuilder = new SessionConfiguration.Builder();
+ // TODO(b/114762170): Must set to preview here until we allow for multiple template types
+ sessionBuilder.setTemplateType(CameraDevice.TEMPLATE_PREVIEW);
+
+ // Add options to UseCaseConfiguration
+ builder.setDefaultSessionConfiguration(sessionBuilder.build());
+ builder.setOptionUnpacker(Camera2OptionUnpacker.INSTANCE);
+
+ // Add default lensFacing if we can
+ try {
+ String defaultId = cameraFactory.cameraIdForLensFacing(LensFacing.BACK);
+ if (defaultId != null) {
+ builder.setLensFacing(LensFacing.BACK);
+ } else {
+ defaultId = cameraFactory.cameraIdForLensFacing(LensFacing.FRONT);
+ if (defaultId != null) {
+ builder.setLensFacing(LensFacing.FRONT);
+ }
+ }
+ } catch (Exception e) {
+ Log.w(TAG, "Unable to determine default lens facing for ImageAnalysisUseCase.", e);
+ }
+
+ return builder.build();
+ }
+}
diff --git a/camera/camera2/src/main/java/androidx/camera/camera2/DefaultImageCaptureConfigurationProvider.java b/camera/camera2/src/main/java/androidx/camera/camera2/DefaultImageCaptureConfigurationProvider.java
new file mode 100644
index 0000000..3a58272
--- /dev/null
+++ b/camera/camera2/src/main/java/androidx/camera/camera2/DefaultImageCaptureConfigurationProvider.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright (C) 2019 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.camera2;
+
+import android.hardware.camera2.CameraDevice;
+import android.util.Log;
+
+import androidx.camera.core.CameraFactory;
+import androidx.camera.core.CameraX.LensFacing;
+import androidx.camera.core.ConfigurationProvider;
+import androidx.camera.core.ImageCaptureUseCase;
+import androidx.camera.core.ImageCaptureUseCaseConfiguration;
+import androidx.camera.core.SessionConfiguration;
+
+/** Provides defaults for {@link ImageCaptureUseCaseConfiguration} in the Camera2 implementation. */
+final class DefaultImageCaptureConfigurationProvider
+ implements ConfigurationProvider<ImageCaptureUseCaseConfiguration> {
+ private static final String TAG = "DefaultImageCaptureConfigurationProvider";
+
+ private final CameraFactory cameraFactory;
+
+ public DefaultImageCaptureConfigurationProvider(CameraFactory cameraFactory) {
+ this.cameraFactory = cameraFactory;
+ }
+
+ @Override
+ public ImageCaptureUseCaseConfiguration getConfiguration() {
+ ImageCaptureUseCaseConfiguration.Builder builder =
+ ImageCaptureUseCaseConfiguration.Builder.fromConfig(
+ ImageCaptureUseCase.DEFAULT_CONFIG.getConfiguration());
+
+ // SessionConfiguration containing all intrinsic properties needed for ImageCaptureUseCase
+ SessionConfiguration.Builder sessionBuilder = new SessionConfiguration.Builder();
+ // TODO(b/114762170): Must set to preview here until we allow for multiple template types
+ sessionBuilder.setTemplateType(CameraDevice.TEMPLATE_PREVIEW);
+
+ // Add options to UseCaseConfiguration
+ builder.setDefaultSessionConfiguration(sessionBuilder.build());
+ builder.setOptionUnpacker(Camera2OptionUnpacker.INSTANCE);
+
+ // Add default lensFacing if we can
+ try {
+ String defaultId = cameraFactory.cameraIdForLensFacing(LensFacing.BACK);
+ if (defaultId != null) {
+ builder.setLensFacing(LensFacing.BACK);
+ } else {
+ defaultId = cameraFactory.cameraIdForLensFacing(LensFacing.FRONT);
+ if (defaultId != null) {
+ builder.setLensFacing(LensFacing.FRONT);
+ }
+ }
+ } catch (Exception e) {
+ Log.w(TAG, "Unable to determine default lens facing for ImageCaptureUseCase.", e);
+ }
+
+ return builder.build();
+ }
+}
diff --git a/camera/camera2/src/main/java/androidx/camera/camera2/DefaultVideoCaptureConfigurationProvider.java b/camera/camera2/src/main/java/androidx/camera/camera2/DefaultVideoCaptureConfigurationProvider.java
new file mode 100644
index 0000000..79bd127
--- /dev/null
+++ b/camera/camera2/src/main/java/androidx/camera/camera2/DefaultVideoCaptureConfigurationProvider.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright (C) 2019 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.camera2;
+
+import android.hardware.camera2.CameraDevice;
+import android.util.Log;
+
+import androidx.camera.core.CameraFactory;
+import androidx.camera.core.CameraX.LensFacing;
+import androidx.camera.core.ConfigurationProvider;
+import androidx.camera.core.SessionConfiguration;
+import androidx.camera.core.VideoCaptureUseCase;
+import androidx.camera.core.VideoCaptureUseCaseConfiguration;
+
+/** Provides defaults for {@link VideoCaptureUseCaseConfiguration} in the Camera2 implementation. */
+final class DefaultVideoCaptureConfigurationProvider
+ implements ConfigurationProvider<VideoCaptureUseCaseConfiguration> {
+ private static final String TAG = "DefaultVideoCaptureConfigurationProvider";
+
+ private final CameraFactory cameraFactory;
+
+ public DefaultVideoCaptureConfigurationProvider(CameraFactory cameraFactory) {
+ this.cameraFactory = cameraFactory;
+ }
+
+ @Override
+ public VideoCaptureUseCaseConfiguration getConfiguration() {
+ VideoCaptureUseCaseConfiguration.Builder builder =
+ VideoCaptureUseCaseConfiguration.Builder.fromConfig(
+ VideoCaptureUseCase.DEFAULT_CONFIG.getConfiguration());
+
+ // SessionConfiguration containing all intrinsic properties needed for VideoCaptureUseCase
+ SessionConfiguration.Builder sessionBuilder = new SessionConfiguration.Builder();
+ // TODO(b/114762170): Must set to preview here until we allow for multiple template types
+ sessionBuilder.setTemplateType(CameraDevice.TEMPLATE_PREVIEW);
+
+ // Add options to UseCaseConfiguration
+ builder.setDefaultSessionConfiguration(sessionBuilder.build());
+ builder.setOptionUnpacker(Camera2OptionUnpacker.INSTANCE);
+
+ // Add default lensFacing if we can
+ try {
+ String defaultId = cameraFactory.cameraIdForLensFacing(LensFacing.BACK);
+ if (defaultId != null) {
+ builder.setLensFacing(LensFacing.BACK);
+ } else {
+ defaultId = cameraFactory.cameraIdForLensFacing(LensFacing.FRONT);
+ if (defaultId != null) {
+ builder.setLensFacing(LensFacing.FRONT);
+ }
+ }
+ } catch (Exception e) {
+ Log.w(TAG, "Unable to determine default lens facing for VideoCaptureUseCase.", e);
+ }
+
+ return builder.build();
+ }
+}
diff --git a/camera/camera2/src/main/java/androidx/camera/camera2/DefaultViewFinderConfigurationProvider.java b/camera/camera2/src/main/java/androidx/camera/camera2/DefaultViewFinderConfigurationProvider.java
new file mode 100644
index 0000000..bfb590b
--- /dev/null
+++ b/camera/camera2/src/main/java/androidx/camera/camera2/DefaultViewFinderConfigurationProvider.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 2019 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.camera2;
+
+import android.hardware.camera2.CameraDevice;
+import android.util.Log;
+
+import androidx.camera.core.CameraFactory;
+import androidx.camera.core.CameraX.LensFacing;
+import androidx.camera.core.ConfigurationProvider;
+import androidx.camera.core.SessionConfiguration;
+import androidx.camera.core.ViewFinderUseCase;
+import androidx.camera.core.ViewFinderUseCaseConfiguration;
+
+/** Provides defaults for {@link ViewFinderUseCaseConfiguration} in the Camera2 implementation. */
+final class DefaultViewFinderConfigurationProvider
+ implements ConfigurationProvider<ViewFinderUseCaseConfiguration> {
+ private static final String TAG = "DefaultViewFinderConfigurationProvider";
+
+ private final CameraFactory cameraFactory;
+
+ public DefaultViewFinderConfigurationProvider(CameraFactory cameraFactory) {
+ this.cameraFactory = cameraFactory;
+ }
+
+ @Override
+ public ViewFinderUseCaseConfiguration getConfiguration() {
+ ViewFinderUseCaseConfiguration.Builder builder =
+ ViewFinderUseCaseConfiguration.Builder.fromConfig(
+ ViewFinderUseCase.DEFAULT_CONFIG.getConfiguration());
+
+ // SessionConfiguration containing all intrinsic properties needed for ViewFinderUseCase
+ SessionConfiguration.Builder sessionBuilder = new SessionConfiguration.Builder();
+ sessionBuilder.setTemplateType(CameraDevice.TEMPLATE_PREVIEW);
+
+ // Add options to UseCaseConfiguration
+ builder.setDefaultSessionConfiguration(sessionBuilder.build());
+ builder.setOptionUnpacker(Camera2OptionUnpacker.INSTANCE);
+
+ // Add default lensFacing if we can
+ try {
+ String defaultId = cameraFactory.cameraIdForLensFacing(LensFacing.BACK);
+ if (defaultId != null) {
+ builder.setLensFacing(LensFacing.BACK);
+ } else {
+ defaultId = cameraFactory.cameraIdForLensFacing(LensFacing.FRONT);
+ if (defaultId != null) {
+ builder.setLensFacing(LensFacing.FRONT);
+ }
+ }
+ } catch (Exception e) {
+ Log.w(TAG, "Unable to determine default lens facing for ViewFinderUseCase.", e);
+ }
+
+ return builder.build();
+ }
+}
diff --git a/camera/camera2/src/main/java/androidx/camera/camera2/SupportedSurfaceCombination.java b/camera/camera2/src/main/java/androidx/camera/camera2/SupportedSurfaceCombination.java
new file mode 100644
index 0000000..eee8d21
--- /dev/null
+++ b/camera/camera2/src/main/java/androidx/camera/camera2/SupportedSurfaceCombination.java
@@ -0,0 +1,988 @@
+/*
+ * Copyright (C) 2019 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.camera2;
+
+import android.annotation.SuppressLint;
+import android.content.Context;
+import android.graphics.ImageFormat;
+import android.graphics.Point;
+import android.graphics.SurfaceTexture;
+import android.hardware.camera2.CameraAccessException;
+import android.hardware.camera2.CameraCharacteristics;
+import android.hardware.camera2.CameraManager;
+import android.hardware.camera2.params.StreamConfigurationMap;
+import android.media.CamcorderProfile;
+import android.os.Build;
+import android.util.Rational;
+import android.util.Size;
+import android.view.WindowManager;
+
+import androidx.camera.core.BaseUseCase;
+import androidx.camera.core.CameraDeviceConfiguration;
+import androidx.camera.core.CameraX;
+import androidx.camera.core.ImageFormatConstants;
+import androidx.camera.core.ImageOutputConfiguration;
+import androidx.camera.core.SurfaceCombination;
+import androidx.camera.core.SurfaceConfiguration;
+import androidx.camera.core.SurfaceConfiguration.ConfigurationSize;
+import androidx.camera.core.SurfaceConfiguration.ConfigurationType;
+import androidx.camera.core.SurfaceSizeDefinition;
+import androidx.camera.core.UseCaseConfiguration;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Camera device supported surface configuration combinations
+ *
+ * <p>{@link android.hardware.camera2.CameraDevice#createCaptureSession} defines the default
+ * guaranteed stream combinations for different hardware level devices. It defines what combination
+ * of surface configuration type and size pairs can be supported for different hardware level camera
+ * devices. This structure is used to store a list of surface combinations that are guaranteed to
+ * support for this camera device.
+ */
+final class SupportedSurfaceCombination {
+ private static final Size MAX_PREVIEW_SIZE = new Size(1920, 1080);
+ private static final Size DEFAULT_SIZE = new Size(640, 480);
+ private static final Size ZERO_SIZE = new Size(0, 0);
+ private static final Size QUALITY_2160P_SIZE = new Size(3840, 2160);
+ private static final Size QUALITY_1080P_SIZE = new Size(1920, 1080);
+ private static final Size QUALITY_720P_SIZE = new Size(1280, 720);
+ private static final Size QUALITY_480P_SIZE = new Size(720, 480);
+ private final List<SurfaceCombination> surfaceCombinationList = new ArrayList<>();
+ private String cameraId;
+ private CameraCharacteristics characteristics;
+ private int hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY;
+ private boolean isRawSupported = false;
+ private boolean isBurstCaptureSupported = false;
+ private Size displayViewSize;
+ private SurfaceSizeDefinition surfaceSizeDefinition;
+ private CamcorderProfileHelper camcorderProfileHelper;
+
+ SupportedSurfaceCombination(
+ Context context, String cameraId, CamcorderProfileHelper camcorderProfileHelper) {
+ this.cameraId = cameraId;
+ this.camcorderProfileHelper = camcorderProfileHelper;
+ init(context);
+ }
+
+ private SupportedSurfaceCombination() {
+ }
+
+ String getCameraId() {
+ return cameraId;
+ }
+
+ boolean isRawSupported() {
+ return isRawSupported;
+ }
+
+ boolean isBurstCaptureSupported() {
+ return isBurstCaptureSupported;
+ }
+
+ /**
+ * Check whether the input surface configuration list is under the capability of any combination
+ * of this object.
+ *
+ * @param surfaceConfigurationList the surface configuration list to be compared
+ * @return the check result that whether it could be supported
+ */
+ boolean checkSupported(List<SurfaceConfiguration> surfaceConfigurationList) {
+ boolean isSupported = false;
+
+ for (SurfaceCombination surfaceCombination : surfaceCombinationList) {
+ isSupported = surfaceCombination.isSupported(surfaceConfigurationList);
+
+ if (isSupported) {
+ break;
+ }
+ }
+
+ return isSupported;
+ }
+
+ /**
+ * Transform to a SurfaceConfiguration object with image format and size info
+ *
+ * @param imageFormat the image format info for the surface configuration object
+ * @param size the size info for the surface configuration object
+ * @return new {@link SurfaceConfiguration} object
+ */
+ SurfaceConfiguration transformSurfaceConfiguration(int imageFormat, Size size) {
+ ConfigurationType configurationType;
+ ConfigurationSize configurationSize = ConfigurationSize.NOT_SUPPORT;
+
+ if (getAllOutputSizesByFormat(imageFormat) == null) {
+ throw new IllegalArgumentException(
+ "Can not get supported output size for the format: " + imageFormat);
+ }
+
+ /**
+ * PRIV refers to any target whose available sizes are found using
+ * StreamConfigurationMap.getOutputSizes(Class) with no direct application-visible format,
+ * YUV refers to a target Surface using the ImageFormat.YUV_420_888 format, JPEG refers to
+ * the ImageFormat.JPEG format, and RAW refers to the ImageFormat.RAW_SENSOR format.
+ */
+ if (imageFormat == ImageFormat.YUV_420_888) {
+ configurationType = ConfigurationType.YUV;
+ } else if (imageFormat == ImageFormat.JPEG) {
+ configurationType = ConfigurationType.JPEG;
+ } else if (imageFormat == ImageFormat.RAW_SENSOR) {
+ configurationType = ConfigurationType.RAW;
+ } else {
+ configurationType = ConfigurationType.PRIV;
+ }
+
+ Size maxSize = surfaceSizeDefinition.getMaximumSizeMap().get(imageFormat);
+
+ // Compare with surface size definition to determine the surface configuration size
+ if (size.getWidth() * size.getHeight()
+ <= surfaceSizeDefinition.getAnalysisSize().getWidth()
+ * surfaceSizeDefinition.getAnalysisSize().getHeight()) {
+ configurationSize = ConfigurationSize.ANALYSIS;
+ } else if (size.getWidth() * size.getHeight()
+ <= surfaceSizeDefinition.getPreviewSize().getWidth()
+ * surfaceSizeDefinition.getPreviewSize().getHeight()) {
+ configurationSize = ConfigurationSize.PREVIEW;
+ } else if (size.getWidth() * size.getHeight()
+ <= surfaceSizeDefinition.getRecordSize().getWidth()
+ * surfaceSizeDefinition.getRecordSize().getHeight()) {
+ configurationSize = ConfigurationSize.RECORD;
+ } else if (size.getWidth() * size.getHeight() <= maxSize.getWidth() * maxSize.getHeight()) {
+ configurationSize = ConfigurationSize.MAXIMUM;
+ }
+
+ return SurfaceConfiguration.create(configurationType, configurationSize);
+ }
+
+ Map<BaseUseCase, Size> getSuggestedResolutions(
+ List<BaseUseCase> originalUseCases, List<BaseUseCase> newUseCases) {
+ Map<BaseUseCase, Size> suggestedResolutionsMap = new HashMap<>();
+
+ // Get the index order list by the use case priority for finding stream configuration
+ List<Integer> useCasesPriorityOrder = getUseCasesPriorityOrder(newUseCases);
+ List<List<Size>> supportedOutputSizesList = new ArrayList<>();
+
+ // Collect supported output sizes for all use cases
+ for (Integer index : useCasesPriorityOrder) {
+ List<Size> supportedOutputSizes = getSupportedOutputSizes(newUseCases.get(index));
+ supportedOutputSizesList.add(supportedOutputSizes);
+ }
+
+ // Get all possible size arrangements
+ List<List<Size>> allPossibleSizeArrangements =
+ getAllPossibleSizeArrangements(supportedOutputSizesList);
+
+ // Transform use cases to SurfaceConfiguration list and find the first (best) workable
+ // combination
+ for (List<Size> possibleSizeList : allPossibleSizeArrangements) {
+ List<SurfaceConfiguration> surfaceConfigurationList = new ArrayList<>();
+
+ // Attach SurfaceConfiguration of original use cases since it will impact the new use
+ // cases
+ if (originalUseCases != null) {
+ for (BaseUseCase useCase : originalUseCases) {
+ CameraDeviceConfiguration configuration =
+ (CameraDeviceConfiguration) useCase.getUseCaseConfiguration();
+ String useCaseCameraId;
+ try {
+ useCaseCameraId =
+ CameraX.getCameraWithLensFacing(configuration.getLensFacing());
+ } catch (Exception e) {
+ throw new IllegalArgumentException(
+ "Unable to get camera ID for use case " + useCase.getName(), e);
+ }
+ Size resolution = useCase.getAttachedSurfaceResolution(useCaseCameraId);
+
+ surfaceConfigurationList.add(
+ transformSurfaceConfiguration(useCase.getImageFormat(), resolution));
+ }
+ }
+
+ // Attach SurfaceConfiguration of new use cases
+ for (Size size : possibleSizeList) {
+ BaseUseCase newUseCase =
+ newUseCases.get(useCasesPriorityOrder.get(possibleSizeList.indexOf(size)));
+ surfaceConfigurationList.add(
+ transformSurfaceConfiguration(newUseCase.getImageFormat(), size));
+ }
+
+ // Check whether the SurfaceConfiguration combination can be supported
+ if (checkSupported(surfaceConfigurationList)) {
+ for (BaseUseCase useCase : newUseCases) {
+ suggestedResolutionsMap.put(
+ useCase,
+ possibleSizeList.get(
+ useCasesPriorityOrder.indexOf(newUseCases.indexOf(useCase))));
+ }
+ break;
+ }
+ }
+
+ return suggestedResolutionsMap;
+ }
+
+ SurfaceSizeDefinition getSurfaceSizeDefinition() {
+ return surfaceSizeDefinition;
+ }
+
+ private List<Integer> getUseCasesPriorityOrder(List<BaseUseCase> newUseCases) {
+ List<Integer> priorityOrder = new ArrayList<>();
+
+ /**
+ * Once the stream resource is occupied by one use case, it will impact the other use cases.
+ * Therefore, we need to define the priority for stream resource usage. For the use cases
+ * with the higher priority, we will try to find the best one for them in priority as
+ * possible.
+ */
+ List<Integer> priorityValueList = new ArrayList<>();
+
+ for (BaseUseCase useCase : newUseCases) {
+ UseCaseConfiguration<?> configuration = useCase.getUseCaseConfiguration();
+ int priority = configuration.getSurfaceOccupancyPriority(0);
+ if (!priorityValueList.contains(priority)) {
+ priorityValueList.add(priority);
+ }
+ }
+
+ Collections.sort(priorityValueList);
+ // Reverse the priority value list in descending order since larger value means higher
+ // priority
+ Collections.reverse(priorityValueList);
+
+ for (int priorityValue : priorityValueList) {
+ for (BaseUseCase useCase : newUseCases) {
+ UseCaseConfiguration<?> configuration = useCase.getUseCaseConfiguration();
+ if (priorityValue == configuration.getSurfaceOccupancyPriority(0)) {
+ priorityOrder.add(newUseCases.indexOf(useCase));
+ }
+ }
+ }
+
+ return priorityOrder;
+ }
+
+ private List<Size> getSupportedOutputSizes(BaseUseCase useCase) {
+ int imageFormat = useCase.getImageFormat();
+ Size[] outputSizes = getAllOutputSizesByFormat(imageFormat);
+ List<Size> outputSizeCandidates = new ArrayList<>();
+ ImageOutputConfiguration configuration =
+ (ImageOutputConfiguration) useCase.getUseCaseConfiguration();
+ Size maxSize = configuration.getMaxResolution(getMaxOutputSizeByFormat(imageFormat));
+
+ // Sort the output sizes. The Comparator result must be reversed to have a descending order
+ // result.
+ Collections.sort(Arrays.asList(outputSizes), new CompareSizesByArea(true));
+
+ // Filter out the ones that exceed the maximum size
+ for (Size outputSize : outputSizes) {
+ if (outputSize.getWidth() * outputSize.getHeight()
+ <= maxSize.getWidth() * maxSize.getHeight()) {
+ outputSizeCandidates.add(outputSize);
+ }
+ }
+
+ if (outputSizeCandidates.isEmpty()) {
+ throw new IllegalArgumentException(
+ "Can not get supported output size under supported maximum for the format: "
+ + imageFormat);
+ }
+
+ // Check whether the desired default resolution is included in the original supported list
+ boolean isDefaultResolutionSupported = outputSizeCandidates.contains(DEFAULT_SIZE);
+
+ // If the target resolution is set, use it to find the minimum one from big enough items
+ Size targetSize = configuration.getTargetResolution(ZERO_SIZE);
+
+ if (!targetSize.equals(ZERO_SIZE)) {
+ int indexBigEnough = 0;
+
+ // Get the index of the item that is big enough for the view size
+ for (Size outputSize : outputSizeCandidates) {
+ if (outputSize.getWidth() * outputSize.getHeight()
+ >= targetSize.getWidth() * targetSize.getHeight()) {
+ indexBigEnough = outputSizeCandidates.indexOf(outputSize);
+ } else {
+ break;
+ }
+ }
+
+ // Remove the additional items that is larger than the big enough item
+ outputSizeCandidates.subList(0, indexBigEnough).clear();
+ }
+
+ if (outputSizeCandidates.isEmpty() && !isDefaultResolutionSupported) {
+ throw new IllegalArgumentException(
+ "Can not get supported output size for the desired output size quality for "
+ + "the format: "
+ + imageFormat);
+ }
+
+ // Rearrange the supported size to put the ones with the same aspect ratio in the front of
+ // the
+ // list and put others in the end from large to small. Some low end devices may not able to
+ // get
+ // an supported resolution that match the preferred aspect ratio.
+ List<Size> sizesMatchAspectRatio = new ArrayList<>();
+ List<Size> sizesNotMatchAspectRatio = new ArrayList<>();
+ Rational aspectRatio = configuration.getTargetAspectRatio();
+
+ for (Size outputSize : outputSizeCandidates) {
+ if (aspectRatio.equals(new Rational(outputSize.getWidth(), outputSize.getHeight()))
+ || aspectRatio.equals(
+ new Rational(outputSize.getHeight(), outputSize.getWidth()))) {
+ sizesMatchAspectRatio.add(outputSize);
+ } else {
+ sizesNotMatchAspectRatio.add(outputSize);
+ }
+ }
+
+ List<Size> supportedResolutions = new ArrayList<>();
+ // No need to sort again since the source list has been sorted previously
+ supportedResolutions.addAll(sizesMatchAspectRatio);
+ supportedResolutions.addAll(sizesNotMatchAspectRatio);
+
+ // If there is no available size for the conditions and default resolution is in the
+ // supported
+ // list, return the default resolution.
+ if (supportedResolutions.isEmpty() && !isDefaultResolutionSupported) {
+ supportedResolutions.add(DEFAULT_SIZE);
+ }
+
+ return supportedResolutions;
+ }
+
+ private List<List<Size>> getAllPossibleSizeArrangements(
+ List<List<Size>> supportedOutputSizesList) {
+ int totalArrangementsCount = 1;
+
+ for (List<Size> supportedOutputSizes : supportedOutputSizesList) {
+ totalArrangementsCount *= supportedOutputSizes.size();
+ }
+
+ // If totalArrangementsCount is 0 means that there may some problem to get
+ // supportedOutputSizes
+ // for some use case
+ if (totalArrangementsCount == 0) {
+ throw new IllegalArgumentException("Failed to find supported resolutions.");
+ }
+
+ List<List<Size>> allPossibleSizeArrangements = new ArrayList<>();
+
+ // Initialize allPossibleSizeArrangements for the following operations
+ for (int i = 0; i < totalArrangementsCount; i++) {
+ List<Size> sizeList = new ArrayList<>();
+ allPossibleSizeArrangements.add(sizeList);
+ }
+
+ /**
+ * Try to list out all possible arrangements by attaching all possible size of each column
+ * in sequence. We have generated supportedOutputSizesList by the priority order for
+ * different use cases. And the supported outputs sizes for each use case are also arranged
+ * from large to small. Therefore, the earlier size arrangement in the result list will be
+ * the better one to choose if finally it won't exceed the camera device's stream
+ * combination capability.
+ */
+ int currentRunCount = totalArrangementsCount;
+ int nextRunCount = currentRunCount / supportedOutputSizesList.get(0).size();
+
+ for (List<Size> supportedOutputSizes : supportedOutputSizesList) {
+ for (int i = 0; i < totalArrangementsCount; i++) {
+ List<Size> surfaceConfigurationList = allPossibleSizeArrangements.get(i);
+
+ surfaceConfigurationList.add(
+ supportedOutputSizes.get((i % currentRunCount) / nextRunCount));
+ }
+
+ int currentIndex = supportedOutputSizesList.indexOf(supportedOutputSizes);
+
+ if (currentIndex < supportedOutputSizesList.size() - 1) {
+ currentRunCount = nextRunCount;
+ nextRunCount =
+ currentRunCount / supportedOutputSizesList.get(currentIndex + 1).size();
+ }
+ }
+
+ return allPossibleSizeArrangements;
+ }
+
+ // TODO(b/124267925): Remove @SuppressLint once we target API 21
+ @SuppressLint("ObsoleteSdkInt")
+ private Size[] getAllOutputSizesByFormat(int imageFormat) {
+ if (characteristics == null) {
+ throw new IllegalStateException("CameraCharacteristics is null.");
+ }
+
+ StreamConfigurationMap map =
+ characteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP);
+
+ if (map == null) {
+ throw new IllegalArgumentException(
+ "Can not get supported output size for the format: " + imageFormat);
+ }
+
+ Size[] outputSizes;
+ if (Build.VERSION.SDK_INT < 23
+ && imageFormat == ImageFormatConstants.INTERNAL_DEFINED_IMAGE_FORMAT_PRIVATE) {
+ // This is a little tricky that 0x22 that is internal defined in
+ // StreamConfigurationMap.java
+ // to be equal to ImageFormat.PRIVATE that is public after Android level 23 but not
+ // public in
+ // Android L. Use {@link SurfaceTexture} or {@link MediaCodec} will finally mapped to
+ // 0x22 in
+ // StreamConfigurationMap to retrieve the output sizes information.
+ outputSizes = map.getOutputSizes(SurfaceTexture.class);
+ } else {
+ outputSizes = map.getOutputSizes(imageFormat);
+ }
+
+ if (outputSizes == null) {
+ throw new IllegalArgumentException(
+ "Can not get supported output size for the format: " + imageFormat);
+ }
+
+ // Sort the output sizes. The Comparator result must be reversed to have a descending order
+ // result.
+ Collections.sort(Arrays.asList(outputSizes), new CompareSizesByArea(true));
+
+ return outputSizes;
+ }
+
+ /**
+ * Get max supported output size for specific image format
+ *
+ * @param imageFormat the image format info
+ * @return the max supported output size for the image format
+ */
+ Size getMaxOutputSizeByFormat(int imageFormat) {
+ Size[] outputSizes = getAllOutputSizesByFormat(imageFormat);
+
+ return Collections.max(Arrays.asList(outputSizes), new CompareSizesByArea());
+ }
+
+ private void init(Context context) {
+ CameraManager cameraManager =
+ (CameraManager) context.getSystemService(Context.CAMERA_SERVICE);
+ WindowManager windowManager =
+ (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
+
+ try {
+ generateSupportedCombinationList(cameraManager);
+ generateSurfaceSizeDefinition(windowManager);
+ } catch (CameraAccessException e) {
+ throw new IllegalArgumentException(
+ "Generate supported combination list and size definition fail - CameraId:"
+ + cameraId,
+ e);
+ }
+ checkCustomization();
+ }
+
+ List<SurfaceCombination> getLegacySupportedCombinationList() {
+ List<SurfaceCombination> combinationList = new ArrayList<>();
+
+ // (PRIV, MAXIMUM)
+ SurfaceCombination surfaceCombination1 = new SurfaceCombination();
+ surfaceCombination1.addSurfaceConfiguration(
+ SurfaceConfiguration.create(ConfigurationType.PRIV, ConfigurationSize.MAXIMUM));
+ combinationList.add(surfaceCombination1);
+
+ // (JPEG, MAXIMUM)
+ SurfaceCombination surfaceCombination2 = new SurfaceCombination();
+ surfaceCombination2.addSurfaceConfiguration(
+ SurfaceConfiguration.create(ConfigurationType.JPEG, ConfigurationSize.MAXIMUM));
+ combinationList.add(surfaceCombination2);
+
+ // (YUV, MAXIMUM)
+ SurfaceCombination surfaceCombination3 = new SurfaceCombination();
+ surfaceCombination3.addSurfaceConfiguration(
+ SurfaceConfiguration.create(ConfigurationType.YUV, ConfigurationSize.MAXIMUM));
+ combinationList.add(surfaceCombination3);
+
+ // Below two combinations are all supported in the combination
+ // (PRIV, PREVIEW) + (JPEG, MAXIMUM)
+ SurfaceCombination surfaceCombination4 = new SurfaceCombination();
+ surfaceCombination4.addSurfaceConfiguration(
+ SurfaceConfiguration.create(ConfigurationType.PRIV, ConfigurationSize.PREVIEW));
+ surfaceCombination4.addSurfaceConfiguration(
+ SurfaceConfiguration.create(ConfigurationType.JPEG, ConfigurationSize.MAXIMUM));
+ combinationList.add(surfaceCombination4);
+
+ // (YUV, PREVIEW) + (JPEG, MAXIMUM)
+ SurfaceCombination surfaceCombination5 = new SurfaceCombination();
+ surfaceCombination5.addSurfaceConfiguration(
+ SurfaceConfiguration.create(ConfigurationType.YUV, ConfigurationSize.PREVIEW));
+ surfaceCombination5.addSurfaceConfiguration(
+ SurfaceConfiguration.create(ConfigurationType.JPEG, ConfigurationSize.MAXIMUM));
+ combinationList.add(surfaceCombination5);
+
+ // (PRIV, PREVIEW) + (PRIV, PREVIEW)
+ SurfaceCombination surfaceCombination6 = new SurfaceCombination();
+ surfaceCombination6.addSurfaceConfiguration(
+ SurfaceConfiguration.create(ConfigurationType.PRIV, ConfigurationSize.PREVIEW));
+ surfaceCombination6.addSurfaceConfiguration(
+ SurfaceConfiguration.create(ConfigurationType.PRIV, ConfigurationSize.PREVIEW));
+ combinationList.add(surfaceCombination6);
+
+ // (PRIV, PREVIEW) + (YUV, PREVIEW)
+ SurfaceCombination surfaceCombination7 = new SurfaceCombination();
+ surfaceCombination7.addSurfaceConfiguration(
+ SurfaceConfiguration.create(ConfigurationType.PRIV, ConfigurationSize.PREVIEW));
+ surfaceCombination7.addSurfaceConfiguration(
+ SurfaceConfiguration.create(ConfigurationType.YUV, ConfigurationSize.PREVIEW));
+ combinationList.add(surfaceCombination7);
+
+ // (PRIV, PREVIEW) + (PRIV, PREVIEW) + (JPEG, MAXIMUM)
+ SurfaceCombination surfaceCombination8 = new SurfaceCombination();
+ surfaceCombination8.addSurfaceConfiguration(
+ SurfaceConfiguration.create(ConfigurationType.PRIV, ConfigurationSize.PREVIEW));
+ surfaceCombination8.addSurfaceConfiguration(
+ SurfaceConfiguration.create(ConfigurationType.YUV, ConfigurationSize.PREVIEW));
+ surfaceCombination8.addSurfaceConfiguration(
+ SurfaceConfiguration.create(ConfigurationType.JPEG, ConfigurationSize.MAXIMUM));
+ combinationList.add(surfaceCombination8);
+
+ return combinationList;
+ }
+
+ List<SurfaceCombination> getLimitedSupportedCombinationList() {
+ List<SurfaceCombination> combinationList = new ArrayList<>();
+
+ // (PRIV, PREVIEW) + (PRIV, RECORD)
+ SurfaceCombination surfaceCombination1 = new SurfaceCombination();
+ surfaceCombination1.addSurfaceConfiguration(
+ SurfaceConfiguration.create(ConfigurationType.PRIV, ConfigurationSize.PREVIEW));
+ surfaceCombination1.addSurfaceConfiguration(
+ SurfaceConfiguration.create(ConfigurationType.PRIV, ConfigurationSize.RECORD));
+ combinationList.add(surfaceCombination1);
+
+ // (PRIV, PREVIEW) + (YUV, RECORD)
+ SurfaceCombination surfaceCombination2 = new SurfaceCombination();
+ surfaceCombination2.addSurfaceConfiguration(
+ SurfaceConfiguration.create(ConfigurationType.PRIV, ConfigurationSize.PREVIEW));
+ surfaceCombination2.addSurfaceConfiguration(
+ SurfaceConfiguration.create(ConfigurationType.YUV, ConfigurationSize.RECORD));
+ combinationList.add(surfaceCombination2);
+
+ // (YUV, PREVIEW) + (YUV, RECORD)
+ SurfaceCombination surfaceCombination3 = new SurfaceCombination();
+ surfaceCombination3.addSurfaceConfiguration(
+ SurfaceConfiguration.create(ConfigurationType.YUV, ConfigurationSize.PREVIEW));
+ surfaceCombination3.addSurfaceConfiguration(
+ SurfaceConfiguration.create(ConfigurationType.YUV, ConfigurationSize.RECORD));
+ combinationList.add(surfaceCombination3);
+
+ // (PRIV, PREVIEW) + (PRIV, RECORD) + (JPEG, RECORD)
+ SurfaceCombination surfaceCombination4 = new SurfaceCombination();
+ surfaceCombination4.addSurfaceConfiguration(
+ SurfaceConfiguration.create(ConfigurationType.PRIV, ConfigurationSize.PREVIEW));
+ surfaceCombination4.addSurfaceConfiguration(
+ SurfaceConfiguration.create(ConfigurationType.PRIV, ConfigurationSize.RECORD));
+ surfaceCombination4.addSurfaceConfiguration(
+ SurfaceConfiguration.create(ConfigurationType.JPEG, ConfigurationSize.RECORD));
+ combinationList.add(surfaceCombination4);
+
+ // (PRIV, PREVIEW) + (YUV, RECORD) + (JPEG, RECORD)
+ SurfaceCombination surfaceCombination5 = new SurfaceCombination();
+ surfaceCombination5.addSurfaceConfiguration(
+ SurfaceConfiguration.create(ConfigurationType.PRIV, ConfigurationSize.PREVIEW));
+ surfaceCombination5.addSurfaceConfiguration(
+ SurfaceConfiguration.create(ConfigurationType.YUV, ConfigurationSize.RECORD));
+ surfaceCombination5.addSurfaceConfiguration(
+ SurfaceConfiguration.create(ConfigurationType.JPEG, ConfigurationSize.RECORD));
+ combinationList.add(surfaceCombination5);
+
+ // (YUV, PREVIEW) + (YUV, PREVIEW) + (JPEG, MAXIMUM)
+ SurfaceCombination surfaceCombination6 = new SurfaceCombination();
+ surfaceCombination6.addSurfaceConfiguration(
+ SurfaceConfiguration.create(ConfigurationType.YUV, ConfigurationSize.PREVIEW));
+ surfaceCombination6.addSurfaceConfiguration(
+ SurfaceConfiguration.create(ConfigurationType.YUV, ConfigurationSize.PREVIEW));
+ surfaceCombination6.addSurfaceConfiguration(
+ SurfaceConfiguration.create(ConfigurationType.JPEG, ConfigurationSize.MAXIMUM));
+ combinationList.add(surfaceCombination6);
+
+ return combinationList;
+ }
+
+ List<SurfaceCombination> getFullSupportedCombinationList() {
+ List<SurfaceCombination> combinationList = new ArrayList<>();
+
+ // (PRIV, PREVIEW) + (PRIV, MAXIMUM)
+ SurfaceCombination surfaceCombination1 = new SurfaceCombination();
+ surfaceCombination1.addSurfaceConfiguration(
+ SurfaceConfiguration.create(ConfigurationType.PRIV, ConfigurationSize.PREVIEW));
+ surfaceCombination1.addSurfaceConfiguration(
+ SurfaceConfiguration.create(ConfigurationType.PRIV, ConfigurationSize.MAXIMUM));
+ combinationList.add(surfaceCombination1);
+
+ // (PRIV, PREVIEW) + (YUV, MAXIMUM)
+ SurfaceCombination surfaceCombination2 = new SurfaceCombination();
+ surfaceCombination2.addSurfaceConfiguration(
+ SurfaceConfiguration.create(ConfigurationType.PRIV, ConfigurationSize.PREVIEW));
+ surfaceCombination2.addSurfaceConfiguration(
+ SurfaceConfiguration.create(ConfigurationType.YUV, ConfigurationSize.MAXIMUM));
+ combinationList.add(surfaceCombination2);
+
+ // (YUV, PREVIEW) + (YUV, MAXIMUM)
+ SurfaceCombination surfaceCombination3 = new SurfaceCombination();
+ surfaceCombination3.addSurfaceConfiguration(
+ SurfaceConfiguration.create(ConfigurationType.YUV, ConfigurationSize.PREVIEW));
+ surfaceCombination3.addSurfaceConfiguration(
+ SurfaceConfiguration.create(ConfigurationType.YUV, ConfigurationSize.MAXIMUM));
+ combinationList.add(surfaceCombination3);
+
+ // (PRIV, PREVIEW) + (PRIV, PREVIEW) + (JPEG, MAXIMUM)
+ SurfaceCombination surfaceCombination4 = new SurfaceCombination();
+ surfaceCombination4.addSurfaceConfiguration(
+ SurfaceConfiguration.create(ConfigurationType.PRIV, ConfigurationSize.PREVIEW));
+ surfaceCombination4.addSurfaceConfiguration(
+ SurfaceConfiguration.create(ConfigurationType.PRIV, ConfigurationSize.PREVIEW));
+ surfaceCombination4.addSurfaceConfiguration(
+ SurfaceConfiguration.create(ConfigurationType.JPEG, ConfigurationSize.MAXIMUM));
+ combinationList.add(surfaceCombination4);
+
+ // (YUV, ANALYSIS) + (PRIV, PREVIEW) + (YUV, MAXIMUM)
+ SurfaceCombination surfaceCombination5 = new SurfaceCombination();
+ surfaceCombination5.addSurfaceConfiguration(
+ SurfaceConfiguration.create(ConfigurationType.YUV, ConfigurationSize.ANALYSIS));
+ surfaceCombination5.addSurfaceConfiguration(
+ SurfaceConfiguration.create(ConfigurationType.PRIV, ConfigurationSize.PREVIEW));
+ surfaceCombination5.addSurfaceConfiguration(
+ SurfaceConfiguration.create(ConfigurationType.YUV, ConfigurationSize.MAXIMUM));
+ combinationList.add(surfaceCombination5);
+
+ // (YUV, ANALYSIS) + (YUV, PREVIEW) + (YUV, MAXIMUM)
+ SurfaceCombination surfaceCombination6 = new SurfaceCombination();
+ surfaceCombination6.addSurfaceConfiguration(
+ SurfaceConfiguration.create(ConfigurationType.YUV, ConfigurationSize.ANALYSIS));
+ surfaceCombination6.addSurfaceConfiguration(
+ SurfaceConfiguration.create(ConfigurationType.YUV, ConfigurationSize.PREVIEW));
+ surfaceCombination6.addSurfaceConfiguration(
+ SurfaceConfiguration.create(ConfigurationType.YUV, ConfigurationSize.MAXIMUM));
+ combinationList.add(surfaceCombination6);
+
+ return combinationList;
+ }
+
+ List<SurfaceCombination> getRAWSupportedCombinationList() {
+ List<SurfaceCombination> combinationList = new ArrayList<>();
+
+ // (RAW, MAXIMUM)
+ SurfaceCombination surfaceCombination1 = new SurfaceCombination();
+ surfaceCombination1.addSurfaceConfiguration(
+ SurfaceConfiguration.create(ConfigurationType.RAW, ConfigurationSize.MAXIMUM));
+ combinationList.add(surfaceCombination1);
+
+ // (PRIV, PREVIEW) + (RAW, MAXIMUM)
+ SurfaceCombination surfaceCombination2 = new SurfaceCombination();
+ surfaceCombination2.addSurfaceConfiguration(
+ SurfaceConfiguration.create(ConfigurationType.PRIV, ConfigurationSize.PREVIEW));
+ surfaceCombination2.addSurfaceConfiguration(
+ SurfaceConfiguration.create(ConfigurationType.RAW, ConfigurationSize.MAXIMUM));
+ combinationList.add(surfaceCombination2);
+
+ // (YUV, PREVIEW) + (RAW, MAXIMUM)
+ SurfaceCombination surfaceCombination3 = new SurfaceCombination();
+ surfaceCombination3.addSurfaceConfiguration(
+ SurfaceConfiguration.create(ConfigurationType.YUV, ConfigurationSize.PREVIEW));
+ surfaceCombination3.addSurfaceConfiguration(
+ SurfaceConfiguration.create(ConfigurationType.RAW, ConfigurationSize.MAXIMUM));
+ combinationList.add(surfaceCombination3);
+
+ // (PRIV, PREVIEW) + (PRIV, PREVIEW) + (RAW, MAXIMUM)
+ SurfaceCombination surfaceCombination4 = new SurfaceCombination();
+ surfaceCombination4.addSurfaceConfiguration(
+ SurfaceConfiguration.create(ConfigurationType.PRIV, ConfigurationSize.PREVIEW));
+ surfaceCombination4.addSurfaceConfiguration(
+ SurfaceConfiguration.create(ConfigurationType.PRIV, ConfigurationSize.PREVIEW));
+ surfaceCombination4.addSurfaceConfiguration(
+ SurfaceConfiguration.create(ConfigurationType.RAW, ConfigurationSize.MAXIMUM));
+ combinationList.add(surfaceCombination4);
+
+ // (PRIV, PREVIEW) + (YUV, PREVIEW) + (RAW, MAXIMUM)
+ SurfaceCombination surfaceCombination5 = new SurfaceCombination();
+ surfaceCombination5.addSurfaceConfiguration(
+ SurfaceConfiguration.create(ConfigurationType.PRIV, ConfigurationSize.PREVIEW));
+ surfaceCombination5.addSurfaceConfiguration(
+ SurfaceConfiguration.create(ConfigurationType.YUV, ConfigurationSize.PREVIEW));
+ surfaceCombination5.addSurfaceConfiguration(
+ SurfaceConfiguration.create(ConfigurationType.RAW, ConfigurationSize.MAXIMUM));
+ combinationList.add(surfaceCombination5);
+
+ // (YUV, PREVIEW) + (YUV, PREVIEW) + (RAW, MAXIMUM)
+ SurfaceCombination surfaceCombination6 = new SurfaceCombination();
+ surfaceCombination6.addSurfaceConfiguration(
+ SurfaceConfiguration.create(ConfigurationType.YUV, ConfigurationSize.PREVIEW));
+ surfaceCombination6.addSurfaceConfiguration(
+ SurfaceConfiguration.create(ConfigurationType.YUV, ConfigurationSize.PREVIEW));
+ surfaceCombination6.addSurfaceConfiguration(
+ SurfaceConfiguration.create(ConfigurationType.RAW, ConfigurationSize.MAXIMUM));
+ combinationList.add(surfaceCombination6);
+
+ // (PRIV, PREVIEW) + (JPEG, MAXIMUM) + (RAW, MAXIMUM)
+ SurfaceCombination surfaceCombination7 = new SurfaceCombination();
+ surfaceCombination7.addSurfaceConfiguration(
+ SurfaceConfiguration.create(ConfigurationType.PRIV, ConfigurationSize.PREVIEW));
+ surfaceCombination7.addSurfaceConfiguration(
+ SurfaceConfiguration.create(ConfigurationType.JPEG, ConfigurationSize.MAXIMUM));
+ surfaceCombination7.addSurfaceConfiguration(
+ SurfaceConfiguration.create(ConfigurationType.RAW, ConfigurationSize.MAXIMUM));
+ combinationList.add(surfaceCombination7);
+
+ // (YUV, PREVIEW) + (JPEG, MAXIMUM) + (RAW, MAXIMUM)
+ SurfaceCombination surfaceCombination8 = new SurfaceCombination();
+ surfaceCombination8.addSurfaceConfiguration(
+ SurfaceConfiguration.create(ConfigurationType.YUV, ConfigurationSize.PREVIEW));
+ surfaceCombination8.addSurfaceConfiguration(
+ SurfaceConfiguration.create(ConfigurationType.JPEG, ConfigurationSize.MAXIMUM));
+ surfaceCombination8.addSurfaceConfiguration(
+ SurfaceConfiguration.create(ConfigurationType.RAW, ConfigurationSize.MAXIMUM));
+ combinationList.add(surfaceCombination8);
+
+ return combinationList;
+ }
+
+ List<SurfaceCombination> getBurstSupportedCombinationList() {
+ List<SurfaceCombination> combinationList = new ArrayList<>();
+
+ // (PRIV, PREVIEW) + (PRIV, MAXIMUM)
+ SurfaceCombination surfaceCombination1 = new SurfaceCombination();
+ surfaceCombination1.addSurfaceConfiguration(
+ SurfaceConfiguration.create(ConfigurationType.PRIV, ConfigurationSize.PREVIEW));
+ surfaceCombination1.addSurfaceConfiguration(
+ SurfaceConfiguration.create(ConfigurationType.PRIV, ConfigurationSize.MAXIMUM));
+ combinationList.add(surfaceCombination1);
+
+ // (PRIV, PREVIEW) + (YUV, MAXIMUM)
+ SurfaceCombination surfaceCombination2 = new SurfaceCombination();
+ surfaceCombination2.addSurfaceConfiguration(
+ SurfaceConfiguration.create(ConfigurationType.PRIV, ConfigurationSize.PREVIEW));
+ surfaceCombination2.addSurfaceConfiguration(
+ SurfaceConfiguration.create(ConfigurationType.YUV, ConfigurationSize.MAXIMUM));
+ combinationList.add(surfaceCombination2);
+
+ // (YUV, PREVIEW) + (YUV, MAXIMUM)
+ SurfaceCombination surfaceCombination3 = new SurfaceCombination();
+ surfaceCombination3.addSurfaceConfiguration(
+ SurfaceConfiguration.create(ConfigurationType.YUV, ConfigurationSize.PREVIEW));
+ surfaceCombination3.addSurfaceConfiguration(
+ SurfaceConfiguration.create(ConfigurationType.YUV, ConfigurationSize.MAXIMUM));
+ combinationList.add(surfaceCombination3);
+
+ return combinationList;
+ }
+
+ List<SurfaceCombination> getLevel3SupportedCombinationList() {
+ List<SurfaceCombination> combinationList = new ArrayList<>();
+
+ // (PRIV, PREVIEW) + (PRIV, ANALYSIS) + (YUV, MAXIMUM) + (RAW, MAXIMUM)
+ SurfaceCombination surfaceCombination1 = new SurfaceCombination();
+ surfaceCombination1.addSurfaceConfiguration(
+ SurfaceConfiguration.create(ConfigurationType.PRIV, ConfigurationSize.PREVIEW));
+ surfaceCombination1.addSurfaceConfiguration(
+ SurfaceConfiguration.create(ConfigurationType.PRIV, ConfigurationSize.ANALYSIS));
+ surfaceCombination1.addSurfaceConfiguration(
+ SurfaceConfiguration.create(ConfigurationType.YUV, ConfigurationSize.MAXIMUM));
+ surfaceCombination1.addSurfaceConfiguration(
+ SurfaceConfiguration.create(ConfigurationType.RAW, ConfigurationSize.MAXIMUM));
+ combinationList.add(surfaceCombination1);
+
+ // (PRIV, PREVIEW) + (PRIV, ANALYSIS) + (JPEG, MAXIMUM) + (RAW, MAXIMUM)
+ SurfaceCombination surfaceCombination2 = new SurfaceCombination();
+ surfaceCombination2.addSurfaceConfiguration(
+ SurfaceConfiguration.create(ConfigurationType.PRIV, ConfigurationSize.PREVIEW));
+ surfaceCombination2.addSurfaceConfiguration(
+ SurfaceConfiguration.create(ConfigurationType.PRIV, ConfigurationSize.ANALYSIS));
+ surfaceCombination2.addSurfaceConfiguration(
+ SurfaceConfiguration.create(ConfigurationType.JPEG, ConfigurationSize.MAXIMUM));
+ surfaceCombination2.addSurfaceConfiguration(
+ SurfaceConfiguration.create(ConfigurationType.RAW, ConfigurationSize.MAXIMUM));
+ combinationList.add(surfaceCombination2);
+
+ return combinationList;
+ }
+
+ private void generateSupportedCombinationList(CameraManager cameraManager)
+ throws CameraAccessException {
+ characteristics = cameraManager.getCameraCharacteristics(cameraId);
+
+ Integer keyValue = characteristics.get(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL);
+
+ if (keyValue != null) {
+ hardwareLevel = keyValue;
+ }
+
+ surfaceCombinationList.addAll(getLegacySupportedCombinationList());
+
+ if (hardwareLevel == CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED
+ || hardwareLevel == CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_FULL
+ || hardwareLevel == CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_3) {
+ surfaceCombinationList.addAll(getLimitedSupportedCombinationList());
+ }
+
+ if (hardwareLevel == CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_FULL
+ || hardwareLevel == CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_3) {
+ surfaceCombinationList.addAll(getFullSupportedCombinationList());
+ }
+
+ int[] availableCapabilities =
+ characteristics.get(CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES);
+
+ if (availableCapabilities != null) {
+ for (int capability : availableCapabilities) {
+ if (capability == CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_RAW) {
+ isRawSupported = true;
+ } else if (capability
+ == CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_BURST_CAPTURE) {
+ isBurstCaptureSupported = true;
+ }
+ }
+ }
+
+ if (isRawSupported) {
+ surfaceCombinationList.addAll(getRAWSupportedCombinationList());
+ }
+
+ if (isBurstCaptureSupported
+ && hardwareLevel == CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED) {
+ surfaceCombinationList.addAll(getBurstSupportedCombinationList());
+ }
+
+ if (hardwareLevel == CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_3) {
+ surfaceCombinationList.addAll(getLevel3SupportedCombinationList());
+ }
+ }
+
+ private void checkCustomization() {
+ // TODO(b/119466260): Integrate found feasible stream combinations into supported list
+ }
+
+ // Utility classes and methods:
+ // *********************************************************************************************
+
+ private void generateSurfaceSizeDefinition(WindowManager windowManager) {
+ Size analysisSize = new Size(640, 480);
+ Size previewSize = getPreviewSize(windowManager);
+ Size recordSize = getRecordSize();
+
+ Map<Integer, Size> maximumSizeMap = new HashMap<>();
+ maximumSizeMap.put(ImageFormat.JPEG, getMaxOutputSizeByFormat(ImageFormat.JPEG));
+ maximumSizeMap.put(
+ ImageFormat.YUV_420_888, getMaxOutputSizeByFormat(ImageFormat.YUV_420_888));
+ /**
+ * Except for ImageFormat.JPEG or ImageFormat.YUV, other image formats like {@link
+ * android.graphics.SurfaceTexture} or {@link android.media.MediaCodec} classes will be
+ * mapped to internal defined format HAL_PIXEL_FORMAT_IMPLEMENTATION_DEFINED (0x22) in
+ * StreamConfigurationMap.java. 0x22 is also the code for ImageFormat.PRIVATE that is public
+ * after Android level 23.Before Android level 23, there is same internal code 0x22 for
+ * internal defined format HAL_PIXEL_FORMAT_IMPLEMENTATION_DEFINED. Therefore, using the
+ * code 0x22 to store maximum size for ViewFinder or VideCapture use cases since they will
+ * finally map to this code.
+ */
+ maximumSizeMap.put(
+ ImageFormatConstants.INTERNAL_DEFINED_IMAGE_FORMAT_PRIVATE,
+ getMaxOutputSizeByFormat(
+ ImageFormatConstants.INTERNAL_DEFINED_IMAGE_FORMAT_PRIVATE));
+
+ surfaceSizeDefinition =
+ SurfaceSizeDefinition.create(analysisSize, previewSize, recordSize, maximumSizeMap);
+ }
+
+ /**
+ * PREVIEW refers to the best size match to the device's screen resolution, or to 1080p
+ * (1920x1080), whichever is smaller.
+ */
+ private Size getPreviewSize(WindowManager windowManager) {
+ Point displaySize = new Point();
+ windowManager.getDefaultDisplay().getRealSize(displaySize);
+
+ if (displaySize.x > displaySize.y) {
+ displayViewSize = new Size(displaySize.x, displaySize.y);
+ } else {
+ displayViewSize = new Size(displaySize.y, displaySize.x);
+ }
+
+ // Limit the max preview size to under min(display size, 1080P) by comparing the area size
+ Size previewSize =
+ Collections.min(
+ Arrays.asList(
+ new Size(displayViewSize.getWidth(), displayViewSize.getHeight()),
+ MAX_PREVIEW_SIZE),
+ new CompareSizesByArea());
+
+ return previewSize;
+ }
+
+ /**
+ * RECORD refers to the camera device's maximum supported recording resolution, as determined by
+ * CamcorderProfile.
+ */
+ private Size getRecordSize() {
+ Size recordSize = QUALITY_480P_SIZE;
+
+ // Check whether 2160P, 1080P, 720P, 480P are supported by CamcorderProfile
+ if (camcorderProfileHelper.hasProfile(
+ Integer.parseInt(cameraId), CamcorderProfile.QUALITY_2160P)) {
+ recordSize = QUALITY_2160P_SIZE;
+ } else if (camcorderProfileHelper.hasProfile(
+ Integer.parseInt(cameraId), CamcorderProfile.QUALITY_1080P)) {
+ recordSize = QUALITY_1080P_SIZE;
+ } else if (camcorderProfileHelper.hasProfile(
+ Integer.parseInt(cameraId), CamcorderProfile.QUALITY_720P)) {
+ recordSize = QUALITY_720P_SIZE;
+ } else if (camcorderProfileHelper.hasProfile(
+ Integer.parseInt(cameraId), CamcorderProfile.QUALITY_480P)) {
+ recordSize = QUALITY_480P_SIZE;
+ }
+
+ return recordSize;
+ }
+
+ /** Comparator based on area of the given {@link Size} objects. */
+ static final class CompareSizesByArea implements Comparator<Size> {
+ private boolean reverse = false;
+
+ CompareSizesByArea() {
+ }
+
+ CompareSizesByArea(boolean reverse) {
+ this.reverse = reverse;
+ }
+
+ @Override
+ public int compare(Size lhs, Size rhs) {
+ // We cast here to ensure the multiplications won't overflow
+ int result =
+ Long.signum(
+ (long) lhs.getWidth() * lhs.getHeight()
+ - (long) rhs.getWidth() * rhs.getHeight());
+
+ if (reverse) {
+ result *= -1;
+ }
+
+ return result;
+ }
+ }
+}
diff --git a/camera/camera2/src/main/java/androidx/camera/camera2/UseCaseSurfaceOccupancyManager.java b/camera/camera2/src/main/java/androidx/camera/camera2/UseCaseSurfaceOccupancyManager.java
new file mode 100644
index 0000000..6c48513
--- /dev/null
+++ b/camera/camera2/src/main/java/androidx/camera/camera2/UseCaseSurfaceOccupancyManager.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright (C) 2019 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.camera2;
+
+import androidx.camera.core.BaseUseCase;
+import androidx.camera.core.ImageCaptureUseCase;
+import androidx.camera.core.VideoCaptureUseCase;
+
+import java.util.List;
+
+/**
+ * Collect the use case surface occupancy customization rules in this class to make
+ * Camera2DeviceSurfaceManager independent from use case type.
+ */
+final class UseCaseSurfaceOccupancyManager {
+ private UseCaseSurfaceOccupancyManager() {
+ }
+
+ static void checkUseCaseLimitNotExceeded(
+ List<BaseUseCase> originalUseCases, List<BaseUseCase> newUseCases) {
+ int imageCaptureUseCaseCount = 0;
+ int videoCaptureUseCaseCount = 0;
+
+ if (newUseCases == null || newUseCases.isEmpty()) {
+ throw new IllegalArgumentException("No new use cases to be bound.");
+ }
+
+ if (originalUseCases != null) {
+ for (BaseUseCase useCase : originalUseCases) {
+ if (useCase instanceof ImageCaptureUseCase) {
+ imageCaptureUseCaseCount++;
+ } else if (useCase instanceof VideoCaptureUseCase) {
+ videoCaptureUseCaseCount++;
+ }
+ }
+ }
+
+ for (BaseUseCase useCase : newUseCases) {
+ if (useCase instanceof ImageCaptureUseCase) {
+ imageCaptureUseCaseCount++;
+ } else if (useCase instanceof VideoCaptureUseCase) {
+ videoCaptureUseCaseCount++;
+ }
+ }
+
+ if (imageCaptureUseCaseCount > 1) {
+ throw new IllegalArgumentException(
+ "Exceeded max simultaneously bound image capture use cases.");
+ }
+
+ if (videoCaptureUseCaseCount > 1) {
+ throw new IllegalArgumentException(
+ "Exceeded max simultaneously bound video capture use cases.");
+ }
+ }
+}
diff --git a/camera/camera2/src/test/java/androidx/camera/camera2/Camera2CameraInfoRobolectricTest.java b/camera/camera2/src/test/java/androidx/camera/camera2/Camera2CameraInfoRobolectricTest.java
new file mode 100644
index 0000000..82387b6
--- /dev/null
+++ b/camera/camera2/src/test/java/androidx/camera/camera2/Camera2CameraInfoRobolectricTest.java
@@ -0,0 +1,152 @@
+/*
+ * Copyright (C) 2019 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.camera2;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.hardware.camera2.CameraCharacteristics;
+import android.hardware.camera2.CameraManager;
+import android.view.Surface;
+
+import androidx.camera.core.CameraInfo;
+import androidx.camera.core.CameraInfoUnavailableException;
+import androidx.camera.core.CameraX.LensFacing;
+import androidx.test.core.app.ApplicationProvider;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.internal.DoNotInstrument;
+import org.robolectric.shadow.api.Shadow;
+import org.robolectric.shadows.ShadowCameraCharacteristics;
+import org.robolectric.shadows.ShadowCameraManager;
+
+@RunWith(RobolectricTestRunner.class)
+@DoNotInstrument
+public class Camera2CameraInfoRobolectricTest {
+
+ private static final String CAMERA0_ID = "0";
+ private static final int CAMERA0_SENSOR_ORIENTATION = 90;
+ private static final LensFacing CAMERA0_LENS_FACING_ENUM = LensFacing.BACK;
+ private static final int CAMERA0_LENS_FACING_INT = CameraCharacteristics.LENS_FACING_BACK;
+
+ private static final String CAMERA1_ID = "1";
+ private static final int CAMERA1_SENSOR_ORIENTATION = 0;
+ private static final int CAMERA1_LENS_FACING_INT = CameraCharacteristics.LENS_FACING_FRONT;
+
+ private CameraManager cameraManager;
+
+ @Before
+ public void setUp() {
+ initCameras();
+ cameraManager =
+ ApplicationProvider.getApplicationContext().getSystemService(CameraManager.class);
+ }
+
+ @Test
+ public void canCreateCameraInfo() throws CameraInfoUnavailableException {
+ CameraInfo cameraInfo = new Camera2CameraInfo(cameraManager, CAMERA0_ID);
+ assertThat(cameraInfo).isNotNull();
+ }
+
+ @Test
+ public void cameraInfo_canReturnSensorOrientation() throws CameraInfoUnavailableException {
+ CameraInfo cameraInfo = new Camera2CameraInfo(cameraManager, CAMERA0_ID);
+ assertThat(cameraInfo.getSensorRotationDegrees()).isEqualTo(CAMERA0_SENSOR_ORIENTATION);
+ }
+
+ @Test
+ public void cameraInfo_canCalculateCorrectRelativeRotation_forBackCamera()
+ throws CameraInfoUnavailableException {
+ CameraInfo cameraInfo = new Camera2CameraInfo(cameraManager, CAMERA0_ID);
+
+ // Note: these numbers depend on the camera being a back-facing camera.
+ assertThat(cameraInfo.getSensorRotationDegrees(Surface.ROTATION_0))
+ .isEqualTo(CAMERA0_SENSOR_ORIENTATION);
+ assertThat(cameraInfo.getSensorRotationDegrees(Surface.ROTATION_90))
+ .isEqualTo((CAMERA0_SENSOR_ORIENTATION - 90 + 360) % 360);
+ assertThat(cameraInfo.getSensorRotationDegrees(Surface.ROTATION_180))
+ .isEqualTo((CAMERA0_SENSOR_ORIENTATION - 180 + 360) % 360);
+ assertThat(cameraInfo.getSensorRotationDegrees(Surface.ROTATION_270))
+ .isEqualTo((CAMERA0_SENSOR_ORIENTATION - 270 + 360) % 360);
+ }
+
+ @Test
+ public void cameraInfo_canCalculateCorrectRelativeRotation_forFrontCamera()
+ throws CameraInfoUnavailableException {
+ CameraInfo cameraInfo = new Camera2CameraInfo(cameraManager, CAMERA1_ID);
+
+ // Note: these numbers depend on the camera being a front-facing camera.
+ assertThat(cameraInfo.getSensorRotationDegrees(Surface.ROTATION_0))
+ .isEqualTo(CAMERA1_SENSOR_ORIENTATION);
+ assertThat(cameraInfo.getSensorRotationDegrees(Surface.ROTATION_90))
+ .isEqualTo((CAMERA1_SENSOR_ORIENTATION + 90) % 360);
+ assertThat(cameraInfo.getSensorRotationDegrees(Surface.ROTATION_180))
+ .isEqualTo((CAMERA1_SENSOR_ORIENTATION + 180) % 360);
+ assertThat(cameraInfo.getSensorRotationDegrees(Surface.ROTATION_270))
+ .isEqualTo((CAMERA1_SENSOR_ORIENTATION + 270) % 360);
+ }
+
+ @Test
+ public void cameraInfo_canReturnLensFacing() throws CameraInfoUnavailableException {
+ CameraInfo cameraInfo = new Camera2CameraInfo(cameraManager, CAMERA0_ID);
+ assertThat(cameraInfo.getLensFacing()).isEqualTo(CAMERA0_LENS_FACING_ENUM);
+ }
+
+ private void initCameras() {
+ // **** Camera 0 characteristics ****//
+ CameraCharacteristics characteristics0 =
+ ShadowCameraCharacteristics.newCameraCharacteristics();
+
+ ShadowCameraCharacteristics shadowCharacteristics0 = Shadow.extract(characteristics0);
+
+ // Add a lens facing to the camera
+ shadowCharacteristics0.set(CameraCharacteristics.LENS_FACING, CAMERA0_LENS_FACING_INT);
+
+ // Mock the sensor orientation
+ shadowCharacteristics0.set(
+ CameraCharacteristics.SENSOR_ORIENTATION, CAMERA0_SENSOR_ORIENTATION);
+
+ // Add the camera to the camera service
+ ((ShadowCameraManager)
+ Shadow.extract(
+ ApplicationProvider.getApplicationContext()
+ .getSystemService(CameraManager.class)))
+ .addCamera(CAMERA0_ID, characteristics0);
+
+ // **** Camera 1 characteristics ****//
+ CameraCharacteristics characteristics1 =
+ ShadowCameraCharacteristics.newCameraCharacteristics();
+
+ ShadowCameraCharacteristics shadowCharacteristics1 = Shadow.extract(characteristics1);
+
+ // Add a lens facing to the camera
+ shadowCharacteristics1.set(CameraCharacteristics.LENS_FACING, CAMERA1_LENS_FACING_INT);
+
+ // Mock the sensor orientation
+ shadowCharacteristics1.set(
+ CameraCharacteristics.SENSOR_ORIENTATION, CAMERA1_SENSOR_ORIENTATION);
+
+ // Add the camera to the camera service
+ ((ShadowCameraManager)
+ Shadow.extract(
+ ApplicationProvider.getApplicationContext()
+ .getSystemService(CameraManager.class)))
+ .addCamera(CAMERA1_ID, characteristics1);
+ }
+}
diff --git a/camera/camera2/src/test/java/androidx/camera/camera2/Camera2DeviceSurfaceManagerRobolectricTest.java b/camera/camera2/src/test/java/androidx/camera/camera2/Camera2DeviceSurfaceManagerRobolectricTest.java
new file mode 100644
index 0000000..5acb00b
--- /dev/null
+++ b/camera/camera2/src/test/java/androidx/camera/camera2/Camera2DeviceSurfaceManagerRobolectricTest.java
@@ -0,0 +1,572 @@
+/*
+ * Copyright (C) 2019 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.camera2;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static junit.framework.Assert.assertEquals;
+import static junit.framework.Assert.assertFalse;
+import static junit.framework.Assert.assertTrue;
+
+import static org.mockito.Matchers.anyInt;
+import static org.mockito.Mockito.when;
+import static org.robolectric.RuntimeEnvironment.application;
+
+import android.content.Context;
+import android.graphics.ImageFormat;
+import android.hardware.camera2.CameraCharacteristics;
+import android.os.Build.VERSION_CODES;
+import android.util.Rational;
+import android.util.Size;
+import android.view.WindowManager;
+
+import androidx.camera.core.AppConfiguration;
+import androidx.camera.core.BaseUseCase;
+import androidx.camera.core.CameraDeviceSurfaceManager;
+import androidx.camera.core.CameraFactory;
+import androidx.camera.core.CameraX;
+import androidx.camera.core.CameraX.LensFacing;
+import androidx.camera.core.ExtendableUseCaseConfigFactory;
+import androidx.camera.core.ImageAnalysisUseCaseConfiguration;
+import androidx.camera.core.ImageCaptureUseCase;
+import androidx.camera.core.ImageCaptureUseCaseConfiguration;
+import androidx.camera.core.ImageFormatConstants;
+import androidx.camera.core.StreamConfigurationMapUtil;
+import androidx.camera.core.SurfaceCombination;
+import androidx.camera.core.SurfaceConfiguration;
+import androidx.camera.core.SurfaceConfiguration.ConfigurationSize;
+import androidx.camera.core.SurfaceConfiguration.ConfigurationType;
+import androidx.camera.core.VideoCaptureUseCase;
+import androidx.camera.core.VideoCaptureUseCaseConfiguration;
+import androidx.camera.core.ViewFinderUseCase;
+import androidx.camera.core.ViewFinderUseCaseConfiguration;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mockito;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.Shadows;
+import org.robolectric.annotation.Config;
+import org.robolectric.annotation.internal.DoNotInstrument;
+import org.robolectric.shadow.api.Shadow;
+import org.robolectric.shadows.ShadowCameraCharacteristics;
+import org.robolectric.shadows.ShadowCameraManager;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+/** Robolectric test for {@link Camera2DeviceSurfaceManager} class */
+@RunWith(RobolectricTestRunner.class)
+@DoNotInstrument
+@Config(minSdk = VERSION_CODES.LOLLIPOP)
+public final class Camera2DeviceSurfaceManagerRobolectricTest {
+ private static final String LEGACY_CAMERA_ID = "0";
+ private static final String LIMITED_CAMERA_ID = "1";
+ private static final String FULL_CAMERA_ID = "2";
+ private static final String LEVEL3_CAMERA_ID = "3";
+ private final Size displaySize = new Size(1280, 720);
+ private final Size analysisSize = new Size(640, 480);
+ private final Size previewSize = displaySize;
+ private final Size recordSize = new Size(3840, 2160);
+ private final Size maximumSize = new Size(4032, 3024);
+ private final Size maximumVideoSize = new Size(1920, 1080);
+ private final CamcorderProfileHelper mockCamcorderProfileHelper =
+ Mockito.mock(CamcorderProfileHelper.class);
+
+ /**
+ * Except for ImageFormat.JPEG or ImageFormat.YUV, other image formats will be mapped to
+ * ImageFormat.PRIVATE (0x22) including SurfaceTexture or MediaCodec classes. Before Android
+ * level 23, there is no ImageFormat.PRIVATE. But there is same internal code 0x22 for internal
+ * corresponding format HAL_PIXEL_FORMAT_IMPLEMENTATION_DEFINED. Therefore, set 0x22 as default
+ * image formate.
+ */
+ private final int[] supportedFormats =
+ new int[]{
+ ImageFormat.YUV_420_888,
+ ImageFormatConstants.INTERNAL_DEFINED_IMAGE_FORMAT_JPEG,
+ ImageFormatConstants.INTERNAL_DEFINED_IMAGE_FORMAT_PRIVATE
+ };
+
+ private final Size[] supportedSizes =
+ new Size[]{
+ new Size(4032, 3024),
+ new Size(3840, 2160),
+ new Size(1920, 1080),
+ new Size(1280, 720),
+ new Size(640, 480),
+ new Size(320, 240),
+ new Size(320, 180)
+ };
+
+ private final Context context = RuntimeEnvironment.application.getApplicationContext();
+ private CameraDeviceSurfaceManager surfaceManager;
+
+ @Before
+ public void setUp() {
+ WindowManager windowManager =
+ (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
+ Shadows.shadowOf(windowManager.getDefaultDisplay()).setRealWidth(displaySize.getWidth());
+ Shadows.shadowOf(windowManager.getDefaultDisplay()).setRealHeight(displaySize.getHeight());
+
+ when(mockCamcorderProfileHelper.hasProfile(anyInt(), anyInt())).thenReturn(true);
+
+ setupCamera();
+ }
+
+ @Test
+ public void checkLegacySurfaceCombinationSupportedInLegacyDevice() {
+ SupportedSurfaceCombination supportedSurfaceCombination =
+ new SupportedSurfaceCombination(
+ context, LEGACY_CAMERA_ID, mockCamcorderProfileHelper);
+
+ List<SurfaceCombination> combinationList =
+ supportedSurfaceCombination.getLegacySupportedCombinationList();
+
+ for (SurfaceCombination combination : combinationList) {
+ boolean isSupported =
+ surfaceManager.checkSupported(
+ LEGACY_CAMERA_ID, combination.getSurfaceConfigurationList());
+ assertTrue(isSupported);
+ }
+ }
+
+ @Test
+ public void checkLimitedSurfaceCombinationNotSupportedInLegacyDevice() {
+ SupportedSurfaceCombination supportedSurfaceCombination =
+ new SupportedSurfaceCombination(
+ context, LEGACY_CAMERA_ID, mockCamcorderProfileHelper);
+
+ List<SurfaceCombination> combinationList =
+ supportedSurfaceCombination.getLimitedSupportedCombinationList();
+
+ for (SurfaceCombination combination : combinationList) {
+ boolean isSupported =
+ surfaceManager.checkSupported(
+ LEGACY_CAMERA_ID, combination.getSurfaceConfigurationList());
+ assertFalse(isSupported);
+ }
+ }
+
+ @Test
+ public void checkFullSurfaceCombinationNotSupportedInLegacyDevice() {
+ SupportedSurfaceCombination supportedSurfaceCombination =
+ new SupportedSurfaceCombination(
+ context, LEGACY_CAMERA_ID, mockCamcorderProfileHelper);
+
+ List<SurfaceCombination> combinationList =
+ supportedSurfaceCombination.getFullSupportedCombinationList();
+
+ for (SurfaceCombination combination : combinationList) {
+ boolean isSupported =
+ surfaceManager.checkSupported(
+ LEGACY_CAMERA_ID, combination.getSurfaceConfigurationList());
+ assertFalse(isSupported);
+ }
+ }
+
+ @Test
+ public void checkLevel3SurfaceCombinationNotSupportedInLegacyDevice() {
+ SupportedSurfaceCombination supportedSurfaceCombination =
+ new SupportedSurfaceCombination(
+ context, LEGACY_CAMERA_ID, mockCamcorderProfileHelper);
+
+ List<SurfaceCombination> combinationList =
+ supportedSurfaceCombination.getLevel3SupportedCombinationList();
+
+ for (SurfaceCombination combination : combinationList) {
+ boolean isSupported =
+ surfaceManager.checkSupported(
+ LEGACY_CAMERA_ID, combination.getSurfaceConfigurationList());
+ assertFalse(isSupported);
+ }
+ }
+
+ @Test
+ public void checkLimitedSurfaceCombinationSupportedInLimitedDevice() {
+ SupportedSurfaceCombination supportedSurfaceCombination =
+ new SupportedSurfaceCombination(
+ context, LIMITED_CAMERA_ID, mockCamcorderProfileHelper);
+
+ List<SurfaceCombination> combinationList =
+ supportedSurfaceCombination.getLimitedSupportedCombinationList();
+
+ for (SurfaceCombination combination : combinationList) {
+ boolean isSupported =
+ surfaceManager.checkSupported(
+ LIMITED_CAMERA_ID, combination.getSurfaceConfigurationList());
+ assertTrue(isSupported);
+ }
+ }
+
+ @Test
+ public void checkFullSurfaceCombinationNotSupportedInLimitedDevice() {
+ SupportedSurfaceCombination supportedSurfaceCombination =
+ new SupportedSurfaceCombination(
+ context, LIMITED_CAMERA_ID, mockCamcorderProfileHelper);
+
+ List<SurfaceCombination> combinationList =
+ supportedSurfaceCombination.getFullSupportedCombinationList();
+
+ for (SurfaceCombination combination : combinationList) {
+ boolean isSupported =
+ surfaceManager.checkSupported(
+ LIMITED_CAMERA_ID, combination.getSurfaceConfigurationList());
+ assertFalse(isSupported);
+ }
+ }
+
+ @Test
+ public void checkLevel3SurfaceCombinationNotSupportedInLimitedDevice() {
+ SupportedSurfaceCombination supportedSurfaceCombination =
+ new SupportedSurfaceCombination(
+ context, LIMITED_CAMERA_ID, mockCamcorderProfileHelper);
+
+ List<SurfaceCombination> combinationList =
+ supportedSurfaceCombination.getLevel3SupportedCombinationList();
+
+ for (SurfaceCombination combination : combinationList) {
+ boolean isSupported =
+ surfaceManager.checkSupported(
+ LIMITED_CAMERA_ID, combination.getSurfaceConfigurationList());
+ assertFalse(isSupported);
+ }
+ }
+
+ @Test
+ public void checkFullSurfaceCombinationSupportedInFullDevice() {
+ SupportedSurfaceCombination supportedSurfaceCombination =
+ new SupportedSurfaceCombination(
+ context, FULL_CAMERA_ID, mockCamcorderProfileHelper);
+
+ List<SurfaceCombination> combinationList =
+ supportedSurfaceCombination.getFullSupportedCombinationList();
+
+ for (SurfaceCombination combination : combinationList) {
+ boolean isSupported =
+ surfaceManager.checkSupported(
+ FULL_CAMERA_ID, combination.getSurfaceConfigurationList());
+ assertTrue(isSupported);
+ }
+ }
+
+ @Test
+ public void checkLevel3SurfaceCombinationNotSupportedInFullDevice() {
+ SupportedSurfaceCombination supportedSurfaceCombination =
+ new SupportedSurfaceCombination(
+ context, FULL_CAMERA_ID, mockCamcorderProfileHelper);
+
+ List<SurfaceCombination> combinationList =
+ supportedSurfaceCombination.getLevel3SupportedCombinationList();
+
+ for (SurfaceCombination combination : combinationList) {
+ boolean isSupported =
+ surfaceManager.checkSupported(
+ FULL_CAMERA_ID, combination.getSurfaceConfigurationList());
+ assertFalse(isSupported);
+ }
+ }
+
+ @Test
+ public void checkLevel3SurfaceCombinationSupportedInLevel3Device() {
+ SupportedSurfaceCombination supportedSurfaceCombination =
+ new SupportedSurfaceCombination(
+ context, LEVEL3_CAMERA_ID, mockCamcorderProfileHelper);
+
+ List<SurfaceCombination> combinationList =
+ supportedSurfaceCombination.getLevel3SupportedCombinationList();
+
+ for (SurfaceCombination combination : combinationList) {
+ boolean isSupported =
+ surfaceManager.checkSupported(
+ LEVEL3_CAMERA_ID, combination.getSurfaceConfigurationList());
+ assertTrue(isSupported);
+ }
+ }
+
+ @Test
+ public void suggestedResolutionsForMixedUseCaseNotSupportedInLegacyDevice() {
+ Rational aspectRatio = new Rational(16, 9);
+ ViewFinderUseCaseConfiguration.Builder viewFinderConfigBuilder =
+ new ViewFinderUseCaseConfiguration.Builder();
+ VideoCaptureUseCaseConfiguration.Builder videoCaptureConfigBuilder =
+ new VideoCaptureUseCaseConfiguration.Builder();
+ ImageCaptureUseCaseConfiguration.Builder imageCaptureConfigBuilder =
+ new ImageCaptureUseCaseConfiguration.Builder();
+
+ viewFinderConfigBuilder.setTargetAspectRatio(aspectRatio);
+ videoCaptureConfigBuilder.setTargetAspectRatio(aspectRatio);
+ imageCaptureConfigBuilder.setTargetAspectRatio(aspectRatio);
+
+ imageCaptureConfigBuilder.setLensFacing(LensFacing.BACK);
+ ImageCaptureUseCase imageCaptureUseCase =
+ new ImageCaptureUseCase(imageCaptureConfigBuilder.build());
+ videoCaptureConfigBuilder.setLensFacing(LensFacing.BACK);
+ VideoCaptureUseCase videoCaptureUseCase =
+ new VideoCaptureUseCase(videoCaptureConfigBuilder.build());
+ viewFinderConfigBuilder.setLensFacing(LensFacing.BACK);
+ ViewFinderUseCase viewFinderUseCase =
+ new ViewFinderUseCase(viewFinderConfigBuilder.build());
+
+ List<BaseUseCase> useCases = new ArrayList<>();
+ useCases.add(imageCaptureUseCase);
+ useCases.add(videoCaptureUseCase);
+ useCases.add(viewFinderUseCase);
+
+ boolean exceptionHappened = false;
+
+ try {
+ // Will throw IllegalArgumentException
+ surfaceManager.getSuggestedResolutions(LEGACY_CAMERA_ID, null, useCases);
+ } catch (IllegalArgumentException e) {
+ exceptionHappened = true;
+ }
+
+ assertTrue(exceptionHappened);
+ }
+
+ @Test
+ public void getSuggestedResolutionsForMixedUseCaseInLimitedDevice() {
+ Rational aspectRatio = new Rational(16, 9);
+ ViewFinderUseCaseConfiguration.Builder viewFinderConfigBuilder =
+ new ViewFinderUseCaseConfiguration.Builder();
+ VideoCaptureUseCaseConfiguration.Builder videoCaptureConfigBuilder =
+ new VideoCaptureUseCaseConfiguration.Builder();
+ ImageCaptureUseCaseConfiguration.Builder imageCaptureConfigBuilder =
+ new ImageCaptureUseCaseConfiguration.Builder();
+
+ viewFinderConfigBuilder.setTargetAspectRatio(aspectRatio);
+ videoCaptureConfigBuilder.setTargetAspectRatio(aspectRatio);
+ imageCaptureConfigBuilder.setTargetAspectRatio(aspectRatio);
+
+ imageCaptureConfigBuilder.setLensFacing(LensFacing.BACK);
+ ImageCaptureUseCase imageCaptureUseCase =
+ new ImageCaptureUseCase(imageCaptureConfigBuilder.build());
+ videoCaptureConfigBuilder.setLensFacing(LensFacing.BACK);
+ VideoCaptureUseCase videoCaptureUseCase =
+ new VideoCaptureUseCase(videoCaptureConfigBuilder.build());
+ viewFinderConfigBuilder.setLensFacing(LensFacing.BACK);
+ ViewFinderUseCase viewFinderUseCase =
+ new ViewFinderUseCase(viewFinderConfigBuilder.build());
+
+ List<BaseUseCase> useCases = new ArrayList<>();
+ useCases.add(imageCaptureUseCase);
+ useCases.add(videoCaptureUseCase);
+ useCases.add(viewFinderUseCase);
+ Map<BaseUseCase, Size> suggestedResolutionMap =
+ surfaceManager.getSuggestedResolutions(LIMITED_CAMERA_ID, null, useCases);
+
+ // (PRIV, PREVIEW) + (PRIV, RECORD) + (JPEG, RECORD)
+ assertThat(suggestedResolutionMap).containsEntry(imageCaptureUseCase, recordSize);
+ assertThat(suggestedResolutionMap).containsEntry(videoCaptureUseCase, maximumVideoSize);
+ assertThat(suggestedResolutionMap).containsEntry(viewFinderUseCase, previewSize);
+ }
+
+ @Test
+ public void transformSurfaceConfigurationWithYUVAnalysisSize() {
+ SurfaceConfiguration surfaceConfiguration =
+ surfaceManager.transformSurfaceConfiguration(
+ LEGACY_CAMERA_ID, ImageFormat.YUV_420_888, analysisSize);
+ SurfaceConfiguration expectedSurfaceConfiguration =
+ SurfaceConfiguration.create(ConfigurationType.YUV, ConfigurationSize.ANALYSIS);
+ assertEquals(expectedSurfaceConfiguration, surfaceConfiguration);
+ }
+
+ @Test
+ public void transformSurfaceConfigurationWithYUVPreviewSize() {
+ SurfaceConfiguration surfaceConfiguration =
+ surfaceManager.transformSurfaceConfiguration(
+ LEGACY_CAMERA_ID, ImageFormat.YUV_420_888, previewSize);
+ SurfaceConfiguration expectedSurfaceConfiguration =
+ SurfaceConfiguration.create(ConfigurationType.YUV, ConfigurationSize.PREVIEW);
+ assertEquals(expectedSurfaceConfiguration, surfaceConfiguration);
+ }
+
+ @Test
+ public void transformSurfaceConfigurationWithYUVRecordSize() {
+ SurfaceConfiguration surfaceConfiguration =
+ surfaceManager.transformSurfaceConfiguration(
+ LEGACY_CAMERA_ID, ImageFormat.YUV_420_888, recordSize);
+ SurfaceConfiguration expectedSurfaceConfiguration =
+ SurfaceConfiguration.create(ConfigurationType.YUV, ConfigurationSize.RECORD);
+ assertEquals(expectedSurfaceConfiguration, surfaceConfiguration);
+ }
+
+ @Test
+ public void transformSurfaceConfigurationWithYUVMaximumSize() {
+ SurfaceConfiguration surfaceConfiguration =
+ surfaceManager.transformSurfaceConfiguration(
+ LEGACY_CAMERA_ID, ImageFormat.YUV_420_888, maximumSize);
+ SurfaceConfiguration expectedSurfaceConfiguration =
+ SurfaceConfiguration.create(ConfigurationType.YUV, ConfigurationSize.MAXIMUM);
+ assertEquals(expectedSurfaceConfiguration, surfaceConfiguration);
+ }
+
+ @Test
+ public void transformSurfaceConfigurationWithYUVNotSupportSize() {
+ SurfaceConfiguration surfaceConfiguration =
+ surfaceManager.transformSurfaceConfiguration(
+ LEGACY_CAMERA_ID,
+ ImageFormat.YUV_420_888,
+ new Size(maximumSize.getWidth() + 1, maximumSize.getHeight() + 1));
+ SurfaceConfiguration expectedSurfaceConfiguration =
+ SurfaceConfiguration.create(ConfigurationType.YUV, ConfigurationSize.NOT_SUPPORT);
+ assertEquals(expectedSurfaceConfiguration, surfaceConfiguration);
+ }
+
+ @Test
+ public void transformSurfaceConfigurationWithJPEGAnalysisSize() {
+ SurfaceConfiguration surfaceConfiguration =
+ surfaceManager.transformSurfaceConfiguration(
+ LEGACY_CAMERA_ID, ImageFormat.JPEG, analysisSize);
+ SurfaceConfiguration expectedSurfaceConfiguration =
+ SurfaceConfiguration.create(ConfigurationType.JPEG, ConfigurationSize.ANALYSIS);
+ assertEquals(expectedSurfaceConfiguration, surfaceConfiguration);
+ }
+
+ @Test
+ public void transformSurfaceConfigurationWithJPEGPreviewSize() {
+ SurfaceConfiguration surfaceConfiguration =
+ surfaceManager.transformSurfaceConfiguration(
+ LEGACY_CAMERA_ID, ImageFormat.JPEG, previewSize);
+ SurfaceConfiguration expectedSurfaceConfiguration =
+ SurfaceConfiguration.create(ConfigurationType.JPEG, ConfigurationSize.PREVIEW);
+ assertEquals(expectedSurfaceConfiguration, surfaceConfiguration);
+ }
+
+ @Test
+ public void transformSurfaceConfigurationWithJPEGRecordSize() {
+ SurfaceConfiguration surfaceConfiguration =
+ surfaceManager.transformSurfaceConfiguration(
+ LEGACY_CAMERA_ID, ImageFormat.JPEG, recordSize);
+ SurfaceConfiguration expectedSurfaceConfiguration =
+ SurfaceConfiguration.create(ConfigurationType.JPEG, ConfigurationSize.RECORD);
+ assertEquals(expectedSurfaceConfiguration, surfaceConfiguration);
+ }
+
+ @Test
+ public void transformSurfaceConfigurationWithJPEGMaximumSize() {
+ SurfaceConfiguration surfaceConfiguration =
+ surfaceManager.transformSurfaceConfiguration(
+ LEGACY_CAMERA_ID, ImageFormat.JPEG, maximumSize);
+ SurfaceConfiguration expectedSurfaceConfiguration =
+ SurfaceConfiguration.create(ConfigurationType.JPEG, ConfigurationSize.MAXIMUM);
+ assertEquals(expectedSurfaceConfiguration, surfaceConfiguration);
+ }
+
+ @Test
+ public void transformSurfaceConfigurationWithJPEGNotSupportSize() {
+ SurfaceConfiguration surfaceConfiguration =
+ surfaceManager.transformSurfaceConfiguration(
+ LEGACY_CAMERA_ID,
+ ImageFormat.JPEG,
+ new Size(maximumSize.getWidth() + 1, maximumSize.getHeight() + 1));
+ SurfaceConfiguration expectedSurfaceConfiguration =
+ SurfaceConfiguration.create(ConfigurationType.JPEG, ConfigurationSize.NOT_SUPPORT);
+ assertEquals(expectedSurfaceConfiguration, surfaceConfiguration);
+ }
+
+ @Test
+ public void getMaximumSizeForImageFormat() {
+ Size maximumYUVSize =
+ surfaceManager.getMaxOutputSize(LEGACY_CAMERA_ID, ImageFormat.YUV_420_888);
+ assertEquals(maximumSize, maximumYUVSize);
+ Size maximumJPEGSize = surfaceManager.getMaxOutputSize(LEGACY_CAMERA_ID, ImageFormat.JPEG);
+ assertEquals(maximumSize, maximumJPEGSize);
+ }
+
+ private void setupCamera() {
+ addBackFacingCamera(
+ LEGACY_CAMERA_ID, CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY, null);
+ addBackFacingCamera(
+ LIMITED_CAMERA_ID,
+ CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED,
+ null);
+ addBackFacingCamera(
+ FULL_CAMERA_ID, CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_FULL, null);
+ addBackFacingCamera(
+ LEVEL3_CAMERA_ID, CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_3, null);
+ initCameraX();
+ }
+
+ private void addBackFacingCamera(String cameraId, int hardwareLevel, int[] capabilities) {
+ CameraCharacteristics characteristics =
+ ShadowCameraCharacteristics.newCameraCharacteristics();
+
+ ShadowCameraCharacteristics shadowCharacteristics = Shadow.extract(characteristics);
+
+ shadowCharacteristics.set(
+ CameraCharacteristics.LENS_FACING, CameraCharacteristics.LENS_FACING_BACK);
+
+ shadowCharacteristics.set(
+ CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL, hardwareLevel);
+
+ if (capabilities != null) {
+ shadowCharacteristics.set(
+ CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES, capabilities);
+ }
+
+ ((ShadowCameraManager) Shadow.extract(application.getSystemService(Context.CAMERA_SERVICE)))
+ .addCamera(cameraId, characteristics);
+
+ shadowCharacteristics.set(
+ CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP,
+ StreamConfigurationMapUtil.generateFakeStreamConfigurationMap(
+ supportedFormats, supportedSizes));
+ }
+
+ private void initCameraX() {
+ AppConfiguration appConfig = createFakeAppConfiguration();
+ CameraX.init(context, appConfig);
+ surfaceManager = CameraX.getSurfaceManager();
+ }
+
+ private AppConfiguration createFakeAppConfiguration() {
+
+ // Create the camera factory for creating Camera2 camera objects
+ CameraFactory cameraFactory = new Camera2CameraFactory(context);
+
+ // Create the DeviceSurfaceManager for Camera2
+ CameraDeviceSurfaceManager surfaceManager =
+ new Camera2DeviceSurfaceManager(context, mockCamcorderProfileHelper);
+
+ // Create default configuration factory
+ ExtendableUseCaseConfigFactory configFactory = new ExtendableUseCaseConfigFactory();
+ configFactory.installDefaultProvider(
+ ImageAnalysisUseCaseConfiguration.class,
+ new DefaultImageAnalysisConfigurationProvider(cameraFactory));
+ configFactory.installDefaultProvider(
+ ImageCaptureUseCaseConfiguration.class,
+ new DefaultImageCaptureConfigurationProvider(cameraFactory));
+ configFactory.installDefaultProvider(
+ VideoCaptureUseCaseConfiguration.class,
+ new DefaultVideoCaptureConfigurationProvider(cameraFactory));
+ configFactory.installDefaultProvider(
+ ViewFinderUseCaseConfiguration.class,
+ new DefaultViewFinderConfigurationProvider(cameraFactory));
+
+ AppConfiguration.Builder appConfigBuilder =
+ new AppConfiguration.Builder()
+ .setCameraFactory(cameraFactory)
+ .setDeviceSurfaceManager(surfaceManager)
+ .setUseCaseConfigFactory(configFactory);
+
+ return appConfigBuilder.build();
+ }
+}
diff --git a/camera/camera2/src/test/java/androidx/camera/camera2/SupportedSurfaceCombinationRobolectricTest.java b/camera/camera2/src/test/java/androidx/camera/camera2/SupportedSurfaceCombinationRobolectricTest.java
new file mode 100644
index 0000000..50abf6e
--- /dev/null
+++ b/camera/camera2/src/test/java/androidx/camera/camera2/SupportedSurfaceCombinationRobolectricTest.java
@@ -0,0 +1,651 @@
+/*
+ * Copyright (C) 2019 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.camera2;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static junit.framework.Assert.assertEquals;
+import static junit.framework.Assert.assertFalse;
+import static junit.framework.Assert.assertTrue;
+
+import static org.mockito.Matchers.anyInt;
+import static org.mockito.Mockito.when;
+import static org.robolectric.RuntimeEnvironment.application;
+
+import android.content.Context;
+import android.graphics.ImageFormat;
+import android.hardware.camera2.CameraCharacteristics;
+import android.os.Build.VERSION_CODES;
+import android.util.Rational;
+import android.util.Size;
+import android.view.WindowManager;
+
+import androidx.camera.core.AppConfiguration;
+import androidx.camera.core.BaseUseCase;
+import androidx.camera.core.CameraX;
+import androidx.camera.core.CameraX.LensFacing;
+import androidx.camera.core.ImageCaptureUseCase;
+import androidx.camera.core.ImageCaptureUseCaseConfiguration;
+import androidx.camera.core.ImageFormatConstants;
+import androidx.camera.core.StreamConfigurationMapUtil;
+import androidx.camera.core.SurfaceCombination;
+import androidx.camera.core.SurfaceConfiguration;
+import androidx.camera.core.SurfaceConfiguration.ConfigurationSize;
+import androidx.camera.core.SurfaceConfiguration.ConfigurationType;
+import androidx.camera.core.VideoCaptureUseCase;
+import androidx.camera.core.VideoCaptureUseCaseConfiguration;
+import androidx.camera.core.ViewFinderUseCase;
+import androidx.camera.core.ViewFinderUseCaseConfiguration;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mockito;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.Shadows;
+import org.robolectric.annotation.Config;
+import org.robolectric.annotation.internal.DoNotInstrument;
+import org.robolectric.shadow.api.Shadow;
+import org.robolectric.shadows.ShadowCameraCharacteristics;
+import org.robolectric.shadows.ShadowCameraManager;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+/** Robolectric test for {@link SupportedSurfaceCombination} class */
+@RunWith(RobolectricTestRunner.class)
+@DoNotInstrument
+@Config(minSdk = VERSION_CODES.LOLLIPOP)
+public final class SupportedSurfaceCombinationRobolectricTest {
+ private static final String LEGACY_CAMERA_ID = "0";
+ private static final String LIMITED_CAMERA_ID = "1";
+ private static final String FULL_CAMERA_ID = "2";
+ private static final String LEVEL3_CAMERA_ID = "3";
+ private final Size displaySize = new Size(1280, 720);
+ private final Size analysisSize = new Size(640, 480);
+ private final Size previewSize = displaySize;
+ private final Size recordSize = new Size(3840, 2160);
+ private final Size maximumSize = new Size(4032, 3024);
+ private final Size maximumVideoSize = new Size(1920, 1080);
+ private final CamcorderProfileHelper mockCamcorderProfileHelper =
+ Mockito.mock(CamcorderProfileHelper.class);
+
+ /**
+ * Except for ImageFormat.JPEG or ImageFormat.YUV, other image formats will be mapped to
+ * ImageFormat.PRIVATE (0x22) including SurfaceTexture or MediaCodec classes. Before Android
+ * level 23, there is no ImageFormat.PRIVATE. But there is same internal code 0x22 for internal
+ * corresponding format HAL_PIXEL_FORMAT_IMPLEMENTATION_DEFINED. Therefore, set 0x22 as default
+ * image formate.
+ */
+ private final int[] supportedFormats =
+ new int[]{
+ ImageFormat.YUV_420_888,
+ ImageFormatConstants.INTERNAL_DEFINED_IMAGE_FORMAT_JPEG,
+ ImageFormatConstants.INTERNAL_DEFINED_IMAGE_FORMAT_PRIVATE
+ };
+
+ private final Size[] supportedSizes =
+ new Size[]{
+ new Size(4032, 3024),
+ new Size(3840, 2160),
+ new Size(1920, 1080),
+ new Size(1280, 720),
+ new Size(640, 480),
+ new Size(320, 240),
+ new Size(320, 180)
+ };
+
+ private final Context context = RuntimeEnvironment.application.getApplicationContext();
+
+ @Before
+ public void setUp() {
+ WindowManager windowManager =
+ (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
+ Shadows.shadowOf(windowManager.getDefaultDisplay()).setRealWidth(displaySize.getWidth());
+ Shadows.shadowOf(windowManager.getDefaultDisplay()).setRealHeight(displaySize.getHeight());
+
+ when(mockCamcorderProfileHelper.hasProfile(anyInt(), anyInt())).thenReturn(true);
+
+ setupCamera();
+ }
+
+ @Test
+ public void checkLegacySurfaceCombinationSupportedInLegacyDevice() {
+ SupportedSurfaceCombination supportedSurfaceCombination =
+ new SupportedSurfaceCombination(
+ context, LEGACY_CAMERA_ID, mockCamcorderProfileHelper);
+
+ List<SurfaceCombination> combinationList =
+ supportedSurfaceCombination.getLegacySupportedCombinationList();
+
+ for (SurfaceCombination combination : combinationList) {
+ boolean isSupported =
+ supportedSurfaceCombination.checkSupported(
+ combination.getSurfaceConfigurationList());
+ assertTrue(isSupported);
+ }
+ }
+
+ @Test
+ public void checkLegacySurfaceCombinationSubListSupportedInLegacyDevice() {
+ SupportedSurfaceCombination supportedSurfaceCombination =
+ new SupportedSurfaceCombination(
+ context, LEGACY_CAMERA_ID, mockCamcorderProfileHelper);
+
+ List<SurfaceCombination> combinationList =
+ supportedSurfaceCombination.getLegacySupportedCombinationList();
+
+ boolean isSupported =
+ isAllSubConfigurationListSupported(supportedSurfaceCombination, combinationList);
+ assertTrue(isSupported);
+ }
+
+ @Test
+ public void checkLimitedSurfaceCombinationNotSupportedInLegacyDevice() {
+ SupportedSurfaceCombination supportedSurfaceCombination =
+ new SupportedSurfaceCombination(
+ context, LEGACY_CAMERA_ID, mockCamcorderProfileHelper);
+
+ List<SurfaceCombination> combinationList =
+ supportedSurfaceCombination.getLimitedSupportedCombinationList();
+
+ for (SurfaceCombination combination : combinationList) {
+ boolean isSupported =
+ supportedSurfaceCombination.checkSupported(
+ combination.getSurfaceConfigurationList());
+ assertFalse(isSupported);
+ }
+ }
+
+ @Test
+ public void checkFullSurfaceCombinationNotSupportedInLegacyDevice() {
+ SupportedSurfaceCombination supportedSurfaceCombination =
+ new SupportedSurfaceCombination(
+ context, LEGACY_CAMERA_ID, mockCamcorderProfileHelper);
+
+ List<SurfaceCombination> combinationList =
+ supportedSurfaceCombination.getFullSupportedCombinationList();
+
+ for (SurfaceCombination combination : combinationList) {
+ boolean isSupported =
+ supportedSurfaceCombination.checkSupported(
+ combination.getSurfaceConfigurationList());
+ assertFalse(isSupported);
+ }
+ }
+
+ @Test
+ public void checkLevel3SurfaceCombinationNotSupportedInLegacyDevice() {
+ SupportedSurfaceCombination supportedSurfaceCombination =
+ new SupportedSurfaceCombination(
+ context, LEGACY_CAMERA_ID, mockCamcorderProfileHelper);
+
+ List<SurfaceCombination> combinationList =
+ supportedSurfaceCombination.getLevel3SupportedCombinationList();
+
+ for (SurfaceCombination combination : combinationList) {
+ boolean isSupported =
+ supportedSurfaceCombination.checkSupported(
+ combination.getSurfaceConfigurationList());
+ assertFalse(isSupported);
+ }
+ }
+
+ @Test
+ public void checkLimitedSurfaceCombinationSupportedInLimitedDevice() {
+ SupportedSurfaceCombination supportedSurfaceCombination =
+ new SupportedSurfaceCombination(
+ context, LIMITED_CAMERA_ID, mockCamcorderProfileHelper);
+
+ List<SurfaceCombination> combinationList =
+ supportedSurfaceCombination.getLimitedSupportedCombinationList();
+
+ for (SurfaceCombination combination : combinationList) {
+ boolean isSupported =
+ supportedSurfaceCombination.checkSupported(
+ combination.getSurfaceConfigurationList());
+ assertTrue(isSupported);
+ }
+ }
+
+ @Test
+ public void checkLimitedSurfaceCombinationSubListSupportedInLimited3Device() {
+ SupportedSurfaceCombination supportedSurfaceCombination =
+ new SupportedSurfaceCombination(
+ context, LIMITED_CAMERA_ID, mockCamcorderProfileHelper);
+
+ List<SurfaceCombination> combinationList =
+ supportedSurfaceCombination.getLimitedSupportedCombinationList();
+
+ boolean isSupported =
+ isAllSubConfigurationListSupported(supportedSurfaceCombination, combinationList);
+ assertTrue(isSupported);
+ }
+
+ @Test
+ public void checkFullSurfaceCombinationNotSupportedInLimitedDevice() {
+ SupportedSurfaceCombination supportedSurfaceCombination =
+ new SupportedSurfaceCombination(
+ context, LIMITED_CAMERA_ID, mockCamcorderProfileHelper);
+
+ List<SurfaceCombination> combinationList =
+ supportedSurfaceCombination.getFullSupportedCombinationList();
+
+ for (SurfaceCombination combination : combinationList) {
+ boolean isSupported =
+ supportedSurfaceCombination.checkSupported(
+ combination.getSurfaceConfigurationList());
+ assertFalse(isSupported);
+ }
+ }
+
+ @Test
+ public void checkLevel3SurfaceCombinationNotSupportedInLimitedDevice() {
+ SupportedSurfaceCombination supportedSurfaceCombination =
+ new SupportedSurfaceCombination(
+ context, LIMITED_CAMERA_ID, mockCamcorderProfileHelper);
+
+ List<SurfaceCombination> combinationList =
+ supportedSurfaceCombination.getLevel3SupportedCombinationList();
+
+ for (SurfaceCombination combination : combinationList) {
+ boolean isSupported =
+ supportedSurfaceCombination.checkSupported(
+ combination.getSurfaceConfigurationList());
+ assertFalse(isSupported);
+ }
+ }
+
+ @Test
+ public void checkFullSurfaceCombinationSupportedInFullDevice() {
+ SupportedSurfaceCombination supportedSurfaceCombination =
+ new SupportedSurfaceCombination(
+ context, FULL_CAMERA_ID, mockCamcorderProfileHelper);
+
+ List<SurfaceCombination> combinationList =
+ supportedSurfaceCombination.getFullSupportedCombinationList();
+
+ for (SurfaceCombination combination : combinationList) {
+ boolean isSupported =
+ supportedSurfaceCombination.checkSupported(
+ combination.getSurfaceConfigurationList());
+ assertTrue(isSupported);
+ }
+ }
+
+ @Test
+ public void checkFullSurfaceCombinationSubListSupportedInFullDevice() {
+ SupportedSurfaceCombination supportedSurfaceCombination =
+ new SupportedSurfaceCombination(
+ context, FULL_CAMERA_ID, mockCamcorderProfileHelper);
+
+ List<SurfaceCombination> combinationList =
+ supportedSurfaceCombination.getFullSupportedCombinationList();
+
+ boolean isSupported =
+ isAllSubConfigurationListSupported(supportedSurfaceCombination, combinationList);
+ assertTrue(isSupported);
+ }
+
+ @Test
+ public void checkLevel3SurfaceCombinationNotSupportedInFullDevice() {
+ SupportedSurfaceCombination supportedSurfaceCombination =
+ new SupportedSurfaceCombination(
+ context, FULL_CAMERA_ID, mockCamcorderProfileHelper);
+
+ List<SurfaceCombination> combinationList =
+ supportedSurfaceCombination.getLevel3SupportedCombinationList();
+
+ for (SurfaceCombination combination : combinationList) {
+ boolean isSupported =
+ supportedSurfaceCombination.checkSupported(
+ combination.getSurfaceConfigurationList());
+ assertFalse(isSupported);
+ }
+ }
+
+ @Test
+ public void checkLevel3SurfaceCombinationSupportedInLevel3Device() {
+ SupportedSurfaceCombination supportedSurfaceCombination =
+ new SupportedSurfaceCombination(
+ context, LEVEL3_CAMERA_ID, mockCamcorderProfileHelper);
+
+ List<SurfaceCombination> combinationList =
+ supportedSurfaceCombination.getLevel3SupportedCombinationList();
+
+ for (SurfaceCombination combination : combinationList) {
+ boolean isSupported =
+ supportedSurfaceCombination.checkSupported(
+ combination.getSurfaceConfigurationList());
+ assertTrue(isSupported);
+ }
+ }
+
+ @Test
+ public void checkLevel3SurfaceCombinationSubListSupportedInLevel3Device() {
+ SupportedSurfaceCombination supportedSurfaceCombination =
+ new SupportedSurfaceCombination(
+ context, LEVEL3_CAMERA_ID, mockCamcorderProfileHelper);
+
+ List<SurfaceCombination> combinationList =
+ supportedSurfaceCombination.getLevel3SupportedCombinationList();
+
+ boolean isSupported =
+ isAllSubConfigurationListSupported(supportedSurfaceCombination, combinationList);
+ assertTrue(isSupported);
+ }
+
+ @Test
+ public void suggestedResolutionsForMixedUseCaseNotSupportedInLegacyDevice() {
+ SupportedSurfaceCombination supportedSurfaceCombination =
+ new SupportedSurfaceCombination(
+ context, LEGACY_CAMERA_ID, mockCamcorderProfileHelper);
+
+ Rational aspectRatio = new Rational(16, 9);
+ ViewFinderUseCaseConfiguration.Builder viewFinderConfigBuilder =
+ new ViewFinderUseCaseConfiguration.Builder();
+ VideoCaptureUseCaseConfiguration.Builder videoCaptureConfigBuilder =
+ new VideoCaptureUseCaseConfiguration.Builder();
+ ImageCaptureUseCaseConfiguration.Builder imageCaptureConfigBuilder =
+ new ImageCaptureUseCaseConfiguration.Builder();
+
+ viewFinderConfigBuilder.setTargetAspectRatio(aspectRatio);
+ videoCaptureConfigBuilder.setTargetAspectRatio(aspectRatio);
+ imageCaptureConfigBuilder.setTargetAspectRatio(aspectRatio);
+
+ imageCaptureConfigBuilder.setLensFacing(LensFacing.BACK);
+ ImageCaptureUseCase imageCaptureUseCase =
+ new ImageCaptureUseCase(imageCaptureConfigBuilder.build());
+ videoCaptureConfigBuilder.setLensFacing(LensFacing.BACK);
+ VideoCaptureUseCase videoCaptureUseCase =
+ new VideoCaptureUseCase(videoCaptureConfigBuilder.build());
+ viewFinderConfigBuilder.setLensFacing(LensFacing.BACK);
+ ViewFinderUseCase viewFinderUseCase =
+ new ViewFinderUseCase(viewFinderConfigBuilder.build());
+
+ List<BaseUseCase> useCases = new ArrayList<>();
+ useCases.add(imageCaptureUseCase);
+ useCases.add(videoCaptureUseCase);
+ useCases.add(viewFinderUseCase);
+ Map<BaseUseCase, Size> suggestedResolutionMap =
+ supportedSurfaceCombination.getSuggestedResolutions(null, useCases);
+
+ assertTrue(suggestedResolutionMap.size() != 3);
+ }
+
+ @Test
+ public void getSuggestedResolutionsForMixedUseCaseInLimitedDevice() {
+ SupportedSurfaceCombination supportedSurfaceCombination =
+ new SupportedSurfaceCombination(
+ context, LIMITED_CAMERA_ID, mockCamcorderProfileHelper);
+
+ Rational aspectRatio = new Rational(16, 9);
+ ViewFinderUseCaseConfiguration.Builder viewFinderConfigBuilder =
+ new ViewFinderUseCaseConfiguration.Builder();
+ VideoCaptureUseCaseConfiguration.Builder videoCaptureConfigBuilder =
+ new VideoCaptureUseCaseConfiguration.Builder();
+ ImageCaptureUseCaseConfiguration.Builder imageCaptureConfigBuilder =
+ new ImageCaptureUseCaseConfiguration.Builder();
+
+ viewFinderConfigBuilder.setTargetAspectRatio(aspectRatio);
+ videoCaptureConfigBuilder.setTargetAspectRatio(aspectRatio);
+ imageCaptureConfigBuilder.setTargetAspectRatio(aspectRatio);
+
+ imageCaptureConfigBuilder.setLensFacing(LensFacing.BACK);
+ ImageCaptureUseCase imageCaptureUseCase =
+ new ImageCaptureUseCase(imageCaptureConfigBuilder.build());
+ videoCaptureConfigBuilder.setLensFacing(LensFacing.BACK);
+ VideoCaptureUseCase videoCaptureUseCase =
+ new VideoCaptureUseCase(videoCaptureConfigBuilder.build());
+ viewFinderConfigBuilder.setLensFacing(LensFacing.BACK);
+ ViewFinderUseCase viewFinderUseCase =
+ new ViewFinderUseCase(viewFinderConfigBuilder.build());
+
+ List<BaseUseCase> useCases = new ArrayList<>();
+ useCases.add(imageCaptureUseCase);
+ useCases.add(videoCaptureUseCase);
+ useCases.add(viewFinderUseCase);
+ Map<BaseUseCase, Size> suggestedResolutionMap =
+ supportedSurfaceCombination.getSuggestedResolutions(null, useCases);
+
+ // (PRIV, PREVIEW) + (PRIV, RECORD) + (JPEG, RECORD)
+ assertThat(suggestedResolutionMap).containsEntry(imageCaptureUseCase, recordSize);
+ assertThat(suggestedResolutionMap).containsEntry(videoCaptureUseCase, maximumVideoSize);
+ assertThat(suggestedResolutionMap).containsEntry(viewFinderUseCase, previewSize);
+ }
+
+ @Test
+ public void transformSurfaceConfigurationWithYUVAnalysisSize() {
+ SupportedSurfaceCombination supportedSurfaceCombination =
+ new SupportedSurfaceCombination(
+ context, LEGACY_CAMERA_ID, mockCamcorderProfileHelper);
+ SurfaceConfiguration surfaceConfiguration =
+ supportedSurfaceCombination.transformSurfaceConfiguration(
+ ImageFormat.YUV_420_888, analysisSize);
+ SurfaceConfiguration expectedSurfaceConfiguration =
+ SurfaceConfiguration.create(ConfigurationType.YUV, ConfigurationSize.ANALYSIS);
+ assertEquals(expectedSurfaceConfiguration, surfaceConfiguration);
+ }
+
+ @Test
+ public void transformSurfaceConfigurationWithYUVPreviewSize() {
+ SupportedSurfaceCombination supportedSurfaceCombination =
+ new SupportedSurfaceCombination(
+ context, LEGACY_CAMERA_ID, mockCamcorderProfileHelper);
+ SurfaceConfiguration surfaceConfiguration =
+ supportedSurfaceCombination.transformSurfaceConfiguration(
+ ImageFormat.YUV_420_888, previewSize);
+ SurfaceConfiguration expectedSurfaceConfiguration =
+ SurfaceConfiguration.create(ConfigurationType.YUV, ConfigurationSize.PREVIEW);
+ assertEquals(expectedSurfaceConfiguration, surfaceConfiguration);
+ }
+
+ @Test
+ public void transformSurfaceConfigurationWithYUVRecordSize() {
+ SupportedSurfaceCombination supportedSurfaceCombination =
+ new SupportedSurfaceCombination(
+ context, LEGACY_CAMERA_ID, mockCamcorderProfileHelper);
+ SurfaceConfiguration surfaceConfiguration =
+ supportedSurfaceCombination.transformSurfaceConfiguration(
+ ImageFormat.YUV_420_888, recordSize);
+ SurfaceConfiguration expectedSurfaceConfiguration =
+ SurfaceConfiguration.create(ConfigurationType.YUV, ConfigurationSize.RECORD);
+ assertEquals(expectedSurfaceConfiguration, surfaceConfiguration);
+ }
+
+ @Test
+ public void transformSurfaceConfigurationWithYUVMaximumSize() {
+ SupportedSurfaceCombination supportedSurfaceCombination =
+ new SupportedSurfaceCombination(
+ context, LEGACY_CAMERA_ID, mockCamcorderProfileHelper);
+ SurfaceConfiguration surfaceConfiguration =
+ supportedSurfaceCombination.transformSurfaceConfiguration(
+ ImageFormat.YUV_420_888, maximumSize);
+ SurfaceConfiguration expectedSurfaceConfiguration =
+ SurfaceConfiguration.create(ConfigurationType.YUV, ConfigurationSize.MAXIMUM);
+ assertEquals(expectedSurfaceConfiguration, surfaceConfiguration);
+ }
+
+ @Test
+ public void transformSurfaceConfigurationWithYUVNotSupportSize() {
+ SupportedSurfaceCombination supportedSurfaceCombination =
+ new SupportedSurfaceCombination(
+ context, LEGACY_CAMERA_ID, mockCamcorderProfileHelper);
+ SurfaceConfiguration surfaceConfiguration =
+ supportedSurfaceCombination.transformSurfaceConfiguration(
+ ImageFormat.YUV_420_888,
+ new Size(maximumSize.getWidth() + 1, maximumSize.getHeight() + 1));
+ SurfaceConfiguration expectedSurfaceConfiguration =
+ SurfaceConfiguration.create(ConfigurationType.YUV, ConfigurationSize.NOT_SUPPORT);
+ assertEquals(expectedSurfaceConfiguration, surfaceConfiguration);
+ }
+
+ @Test
+ public void transformSurfaceConfigurationWithJPEGAnalysisSize() {
+ SupportedSurfaceCombination supportedSurfaceCombination =
+ new SupportedSurfaceCombination(
+ context, LEGACY_CAMERA_ID, mockCamcorderProfileHelper);
+ SurfaceConfiguration surfaceConfiguration =
+ supportedSurfaceCombination.transformSurfaceConfiguration(
+ ImageFormat.JPEG, analysisSize);
+ SurfaceConfiguration expectedSurfaceConfiguration =
+ SurfaceConfiguration.create(ConfigurationType.JPEG, ConfigurationSize.ANALYSIS);
+ assertEquals(expectedSurfaceConfiguration, surfaceConfiguration);
+ }
+
+ @Test
+ public void transformSurfaceConfigurationWithJPEGPreviewSize() {
+ SupportedSurfaceCombination supportedSurfaceCombination =
+ new SupportedSurfaceCombination(
+ context, LEGACY_CAMERA_ID, mockCamcorderProfileHelper);
+ SurfaceConfiguration surfaceConfiguration =
+ supportedSurfaceCombination.transformSurfaceConfiguration(
+ ImageFormat.JPEG, previewSize);
+ SurfaceConfiguration expectedSurfaceConfiguration =
+ SurfaceConfiguration.create(ConfigurationType.JPEG, ConfigurationSize.PREVIEW);
+ assertEquals(expectedSurfaceConfiguration, surfaceConfiguration);
+ }
+
+ @Test
+ public void transformSurfaceConfigurationWithJPEGRecordSize() {
+ SupportedSurfaceCombination supportedSurfaceCombination =
+ new SupportedSurfaceCombination(
+ context, LEGACY_CAMERA_ID, mockCamcorderProfileHelper);
+ SurfaceConfiguration surfaceConfiguration =
+ supportedSurfaceCombination.transformSurfaceConfiguration(
+ ImageFormat.JPEG, recordSize);
+ SurfaceConfiguration expectedSurfaceConfiguration =
+ SurfaceConfiguration.create(ConfigurationType.JPEG, ConfigurationSize.RECORD);
+ assertEquals(expectedSurfaceConfiguration, surfaceConfiguration);
+ }
+
+ @Test
+ public void transformSurfaceConfigurationWithJPEGMaximumSize() {
+ SupportedSurfaceCombination supportedSurfaceCombination =
+ new SupportedSurfaceCombination(
+ context, LEGACY_CAMERA_ID, mockCamcorderProfileHelper);
+ SurfaceConfiguration surfaceConfiguration =
+ supportedSurfaceCombination.transformSurfaceConfiguration(
+ ImageFormat.JPEG, maximumSize);
+ SurfaceConfiguration expectedSurfaceConfiguration =
+ SurfaceConfiguration.create(ConfigurationType.JPEG, ConfigurationSize.MAXIMUM);
+ assertEquals(expectedSurfaceConfiguration, surfaceConfiguration);
+ }
+
+ @Test
+ public void transformSurfaceConfigurationWithJPEGNotSupportSize() {
+ SupportedSurfaceCombination supportedSurfaceCombination =
+ new SupportedSurfaceCombination(
+ context, LEGACY_CAMERA_ID, mockCamcorderProfileHelper);
+ SurfaceConfiguration surfaceConfiguration =
+ supportedSurfaceCombination.transformSurfaceConfiguration(
+ ImageFormat.JPEG,
+ new Size(maximumSize.getWidth() + 1, maximumSize.getHeight() + 1));
+ SurfaceConfiguration expectedSurfaceConfiguration =
+ SurfaceConfiguration.create(ConfigurationType.JPEG, ConfigurationSize.NOT_SUPPORT);
+ assertEquals(expectedSurfaceConfiguration, surfaceConfiguration);
+ }
+
+ @Test
+ public void getMaximumSizeForImageFormat() {
+ SupportedSurfaceCombination supportedSurfaceCombination =
+ new SupportedSurfaceCombination(
+ context, LEGACY_CAMERA_ID, mockCamcorderProfileHelper);
+ Size maximumYUVSize =
+ supportedSurfaceCombination.getMaxOutputSizeByFormat(ImageFormat.YUV_420_888);
+ assertEquals(maximumSize, maximumYUVSize);
+ Size maximumJPEGSize =
+ supportedSurfaceCombination.getMaxOutputSizeByFormat(ImageFormat.JPEG);
+ assertEquals(maximumSize, maximumJPEGSize);
+ }
+
+ private void setupCamera() {
+ addBackFacingCamera(
+ LEGACY_CAMERA_ID, CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY, null);
+ addBackFacingCamera(
+ LIMITED_CAMERA_ID,
+ CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED,
+ null);
+ addBackFacingCamera(
+ FULL_CAMERA_ID, CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_FULL, null);
+ addBackFacingCamera(
+ LEVEL3_CAMERA_ID, CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_3, null);
+ initCameraX();
+ }
+
+ private void addBackFacingCamera(String cameraId, int hardwareLevel, int[] capabilities) {
+ CameraCharacteristics characteristics =
+ ShadowCameraCharacteristics.newCameraCharacteristics();
+
+ ShadowCameraCharacteristics shadowCharacteristics = Shadow.extract(characteristics);
+ shadowCharacteristics.set(
+ CameraCharacteristics.LENS_FACING, CameraCharacteristics.LENS_FACING_BACK);
+
+ shadowCharacteristics.set(
+ CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL, hardwareLevel);
+
+ if (capabilities != null) {
+ shadowCharacteristics.set(
+ CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES, capabilities);
+ }
+
+ ((ShadowCameraManager) Shadow.extract(application.getSystemService(Context.CAMERA_SERVICE)))
+ .addCamera(cameraId, characteristics);
+
+ shadowCharacteristics.set(
+ CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP,
+ StreamConfigurationMapUtil.generateFakeStreamConfigurationMap(
+ supportedFormats, supportedSizes));
+ }
+
+ private void initCameraX() {
+ AppConfiguration appConfig = Camera2AppConfiguration.create(context);
+ CameraX.init(context, appConfig);
+ }
+
+ private boolean isAllSubConfigurationListSupported(
+ SupportedSurfaceCombination supportedSurfaceCombination,
+ List<SurfaceCombination> combinationList) {
+ boolean isSupported = true;
+
+ for (SurfaceCombination combination : combinationList) {
+ List<SurfaceConfiguration> configurationList =
+ combination.getSurfaceConfigurationList();
+ int length = configurationList.size();
+
+ if (length <= 1) {
+ continue;
+ }
+
+ for (int index = 0; index < length; index++) {
+ List<SurfaceConfiguration> subConfigurationList = new ArrayList<>();
+ subConfigurationList.addAll(configurationList);
+ subConfigurationList.remove(index);
+
+ isSupported &= supportedSurfaceCombination.checkSupported(subConfigurationList);
+
+ if (!isSupported) {
+ return false;
+ }
+ }
+ }
+
+ return isSupported;
+ }
+}
diff --git a/camera/core/proguard.flags b/camera/core/proguard.flags
new file mode 100644
index 0000000..06c47ac
--- /dev/null
+++ b/camera/core/proguard.flags
@@ -0,0 +1,85 @@
+# Copyright (C) 2019 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.
+
+# Save the obfuscation mapping to a file, so we can de-obfuscate any stack
+# traces later on. Keep a fixed source file attribute and all line number
+# tables to get line numbers in the stack traces.
+# You can comment this out if you're not interested in stack traces.
+
+-printmapping out.map
+-keepparameternames
+-renamesourcefileattribute SourceFile
+-keepattributes Exceptions,InnerClasses,Deprecated,
+ SourceFile,LineNumberTable,EnclosingMethod
+
+# Preserve all annotations.
+
+-keepattributes *Annotation*
+
+# Preserve all public classes, and their public and protected fields and
+# methods.
+
+-keep public class * {
+ public protected *;
+}
+
+# Preserve all .class method names.
+
+-keepclassmembernames class * {
+ java.lang.Class class$(java.lang.String);
+ java.lang.Class class$(java.lang.String, boolean);
+}
+
+# Preserve all native method names and the names of their classes.
+
+-keepclasseswithmembernames class * {
+ native <methods>;
+}
+
+# Preserve the special static methods that are required in all enumeration
+# classes.
+
+-keepclassmembers class * extends java.lang.Enum {
+ public static **[] values();
+ public static ** valueOf(java.lang.String);
+}
+
+# Explicitly preserve all serialization members. The Serializable interface
+# is only a marker interface, so it wouldn't save them.
+# You can comment this out if your library doesn't use serialization.
+# If your code contains serializable classes that have to be backward
+# compatible, please refer to the manual.
+
+-keepclassmembers class * implements java.io.Serializable {
+ static final long serialVersionUID;
+ static final java.io.ObjectStreamField[] serialPersistentFields;
+ private void writeObject(java.io.ObjectOutputStream);
+ private void readObject(java.io.ObjectInputStream);
+ java.lang.Object writeReplace();
+ java.lang.Object readResolve();
+}
+
+# Keep constructors for UseCase classes. They are used in reflection.
+-keepclassmembers class androidx.camera.core.BaseUseCase {
+ protected <init>(...);
+}
+-keepclassmembers class * extends androidx.camera.core.BaseUseCase {
+ public <init>(...);
+}
+
+# Keep generic types for the TypeReference class
+-keepattributes Signature
+
+# Keep the TypeReference class as it uses self-inspection
+-keep class * extends androidx.camera.core.TypeReference
diff --git a/camera/core/src/androidTest/AndroidManifest.xml b/camera/core/src/androidTest/AndroidManifest.xml
new file mode 100644
index 0000000..cc2b1cb
--- /dev/null
+++ b/camera/core/src/androidTest/AndroidManifest.xml
@@ -0,0 +1,49 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2019 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.
+-->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="androidx.camera.core">
+
+ <uses-sdk
+ android:minSdkVersion="21"
+ android:targetSdkVersion="27" />
+
+ <uses-permission android:name="android.permission.CAMERA" />
+ <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
+ <uses-permission android:name="android.permission.RECORD_AUDIO" />
+
+ <uses-feature android:name="android.hardware.camera" />
+
+ <application android:debuggable="true">
+ <uses-library
+ android:name="android.test.runner"
+ android:required="false" />
+ <uses-library
+ android:name="android.test.base"
+ android:required="false" />
+ <uses-library
+ android:name="android.test.mock"
+ android:required="false" />
+
+ <activity
+ android:name="androidx.camera.core.FakeActivity"
+ android:label="Fake Activity">
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN" />
+ <category android:name="android.intent.category.LAUNCHER" />
+ </intent-filter>
+ </activity>
+ </application>
+</manifest>
diff --git a/camera/core/src/androidTest/java/androidx/camera/core/AndroidImageProxyAndroidTest.java b/camera/core/src/androidTest/java/androidx/camera/core/AndroidImageProxyAndroidTest.java
new file mode 100644
index 0000000..62da537
--- /dev/null
+++ b/camera/core/src/androidTest/java/androidx/camera/core/AndroidImageProxyAndroidTest.java
@@ -0,0 +1,130 @@
+/*
+ * Copyright (C) 2019 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.core;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.graphics.ImageFormat;
+import android.graphics.Rect;
+import android.media.Image;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.nio.ByteBuffer;
+
+@RunWith(AndroidJUnit4.class)
+public final class AndroidImageProxyAndroidTest {
+ private static final long INITIAL_TIMESTAMP = 138990020L;
+
+ private final Image image = mock(Image.class);
+ private final Image.Plane yPlane = mock(Image.Plane.class);
+ private final Image.Plane uPlane = mock(Image.Plane.class);
+ private final Image.Plane vPlane = mock(Image.Plane.class);
+ private ImageProxy imageProxy;
+
+ @Before
+ public void setUp() {
+ when(image.getPlanes()).thenReturn(new Image.Plane[]{yPlane, uPlane, vPlane});
+ when(yPlane.getRowStride()).thenReturn(640);
+ when(yPlane.getPixelStride()).thenReturn(1);
+ when(yPlane.getBuffer()).thenReturn(ByteBuffer.allocateDirect(640 * 480));
+ when(uPlane.getRowStride()).thenReturn(320);
+ when(uPlane.getPixelStride()).thenReturn(1);
+ when(uPlane.getBuffer()).thenReturn(ByteBuffer.allocateDirect(320 * 240));
+ when(vPlane.getRowStride()).thenReturn(320);
+ when(vPlane.getPixelStride()).thenReturn(1);
+ when(vPlane.getBuffer()).thenReturn(ByteBuffer.allocateDirect(320 * 240));
+
+ when(image.getTimestamp()).thenReturn(INITIAL_TIMESTAMP);
+ imageProxy = new AndroidImageProxy(image);
+ }
+
+ @Test
+ public void close_closesWrappedImage() {
+ imageProxy.close();
+
+ verify(image).close();
+ }
+
+ @Test
+ public void getCropRect_returnsCropRectForWrappedImage() {
+ when(image.getCropRect()).thenReturn(new Rect(0, 0, 20, 20));
+
+ assertThat(imageProxy.getCropRect()).isEqualTo(new Rect(0, 0, 20, 20));
+ }
+
+ @Test
+ public void setCropRect_setsCropRectForWrappedImage() {
+ imageProxy.setCropRect(new Rect(0, 0, 40, 40));
+
+ verify(image).setCropRect(new Rect(0, 0, 40, 40));
+ }
+
+ @Test
+ public void getFormat_returnsFormatForWrappedImage() {
+ when(image.getFormat()).thenReturn(ImageFormat.YUV_420_888);
+
+ assertThat(imageProxy.getFormat()).isEqualTo(ImageFormat.YUV_420_888);
+ }
+
+ @Test
+ public void getHeight_returnsHeightForWrappedImage() {
+ when(image.getHeight()).thenReturn(480);
+
+ assertThat(imageProxy.getHeight()).isEqualTo(480);
+ }
+
+ @Test
+ public void getWidth_returnsWidthForWrappedImage() {
+ when(image.getWidth()).thenReturn(640);
+
+ assertThat(imageProxy.getWidth()).isEqualTo(640);
+ }
+
+ @Test
+ public void getTimestamp_returnsTimestampForWrappedImage() {
+ assertThat(imageProxy.getTimestamp()).isEqualTo(INITIAL_TIMESTAMP);
+ }
+
+ public void setTimestamp_setsTimestampForWrappedImage() {
+ imageProxy.setTimestamp(INITIAL_TIMESTAMP + 10);
+
+ assertThat(imageProxy.getTimestamp()).isEqualTo(INITIAL_TIMESTAMP + 10);
+ }
+
+ @Test
+ public void getPlanes_returnsPlanesForWrappedImage() {
+ ImageProxy.PlaneProxy[] wrappedPlanes = imageProxy.getPlanes();
+
+ Image.Plane[] originalPlanes = new Image.Plane[]{yPlane, uPlane, vPlane};
+ assertThat(wrappedPlanes.length).isEqualTo(3);
+ for (int i = 0; i < 3; ++i) {
+ assertThat(wrappedPlanes[i].getRowStride()).isEqualTo(originalPlanes[i].getRowStride());
+ assertThat(wrappedPlanes[i].getPixelStride())
+ .isEqualTo(originalPlanes[i].getPixelStride());
+ assertThat(wrappedPlanes[i].getBuffer()).isEqualTo(originalPlanes[i].getBuffer());
+ }
+ }
+}
diff --git a/camera/core/src/androidTest/java/androidx/camera/core/AndroidImageReaderProxyAndroidTest.java b/camera/core/src/androidTest/java/androidx/camera/core/AndroidImageReaderProxyAndroidTest.java
new file mode 100644
index 0000000..053681c
--- /dev/null
+++ b/camera/core/src/androidTest/java/androidx/camera/core/AndroidImageReaderProxyAndroidTest.java
@@ -0,0 +1,125 @@
+/*
+ * Copyright (C) 2019 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.core;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.graphics.ImageFormat;
+import android.media.Image;
+import android.media.ImageReader;
+import android.os.Handler;
+import android.view.Surface;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+
+@RunWith(AndroidJUnit4.class)
+public final class AndroidImageReaderProxyAndroidTest {
+ private final ImageReader imageReader = mock(ImageReader.class);
+ private ImageReaderProxy imageReaderProxy;
+
+ @Before
+ public void setUp() {
+ imageReaderProxy = new AndroidImageReaderProxy(imageReader);
+ when(imageReader.acquireLatestImage()).thenReturn(mock(Image.class));
+ when(imageReader.acquireNextImage()).thenReturn(mock(Image.class));
+ }
+
+ @Test
+ public void acquireLatestImage_invokesMethodOnWrappedReader() {
+ imageReaderProxy.acquireLatestImage();
+
+ verify(imageReader, times(1)).acquireLatestImage();
+ }
+
+ @Test
+ public void acquireNextImage_invokesMethodOnWrappedReader() {
+ imageReaderProxy.acquireNextImage();
+
+ verify(imageReader, times(1)).acquireNextImage();
+ }
+
+ @Test
+ public void close_invokesMethodOnWrappedReader() {
+ imageReaderProxy.close();
+
+ verify(imageReader, times(1)).close();
+ }
+
+ @Test
+ public void getWidth_returnsWidthOfWrappedReader() {
+ when(imageReader.getWidth()).thenReturn(640);
+
+ assertThat(imageReaderProxy.getWidth()).isEqualTo(640);
+ }
+
+ @Test
+ public void getHeight_returnsHeightOfWrappedReader() {
+ when(imageReader.getHeight()).thenReturn(480);
+
+ assertThat(imageReaderProxy.getHeight()).isEqualTo(480);
+ }
+
+ @Test
+ public void getImageFormat_returnsImageFormatOfWrappedReader() {
+ when(imageReader.getImageFormat()).thenReturn(ImageFormat.YUV_420_888);
+
+ assertThat(imageReaderProxy.getImageFormat()).isEqualTo(ImageFormat.YUV_420_888);
+ }
+
+ @Test
+ public void getMaxImages_returnsMaxImagesOfWrappedReader() {
+ when(imageReader.getMaxImages()).thenReturn(8);
+
+ assertThat(imageReaderProxy.getMaxImages()).isEqualTo(8);
+ }
+
+ @Test
+ public void getSurface_returnsSurfaceOfWrappedReader() {
+ Surface surface = mock(Surface.class);
+ when(imageReader.getSurface()).thenReturn(surface);
+
+ assertThat(imageReaderProxy.getSurface()).isSameAs(surface);
+ }
+
+ @Test
+ public void setOnImageAvailableListener_setsListenerOfWrappedReader() {
+ ImageReaderProxy.OnImageAvailableListener listener =
+ mock(ImageReaderProxy.OnImageAvailableListener.class);
+
+ imageReaderProxy.setOnImageAvailableListener(listener, /*handler=*/ null);
+
+ ArgumentCaptor<ImageReader.OnImageAvailableListener> transformedListenerCaptor =
+ ArgumentCaptor.forClass(ImageReader.OnImageAvailableListener.class);
+ ArgumentCaptor<Handler> handlerCaptor = ArgumentCaptor.forClass(Handler.class);
+ verify(imageReader, times(1))
+ .setOnImageAvailableListener(
+ transformedListenerCaptor.capture(), handlerCaptor.capture());
+
+ transformedListenerCaptor.getValue().onImageAvailable(imageReader);
+ verify(listener, times(1)).onImageAvailable(imageReaderProxy);
+ }
+}
diff --git a/camera/core/src/androidTest/java/androidx/camera/core/BaseUseCaseAndroidTest.java b/camera/core/src/androidTest/java/androidx/camera/core/BaseUseCaseAndroidTest.java
new file mode 100644
index 0000000..bd7fdf0
--- /dev/null
+++ b/camera/core/src/androidTest/java/androidx/camera/core/BaseUseCaseAndroidTest.java
@@ -0,0 +1,205 @@
+/*
+ * Copyright (C) 2019 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.core;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Matchers.any;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import android.util.Size;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mockito;
+
+import java.util.Map;
+import java.util.Set;
+
+@RunWith(AndroidJUnit4.class)
+public class BaseUseCaseAndroidTest {
+ private BaseUseCase.StateChangeListener mockUseCaseListener;
+
+ @Before
+ public void setup() {
+ mockUseCaseListener = Mockito.mock(BaseUseCase.StateChangeListener.class);
+ }
+
+ @Test
+ public void getAttachedCamera() {
+ FakeUseCaseConfiguration configuration =
+ new FakeUseCaseConfiguration.Builder().setTargetName("UseCase").build();
+ TestUseCase testUseCase = new TestUseCase(configuration);
+ SessionConfiguration sessionToAttach = new SessionConfiguration.Builder().build();
+ testUseCase.attachToCamera("Camera", sessionToAttach);
+
+ Set<String> attachedCameras = testUseCase.getAttachedCameraIds();
+
+ assertThat(attachedCameras).contains("Camera");
+ }
+
+ @Test
+ public void getAttachedSessionConfiguration() {
+ FakeUseCaseConfiguration configuration =
+ new FakeUseCaseConfiguration.Builder().setTargetName("UseCase").build();
+ TestUseCase testUseCase = new TestUseCase(configuration);
+ SessionConfiguration sessionToAttach = new SessionConfiguration.Builder().build();
+ testUseCase.attachToCamera("Camera", sessionToAttach);
+
+ SessionConfiguration attachedSession = testUseCase.getSessionConfiguration("Camera");
+
+ assertThat(attachedSession).isEqualTo(sessionToAttach);
+ }
+
+ @Test
+ public void removeListener() {
+ FakeUseCaseConfiguration configuration =
+ new FakeUseCaseConfiguration.Builder().setTargetName("UseCase").build();
+ TestUseCase testUseCase = new TestUseCase(configuration);
+ testUseCase.addStateChangeListener(mockUseCaseListener);
+ testUseCase.removeStateChangeListener(mockUseCaseListener);
+
+ testUseCase.activate();
+
+ verify(mockUseCaseListener, never()).onUseCaseActive(any());
+ }
+
+ @Test
+ public void clearListeners() {
+ FakeUseCaseConfiguration configuration =
+ new FakeUseCaseConfiguration.Builder().setTargetName("UseCase").build();
+ TestUseCase testUseCase = new TestUseCase(configuration);
+ testUseCase.addStateChangeListener(mockUseCaseListener);
+ testUseCase.clear();
+
+ testUseCase.activate();
+ verify(mockUseCaseListener, never()).onUseCaseActive(any());
+ }
+
+ @Test
+ public void notifyActiveState() {
+ FakeUseCaseConfiguration configuration =
+ new FakeUseCaseConfiguration.Builder().setTargetName("UseCase").build();
+ TestUseCase testUseCase = new TestUseCase(configuration);
+ testUseCase.addStateChangeListener(mockUseCaseListener);
+
+ testUseCase.activate();
+ verify(mockUseCaseListener, times(1)).onUseCaseActive(testUseCase);
+ }
+
+ @Test
+ public void notifyInactiveState() {
+ FakeUseCaseConfiguration configuration =
+ new FakeUseCaseConfiguration.Builder().setTargetName("UseCase").build();
+ TestUseCase testUseCase = new TestUseCase(configuration);
+ testUseCase.addStateChangeListener(mockUseCaseListener);
+
+ testUseCase.deactivate();
+ verify(mockUseCaseListener, times(1)).onUseCaseInactive(testUseCase);
+ }
+
+ @Test
+ public void notifyUpdatedSettings() {
+ FakeUseCaseConfiguration configuration =
+ new FakeUseCaseConfiguration.Builder().setTargetName("UseCase").build();
+ TestUseCase testUseCase = new TestUseCase(configuration);
+ testUseCase.addStateChangeListener(mockUseCaseListener);
+
+ testUseCase.update();
+ verify(mockUseCaseListener, times(1)).onUseCaseUpdated(testUseCase);
+ }
+
+ @Test
+ public void notifyResetUseCase() {
+ FakeUseCaseConfiguration configuration =
+ new FakeUseCaseConfiguration.Builder().setTargetName("UseCase").build();
+ TestUseCase testUseCase = new TestUseCase(configuration);
+ testUseCase.addStateChangeListener(mockUseCaseListener);
+
+ testUseCase.notifyReset();
+ verify(mockUseCaseListener, times(1)).onUseCaseReset(testUseCase);
+ }
+
+ @Test
+ public void notifySingleCapture() {
+ FakeUseCaseConfiguration configuration =
+ new FakeUseCaseConfiguration.Builder().setTargetName("UseCase").build();
+ TestUseCase testUseCase = new TestUseCase(configuration);
+ testUseCase.addStateChangeListener(mockUseCaseListener);
+ CaptureRequestConfiguration captureRequestConfiguration =
+ new CaptureRequestConfiguration.Builder().build();
+
+ testUseCase.notifySingleCapture(captureRequestConfiguration);
+ verify(mockUseCaseListener, times(1))
+ .onUseCaseSingleRequest(testUseCase, captureRequestConfiguration);
+ }
+
+ @Test
+ public void useCaseConfiguration_canBeUpdated() {
+ String originalName = "UseCase";
+ FakeUseCaseConfiguration.Builder configurationBuilder =
+ new FakeUseCaseConfiguration.Builder().setTargetName(originalName);
+
+ TestUseCase testUseCase = new TestUseCase(configurationBuilder.build());
+ String originalRetrievedName = testUseCase.getUseCaseConfiguration().getTargetName();
+
+ // NOTE: Updating the use case name is probably a very bad idea in most cases. However,
+ // we'll do
+ // it here for the sake of this test.
+ String newName = "UseCase-New";
+ configurationBuilder.setTargetName(newName);
+ testUseCase.updateUseCaseConfiguration(configurationBuilder.build());
+ String newRetrievedName = testUseCase.getUseCaseConfiguration().getTargetName();
+
+ assertThat(originalRetrievedName).isEqualTo(originalName);
+ assertThat(newRetrievedName).isEqualTo(newName);
+ }
+
+ static class TestUseCase extends FakeUseCase {
+ TestUseCase(FakeUseCaseConfiguration configuration) {
+ super(configuration);
+ }
+
+ void activate() {
+ notifyActive();
+ }
+
+ void deactivate() {
+ notifyInactive();
+ }
+
+ void update() {
+ notifyUpdated();
+ }
+
+ @Override
+ protected void updateUseCaseConfiguration(UseCaseConfiguration<?> useCaseConfiguration) {
+ super.updateUseCaseConfiguration(useCaseConfiguration);
+ }
+
+ @Override
+ protected Map<String, Size> onSuggestedResolutionUpdated(
+ Map<String, Size> suggestedResolutionMap) {
+ return suggestedResolutionMap;
+ }
+ }
+}
diff --git a/camera/core/src/androidTest/java/androidx/camera/core/CameraCaptureCallbacksAndroidTest.java b/camera/core/src/androidTest/java/androidx/camera/core/CameraCaptureCallbacksAndroidTest.java
new file mode 100644
index 0000000..7877984
--- /dev/null
+++ b/camera/core/src/androidTest/java/androidx/camera/core/CameraCaptureCallbacksAndroidTest.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2019 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.core;
+
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.Mockito;
+
+@RunWith(JUnit4.class)
+public final class CameraCaptureCallbacksAndroidTest {
+
+ @Test
+ public void comboCallbackInvokesConstituentCallbacks() {
+ CameraCaptureCallback callback0 = Mockito.mock(CameraCaptureCallback.class);
+ CameraCaptureCallback callback1 = Mockito.mock(CameraCaptureCallback.class);
+ CameraCaptureCallback comboCallback =
+ CameraCaptureCallbacks.createComboCallback(callback0, callback1);
+ CameraCaptureResult result = Mockito.mock(CameraCaptureResult.class);
+ CameraCaptureFailure failure = new CameraCaptureFailure(CameraCaptureFailure.Reason.ERROR);
+
+ comboCallback.onCaptureCompleted(result);
+ verify(callback0, times(1)).onCaptureCompleted(result);
+ verify(callback1, times(1)).onCaptureCompleted(result);
+
+ comboCallback.onCaptureFailed(failure);
+ verify(callback0, times(1)).onCaptureFailed(failure);
+ verify(callback1, times(1)).onCaptureFailed(failure);
+ }
+}
diff --git a/camera/core/src/androidTest/java/androidx/camera/core/CameraCaptureFailureAndroidTest.java b/camera/core/src/androidTest/java/androidx/camera/core/CameraCaptureFailureAndroidTest.java
new file mode 100644
index 0000000..105c2d1
--- /dev/null
+++ b/camera/core/src/androidTest/java/androidx/camera/core/CameraCaptureFailureAndroidTest.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2019 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.core;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import androidx.camera.core.CameraCaptureFailure.Reason;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public class CameraCaptureFailureAndroidTest {
+
+ @Test
+ public void getReason() {
+ CameraCaptureFailure failure = new CameraCaptureFailure(Reason.ERROR);
+ assertThat(failure.getReason()).isEqualTo(Reason.ERROR);
+ }
+}
diff --git a/camera/core/src/androidTest/java/androidx/camera/core/CameraCaptureSessionStateCallbacksAndroidTest.java b/camera/core/src/androidTest/java/androidx/camera/core/CameraCaptureSessionStateCallbacksAndroidTest.java
new file mode 100644
index 0000000..e67da4a
--- /dev/null
+++ b/camera/core/src/androidTest/java/androidx/camera/core/CameraCaptureSessionStateCallbacksAndroidTest.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright (C) 2019 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.core;
+
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import android.hardware.camera2.CameraCaptureSession;
+import android.os.Build;
+import android.view.Surface;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.Mockito;
+
+@RunWith(JUnit4.class)
+public final class CameraCaptureSessionStateCallbacksAndroidTest {
+
+ @Test
+ public void comboCallbackInvokesConstituentCallbacks() {
+ CameraCaptureSession.StateCallback callback0 =
+ Mockito.mock(CameraCaptureSession.StateCallback.class);
+ CameraCaptureSession.StateCallback callback1 =
+ Mockito.mock(CameraCaptureSession.StateCallback.class);
+ CameraCaptureSession.StateCallback comboCallback =
+ CameraCaptureSessionStateCallbacks.createComboCallback(callback0, callback1);
+ CameraCaptureSession session = Mockito.mock(CameraCaptureSession.class);
+ Surface surface = Mockito.mock(Surface.class);
+
+ comboCallback.onConfigured(session);
+ verify(callback0, times(1)).onConfigured(session);
+ verify(callback1, times(1)).onConfigured(session);
+
+ comboCallback.onActive(session);
+ verify(callback0, times(1)).onActive(session);
+ verify(callback1, times(1)).onActive(session);
+
+ comboCallback.onClosed(session);
+ verify(callback0, times(1)).onClosed(session);
+ verify(callback1, times(1)).onClosed(session);
+
+ comboCallback.onReady(session);
+ verify(callback0, times(1)).onReady(session);
+ verify(callback1, times(1)).onReady(session);
+
+ if (Build.VERSION.SDK_INT >= 26) {
+ comboCallback.onCaptureQueueEmpty(session);
+ verify(callback0, times(1)).onCaptureQueueEmpty(session);
+ verify(callback1, times(1)).onCaptureQueueEmpty(session);
+ }
+
+ if (Build.VERSION.SDK_INT >= 23) {
+ comboCallback.onSurfacePrepared(session, surface);
+ verify(callback0, times(1)).onSurfacePrepared(session, surface);
+ verify(callback1, times(1)).onSurfacePrepared(session, surface);
+ }
+
+ comboCallback.onConfigureFailed(session);
+ verify(callback0, times(1)).onConfigureFailed(session);
+ verify(callback1, times(1)).onConfigureFailed(session);
+ }
+}
diff --git a/camera/core/src/androidTest/java/androidx/camera/core/CameraDeviceStateCallbacksAndroidTest.java b/camera/core/src/androidTest/java/androidx/camera/core/CameraDeviceStateCallbacksAndroidTest.java
new file mode 100644
index 0000000..b1d22cc
--- /dev/null
+++ b/camera/core/src/androidTest/java/androidx/camera/core/CameraDeviceStateCallbacksAndroidTest.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2019 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.core;
+
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import android.hardware.camera2.CameraDevice;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.Mockito;
+
+@RunWith(JUnit4.class)
+public final class CameraDeviceStateCallbacksAndroidTest {
+
+ @Test
+ public void comboCallbackInvokesConstituentCallbacks() {
+ CameraDevice.StateCallback callback0 = Mockito.mock(CameraDevice.StateCallback.class);
+ CameraDevice.StateCallback callback1 = Mockito.mock(CameraDevice.StateCallback.class);
+ CameraDevice.StateCallback comboCallback =
+ CameraDeviceStateCallbacks.createComboCallback(callback0, callback1);
+ CameraDevice device = Mockito.mock(CameraDevice.class);
+
+ comboCallback.onOpened(device);
+ verify(callback0, times(1)).onOpened(device);
+ verify(callback1, times(1)).onOpened(device);
+
+ comboCallback.onClosed(device);
+ verify(callback0, times(1)).onClosed(device);
+ verify(callback1, times(1)).onClosed(device);
+
+ comboCallback.onDisconnected(device);
+ verify(callback0, times(1)).onDisconnected(device);
+ verify(callback1, times(1)).onDisconnected(device);
+
+ final int error = 1;
+ comboCallback.onError(device, error);
+ verify(callback0, times(1)).onError(device, error);
+ verify(callback1, times(1)).onError(device, error);
+ }
+}
diff --git a/camera/core/src/androidTest/java/androidx/camera/core/CameraRepositoryAndroidTest.java b/camera/core/src/androidTest/java/androidx/camera/core/CameraRepositoryAndroidTest.java
new file mode 100644
index 0000000..29793d6
--- /dev/null
+++ b/camera/core/src/androidTest/java/androidx/camera/core/CameraRepositoryAndroidTest.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright (C) 2019 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.core;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+
+import androidx.camera.testing.fakes.FakeCameraFactory;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.Set;
+
+@RunWith(AndroidJUnit4.class)
+public final class CameraRepositoryAndroidTest {
+
+ private CameraRepository cameraRepository;
+
+ @Before
+ public void setUp() {
+ cameraRepository = new CameraRepository();
+ cameraRepository.init(new FakeCameraFactory());
+ }
+
+ @Test
+ public void cameraIdsCanBeAcquired() {
+ Set<String> cameraIds = cameraRepository.getCameraIds();
+
+ assertThat(cameraIds).isNotEmpty();
+ }
+
+ @Test
+ public void cameraCanBeObtainedWithValidId() {
+ for (String cameraId : cameraRepository.getCameraIds()) {
+ BaseCamera camera = cameraRepository.getCamera(cameraId);
+
+ assertThat(camera).isNotNull();
+ }
+ }
+
+ @Test
+ public void cameraCannotBeObtainedWithInvalidId() {
+ assertThrows(
+ IllegalArgumentException.class, () -> cameraRepository.getCamera("no_such_id"));
+ }
+}
diff --git a/camera/core/src/androidTest/java/androidx/camera/core/CameraUtil.java b/camera/core/src/androidTest/java/androidx/camera/core/CameraUtil.java
new file mode 100644
index 0000000..645812a
--- /dev/null
+++ b/camera/core/src/androidTest/java/androidx/camera/core/CameraUtil.java
@@ -0,0 +1,154 @@
+/*
+ * Copyright (C) 2019 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.core;
+
+import android.content.Context;
+import android.hardware.camera2.CameraAccessException;
+import android.hardware.camera2.CameraDevice;
+import android.hardware.camera2.CameraDevice.StateCallback;
+import android.hardware.camera2.CameraManager;
+import android.os.Handler;
+import android.os.HandlerThread;
+
+import androidx.test.core.app.ApplicationProvider;
+
+import java.util.Arrays;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicReference;
+
+/** Utility functions for obtaining instances of camera2 classes. */
+public final class CameraUtil {
+ /** Amount of time to wait before timing out when trying to open a {@link CameraDevice}. */
+ private static final int CAMERA_OPEN_TIMEOUT_SECONDS = 2;
+
+ /**
+ * Gets a new instance of a {@link CameraDevice}.
+ *
+ * <p>This method attempts to open up a new camera. Since the camera api is asynchronous it
+ * needs to wait for camera open
+ *
+ * <p>After the camera is no longer needed {@link #releaseCameraDevice(CameraDevice)} should be
+ * called to clean up resources.
+ *
+ * @throws CameraAccessException if the device is unable to access the camera
+ * @throws InterruptedException if a {@link CameraDevice} can not be retrieved within a set
+ * time
+ */
+ public static CameraDevice getCameraDevice()
+ throws CameraAccessException, InterruptedException {
+ // Setup threading required for callback on openCamera()
+ HandlerThread handlerThread = new HandlerThread("handler thread");
+ handlerThread.start();
+ Handler handler = new Handler(handlerThread.getLooper());
+
+ CameraManager cameraManager = getCameraManager();
+
+ // Use the first camera available.
+ String[] cameraIds = cameraManager.getCameraIdList();
+ if (cameraIds.length <= 0) {
+ throw new CameraAccessException(
+ CameraAccessException.CAMERA_ERROR, "Device contains no cameras.");
+ }
+ String cameraName = cameraIds[0];
+
+ // Use an AtomicReference to store the CameraDevice because it is initialized in a lambda.
+ // This
+ // way the AtomicReference itself is effectively final.
+ AtomicReference<CameraDevice> cameraDeviceHolder = new AtomicReference<>();
+
+ // Open the camera using the CameraManager which returns a valid and open CameraDevice only
+ // when
+ // onOpened() is called.
+ CountDownLatch latch = new CountDownLatch(1);
+ cameraManager.openCamera(
+ cameraName,
+ new StateCallback() {
+ @Override
+ public void onOpened(CameraDevice camera) {
+ cameraDeviceHolder.set(camera);
+ latch.countDown();
+ }
+
+ @Override
+ public void onClosed(CameraDevice cameraDevice) {
+ handlerThread.quit();
+ }
+
+ @Override
+ public void onDisconnected(CameraDevice camera) {
+ }
+
+ @Override
+ public void onError(CameraDevice camera, int error) {
+ }
+ },
+ handler);
+
+ // Wait for the callback to initialize the CameraDevice
+ latch.await(CAMERA_OPEN_TIMEOUT_SECONDS, TimeUnit.SECONDS);
+
+ return cameraDeviceHolder.get();
+ }
+
+ /**
+ * Cleans up resources that need to be kept around while the camera device is active.
+ *
+ * @param cameraDevice camera that was obtained via {@link #getCameraDevice()}
+ */
+ public static void releaseCameraDevice(CameraDevice cameraDevice) {
+ cameraDevice.close();
+ }
+
+ public static CameraManager getCameraManager() {
+ return (CameraManager)
+ ApplicationProvider.getApplicationContext()
+ .getSystemService(Context.CAMERA_SERVICE);
+ }
+
+ /**
+ * Opens a camera and associates the camera with multiple use cases.
+ *
+ * <p>Sets the use case to be online and active, so that the use case is in a state to issue
+ * capture requests to the camera. The caller is responsible for making the use case inactive
+ * and offline and for closing the camera afterwards.
+ *
+ * @param camera to open
+ * @param useCases to associate with
+ */
+ public static void openCameraWithUseCase(BaseCamera camera, BaseUseCase... useCases) {
+ camera.addOnlineUseCase(Arrays.asList(useCases));
+ for (BaseUseCase useCase : useCases) {
+ camera.onUseCaseActive(useCase);
+ }
+ }
+
+ /**
+ * Detach multiple use cases from a camera.
+ *
+ * <p>Sets the use cases to be inactive and remove from the online list.
+ *
+ * @param camera to detach from
+ * @param useCases to be detached
+ */
+ public static void detachUseCaseFromCamera(BaseCamera camera, BaseUseCase... useCases) {
+ for (BaseUseCase useCase : useCases) {
+ camera.onUseCaseInactive(useCase);
+ }
+ camera.removeOnlineUseCase(Arrays.asList(useCases));
+ }
+}
diff --git a/camera/core/src/androidTest/java/androidx/camera/core/CameraXAndroidTest.java b/camera/core/src/androidTest/java/androidx/camera/core/CameraXAndroidTest.java
new file mode 100644
index 0000000..6189396
--- /dev/null
+++ b/camera/core/src/androidTest/java/androidx/camera/core/CameraXAndroidTest.java
@@ -0,0 +1,270 @@
+/*
+ * Copyright (C) 2019 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.core;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+import static org.mockito.Mockito.spy;
+
+import android.content.Context;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.util.Size;
+
+import androidx.camera.core.CameraX.ErrorCode;
+import androidx.camera.core.CameraX.ErrorListener;
+import androidx.camera.core.CameraX.LensFacing;
+import androidx.camera.testing.fakes.FakeCameraDeviceSurfaceManager;
+import androidx.camera.testing.fakes.FakeCameraFactory;
+import androidx.camera.testing.fakes.FakeLifecycleOwner;
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mockito;
+
+import java.util.Map;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
+
+@RunWith(AndroidJUnit4.class)
+public final class CameraXAndroidTest {
+ static CameraFactory cameraFactory = new FakeCameraFactory();
+ String cameraId;
+ BaseCamera camera;
+ private FakeLifecycleOwner lifecycle;
+ private CountingErrorListener errorListener;
+ private CountDownLatch latch;
+ private HandlerThread handlerThread;
+ private Handler handler;
+
+ private static final String getCameraIdUnchecked(LensFacing lensFacing) {
+ try {
+ return CameraX.getCameraWithLensFacing(lensFacing);
+ } catch (Exception e) {
+ throw new IllegalArgumentException(
+ "Unable to get camera id for camera lens facing " + lensFacing, e);
+ }
+ }
+
+ @Before
+ public void setUp() {
+ Context context = ApplicationProvider.getApplicationContext();
+ CameraDeviceSurfaceManager surfaceManager = new FakeCameraDeviceSurfaceManager();
+ UseCaseConfigurationFactory defaultConfigFactory = new ExtendableUseCaseConfigFactory();
+ AppConfiguration.Builder appConfigBuilder =
+ new AppConfiguration.Builder()
+ .setCameraFactory(cameraFactory)
+ .setDeviceSurfaceManager(surfaceManager)
+ .setUseCaseConfigFactory(defaultConfigFactory);
+
+ // CameraX.init will actually init just once across all test cases. However we need to get
+ // the real CameraFactory instance being injected into the init process. So here we store
+ // the
+ // CameraFactory instance in static fields.
+ CameraX.init(context, appConfigBuilder.build());
+ lifecycle = new FakeLifecycleOwner();
+ cameraId = getCameraIdUnchecked(LensFacing.BACK);
+ camera = cameraFactory.getCamera(cameraId);
+ latch = new CountDownLatch(1);
+ errorListener = new CountingErrorListener(latch);
+ handlerThread = new HandlerThread("ErrorHandlerThread");
+ handlerThread.start();
+ handler = new Handler(handlerThread.getLooper());
+ }
+
+ @After
+ public void tearDown() throws InterruptedException {
+ CameraX.unbindAll();
+ handlerThread.quitSafely();
+
+ // Wait some time for the cameras to close. We need the cameras to close to bring CameraX
+ // back
+ // to the initial state.
+ Thread.sleep(3000);
+ }
+
+ @Test
+ public void bind_createsNewUseCaseGroup() {
+ CameraX.bindToLifecycle(lifecycle, new FakeUseCase());
+
+ // One observer is the use case group. The other observer removes the use case upon the
+ // lifecycle's destruction.
+ assertThat(lifecycle.getObserverCount()).isEqualTo(2);
+ }
+
+ @Test
+ public void bindMultipleUseCases() {
+ FakeUseCaseConfiguration configuration0 =
+ new FakeUseCaseConfiguration.Builder().setTargetName("config0").build();
+ FakeUseCase fakeUseCase = new FakeUseCase(configuration0);
+ FakeOtherUseCaseConfiguration configuration1 =
+ new FakeOtherUseCaseConfiguration.Builder().setTargetName("config1").build();
+ FakeOtherUseCase fakeOtherUseCase = new FakeOtherUseCase(configuration1);
+
+ CameraX.bindToLifecycle(lifecycle, fakeUseCase, fakeOtherUseCase);
+
+ assertThat(CameraX.isBound(fakeUseCase)).isTrue();
+ assertThat(CameraX.isBound(fakeOtherUseCase)).isTrue();
+ }
+
+ @Test
+ public void isNotBound_afterUnbind() {
+ FakeUseCase fakeUseCase = new FakeUseCase();
+ CameraX.bindToLifecycle(lifecycle, fakeUseCase);
+
+ CameraX.unbind(fakeUseCase);
+ assertThat(CameraX.isBound(fakeUseCase)).isFalse();
+ }
+
+ @Test
+ public void bind_createsDifferentUseCaseGroups_forDifferentLifecycles() {
+ FakeUseCaseConfiguration configuration0 =
+ new FakeUseCaseConfiguration.Builder().setTargetName("config0").build();
+ CameraX.bindToLifecycle(lifecycle, new FakeUseCase(configuration0));
+
+ FakeUseCaseConfiguration configuration1 =
+ new FakeUseCaseConfiguration.Builder().setTargetName("config1").build();
+ FakeLifecycleOwner anotherLifecycle = new FakeLifecycleOwner();
+ CameraX.bindToLifecycle(anotherLifecycle, new FakeUseCase(configuration1));
+
+ // One observer is the use case group. The other observer removes the use case upon the
+ // lifecycle's destruction.
+ assertThat(lifecycle.getObserverCount()).isEqualTo(2);
+ assertThat(anotherLifecycle.getObserverCount()).isEqualTo(2);
+ }
+
+ @Test
+ public void exception_withDestroyedLifecycle() {
+ FakeUseCase useCase = new FakeUseCase();
+
+ lifecycle.destroy();
+
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> {
+ CameraX.bindToLifecycle(lifecycle, useCase);
+ });
+ }
+
+ @Test
+ public void errorListenerGetsCalled_whenErrorPosted() throws InterruptedException {
+ CameraX.setErrorListener(errorListener, handler);
+ CameraX.postError(CameraX.ErrorCode.CAMERA_STATE_INCONSISTENT, "");
+ latch.await(1, TimeUnit.SECONDS);
+
+ assertThat(errorListener.getCount()).isEqualTo(1);
+ }
+
+ @Test
+ public void requestingDefaultConfiguration_returnsDefaultConfiguration() {
+ // Requesting a default configuration will throw if CameraX is not initialized.
+ FakeUseCaseConfiguration config =
+ CameraX.getDefaultUseCaseConfiguration(FakeUseCaseConfiguration.class);
+ assertThat(config).isNotNull();
+ assertThat(config.getTargetClass(null)).isEqualTo(FakeUseCase.class);
+ }
+
+ @Test
+ public void attachCameraControl_afterBindToLifecycle() {
+ FakeUseCaseConfiguration configuration0 =
+ new FakeUseCaseConfiguration.Builder().setTargetName("config0").build();
+ AttachCameraFakeCase fakeUseCase = new AttachCameraFakeCase(configuration0);
+
+ CameraX.bindToLifecycle(lifecycle, fakeUseCase);
+
+ assertThat(fakeUseCase.getCameraControl(cameraId)).isEqualTo(camera.getCameraControl());
+ }
+
+ @Test
+ public void onCameraControlReadyIsCalled_afterBindToLifecycle() {
+ FakeUseCaseConfiguration configuration0 =
+ new FakeUseCaseConfiguration.Builder().setTargetName("config0").build();
+ AttachCameraFakeCase fakeUseCase = spy(new AttachCameraFakeCase(configuration0));
+
+ CameraX.bindToLifecycle(lifecycle, fakeUseCase);
+
+ Mockito.verify(fakeUseCase).onCameraControlReady(cameraId);
+ }
+
+ @Test
+ public void detachCameraControl_afterUnbind() {
+ FakeUseCaseConfiguration configuration0 =
+ new FakeUseCaseConfiguration.Builder().setTargetName("config0").build();
+ AttachCameraFakeCase fakeUseCase = new AttachCameraFakeCase(configuration0);
+ CameraX.bindToLifecycle(lifecycle, fakeUseCase);
+
+ CameraX.unbind(fakeUseCase);
+
+ // after unbind, Camera's CameraControl should be detached from Usecase
+ assertThat(fakeUseCase.getCameraControl(cameraId)).isNotEqualTo(camera.getCameraControl());
+ // UseCase still gets a non-null default CameraControl that does nothing.
+ assertThat(fakeUseCase.getCameraControl(cameraId)).isNotNull();
+ }
+
+ @Test
+ public void canRetrieveCameraInfo() throws CameraInfoUnavailableException {
+ String cameraId = CameraX.getCameraWithLensFacing(LensFacing.BACK);
+ CameraInfo cameraInfo = CameraX.getCameraInfo(cameraId);
+ assertThat(cameraInfo).isNotNull();
+ }
+
+ private static class CountingErrorListener implements ErrorListener {
+ CountDownLatch latch;
+ AtomicInteger count = new AtomicInteger(0);
+
+ CountingErrorListener(CountDownLatch latch) {
+ this.latch = latch;
+ }
+
+ @Override
+ public void onError(ErrorCode errorCode, String message) {
+ count.getAndIncrement();
+ latch.countDown();
+ }
+
+ public int getCount() {
+ return count.get();
+ }
+ }
+
+ /** FakeUseCase that will call attachToCamera */
+ public static class AttachCameraFakeCase extends FakeUseCase {
+
+ public AttachCameraFakeCase(FakeUseCaseConfiguration configuration) {
+ super(configuration);
+ }
+
+ @Override
+ protected Map<String, Size> onSuggestedResolutionUpdated(
+ Map<String, Size> suggestedResolutionMap) {
+
+ SessionConfiguration.Builder builder = new SessionConfiguration.Builder();
+
+ CameraDeviceConfiguration configuration =
+ (CameraDeviceConfiguration) getUseCaseConfiguration();
+ String cameraId = getCameraIdUnchecked(configuration.getLensFacing());
+ attachToCamera(cameraId, builder.build());
+ return suggestedResolutionMap;
+ }
+ }
+}
diff --git a/camera/core/src/androidTest/java/androidx/camera/core/CaptureRequestConfigurationAndroidTest.java b/camera/core/src/androidTest/java/androidx/camera/core/CaptureRequestConfigurationAndroidTest.java
new file mode 100644
index 0000000..f883966
--- /dev/null
+++ b/camera/core/src/androidTest/java/androidx/camera/core/CaptureRequestConfigurationAndroidTest.java
@@ -0,0 +1,137 @@
+/*
+ * Copyright (C) 2019 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.core;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.hardware.camera2.CameraAccessException;
+import android.hardware.camera2.CameraDevice;
+import android.hardware.camera2.CaptureRequest;
+import android.hardware.camera2.CaptureRequest.Key;
+import android.view.Surface;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mockito;
+
+import java.util.List;
+import java.util.Map;
+
+@RunWith(AndroidJUnit4.class)
+public class CaptureRequestConfigurationAndroidTest {
+ private DeferrableSurface mockSurface0;
+
+ @Before
+ public void setup() {
+ mockSurface0 = Mockito.mock(DeferrableSurface.class);
+ }
+
+ @Test
+ public void buildCaptureRequestWithNullCameraDevice() throws CameraAccessException {
+ CaptureRequestConfiguration.Builder builder = new CaptureRequestConfiguration.Builder();
+ CameraDevice cameraDevice = null;
+ CaptureRequestConfiguration captureRequestConfiguration = builder.build();
+
+ CaptureRequest.Builder captureRequestBuilder =
+ captureRequestConfiguration.buildCaptureRequest(cameraDevice);
+
+ assertThat(captureRequestBuilder).isNull();
+ }
+
+ @Test
+ public void builderSetTemplate() {
+ CaptureRequestConfiguration.Builder builder = new CaptureRequestConfiguration.Builder();
+
+ builder.setTemplateType(CameraDevice.TEMPLATE_PREVIEW);
+ CaptureRequestConfiguration captureRequestConfiguration = builder.build();
+
+ assertThat(captureRequestConfiguration.getTemplateType())
+ .isEqualTo(CameraDevice.TEMPLATE_PREVIEW);
+ }
+
+ @Test
+ public void builderAddSurface() {
+ CaptureRequestConfiguration.Builder builder = new CaptureRequestConfiguration.Builder();
+
+ builder.addSurface(mockSurface0);
+ CaptureRequestConfiguration captureRequestConfiguration = builder.build();
+
+ List<DeferrableSurface> surfaces = captureRequestConfiguration.getSurfaces();
+
+ assertThat(surfaces).hasSize(1);
+ assertThat(surfaces).contains(mockSurface0);
+ }
+
+ @Test
+ public void builderRemoveSurface() {
+ CaptureRequestConfiguration.Builder builder = new CaptureRequestConfiguration.Builder();
+
+ builder.addSurface(mockSurface0);
+ builder.removeSurface(mockSurface0);
+ CaptureRequestConfiguration captureRequestConfiguration = builder.build();
+
+ List<Surface> surfaces =
+ DeferrableSurfaces.surfaceList(captureRequestConfiguration.getSurfaces());
+ assertThat(surfaces).isEmpty();
+ }
+
+ @Test
+ public void builderClearSurface() {
+ CaptureRequestConfiguration.Builder builder = new CaptureRequestConfiguration.Builder();
+
+ builder.addSurface(mockSurface0);
+ builder.clearSurfaces();
+ CaptureRequestConfiguration captureRequestConfiguration = builder.build();
+
+ List<Surface> surfaces =
+ DeferrableSurfaces.surfaceList(captureRequestConfiguration.getSurfaces());
+ assertThat(surfaces.size()).isEqualTo(0);
+ }
+
+ @Test
+ public void builderAddCharacteristic() {
+ CaptureRequestConfiguration.Builder builder = new CaptureRequestConfiguration.Builder();
+
+ builder.addCharacteristic(
+ CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_AUTO);
+ CaptureRequestConfiguration captureRequestConfiguration = builder.build();
+
+ Map<Key<?>, CaptureRequestParameter<?>> parameterMap =
+ captureRequestConfiguration.getCameraCharacteristics();
+
+ assertThat(parameterMap.containsKey(CaptureRequest.CONTROL_AF_MODE)).isTrue();
+ assertThat(parameterMap)
+ .containsEntry(
+ CaptureRequest.CONTROL_AF_MODE,
+ CaptureRequestParameter.create(
+ CaptureRequest.CONTROL_AF_MODE,
+ CaptureRequest.CONTROL_AF_MODE_AUTO));
+ }
+
+ @Test
+ public void builderSetUseTargetedSurface() {
+ CaptureRequestConfiguration.Builder builder = new CaptureRequestConfiguration.Builder();
+
+ builder.setUseRepeatingSurface(true);
+ CaptureRequestConfiguration captureRequestConfiguration = builder.build();
+
+ assertThat(captureRequestConfiguration.isUseRepeatingSurface()).isTrue();
+ }
+}
diff --git a/camera/core/src/androidTest/java/androidx/camera/core/CaptureRequestParameterAndroidTest.java b/camera/core/src/androidTest/java/androidx/camera/core/CaptureRequestParameterAndroidTest.java
new file mode 100644
index 0000000..da520917
--- /dev/null
+++ b/camera/core/src/androidTest/java/androidx/camera/core/CaptureRequestParameterAndroidTest.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright (C) 2019 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.core;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.hardware.camera2.CameraAccessException;
+import android.hardware.camera2.CameraDevice;
+import android.hardware.camera2.CaptureRequest;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+public class CaptureRequestParameterAndroidTest {
+ private CameraDevice cameraDevice;
+
+ @Before
+ public void setup() throws CameraAccessException, InterruptedException {
+ cameraDevice = CameraUtil.getCameraDevice();
+ }
+
+ @After
+ public void teardown() {
+ CameraUtil.releaseCameraDevice(cameraDevice);
+ }
+
+ @Test
+ public void instanceCreation() {
+ CaptureRequestParameter<?> captureRequestParameter =
+ CaptureRequestParameter.create(
+ CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_AUTO);
+
+ assertThat(captureRequestParameter.getKey()).isEqualTo(CaptureRequest.CONTROL_AF_MODE);
+ assertThat(captureRequestParameter.getValue())
+ .isEqualTo(CaptureRequest.CONTROL_AF_MODE_AUTO);
+ }
+
+ @Test
+ public void applyParameter() throws CameraAccessException {
+ CaptureRequest.Builder builder =
+ cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);
+
+ assertThat(builder).isNotNull();
+
+ CaptureRequestParameter<?> captureRequestParameter =
+ CaptureRequestParameter.create(
+ CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_AUTO);
+
+ captureRequestParameter.apply(builder);
+
+ assertThat(builder.get(CaptureRequest.CONTROL_AF_MODE))
+ .isEqualTo(CaptureRequest.CONTROL_AF_MODE_AUTO);
+ }
+}
diff --git a/camera/core/src/androidTest/java/androidx/camera/core/ErrorHandlerAndroidTest.java b/camera/core/src/androidTest/java/androidx/camera/core/ErrorHandlerAndroidTest.java
new file mode 100644
index 0000000..d750b5c
--- /dev/null
+++ b/camera/core/src/androidTest/java/androidx/camera/core/ErrorHandlerAndroidTest.java
@@ -0,0 +1,109 @@
+/*
+ * Copyright (C) 2019 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.core;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.os.Handler;
+import android.os.HandlerThread;
+
+import androidx.camera.core.CameraX.ErrorCode;
+import androidx.camera.core.CameraX.ErrorListener;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
+
+@RunWith(AndroidJUnit4.class)
+public class ErrorHandlerAndroidTest {
+ private ErrorHandler errorHandler;
+ private CountingErrorListener errorListener0;
+ private CountingErrorListener errorListener1;
+ private HandlerThread handlerThread;
+ private Handler handler;
+ private CountDownLatch latch;
+
+ @Before
+ public void setup() {
+ errorHandler = new ErrorHandler();
+ latch = new CountDownLatch(1);
+ errorListener0 = new CountingErrorListener(latch);
+ errorListener1 = new CountingErrorListener(latch);
+
+ handlerThread = new HandlerThread("ErrorHandlerThread");
+ handlerThread.start();
+ handler = new Handler(handlerThread.getLooper());
+ }
+
+ @Test
+ public void errorListenerCalled_whenSet() throws InterruptedException {
+ errorHandler.setErrorListener(errorListener0, handler);
+
+ errorHandler.postError(CameraX.ErrorCode.CAMERA_STATE_INCONSISTENT, "");
+
+ latch.await(1, TimeUnit.SECONDS);
+
+ assertThat(errorListener0.getCount()).isEqualTo(1);
+ }
+
+ @Test
+ public void errorListenerRemoved_whenNullSet() throws InterruptedException {
+ errorHandler.setErrorListener(errorListener0, handler);
+ errorHandler.setErrorListener(null, handler);
+
+ errorHandler.postError(CameraX.ErrorCode.CAMERA_STATE_INCONSISTENT, "");
+
+ assertThat(latch.await(1, TimeUnit.SECONDS)).isFalse();
+ }
+
+ @Test
+ public void errorListenerReplaced() throws InterruptedException {
+ errorHandler.setErrorListener(errorListener0, handler);
+ errorHandler.setErrorListener(errorListener1, handler);
+
+ errorHandler.postError(CameraX.ErrorCode.CAMERA_STATE_INCONSISTENT, "");
+
+ latch.await(1, TimeUnit.SECONDS);
+
+ assertThat(errorListener0.getCount()).isEqualTo(0);
+ assertThat(errorListener1.getCount()).isEqualTo(1);
+ }
+
+ private static class CountingErrorListener implements ErrorListener {
+ CountDownLatch latch;
+ AtomicInteger count = new AtomicInteger(0);
+
+ CountingErrorListener(CountDownLatch latch) {
+ this.latch = latch;
+ }
+
+ @Override
+ public void onError(ErrorCode errorCode, String message) {
+ count.getAndIncrement();
+ latch.countDown();
+ }
+
+ public int getCount() {
+ return count.get();
+ }
+ }
+}
diff --git a/camera/core/src/androidTest/java/androidx/camera/core/FakeActivity.java b/camera/core/src/androidTest/java/androidx/camera/core/FakeActivity.java
new file mode 100644
index 0000000..1293807
--- /dev/null
+++ b/camera/core/src/androidTest/java/androidx/camera/core/FakeActivity.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2019 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.core;
+
+import android.app.Activity;
+import android.os.Bundle;
+
+/** A fake {@link Activity} that checks properties of the CameraX library. */
+public class FakeActivity extends Activity {
+ private volatile boolean isCameraXInitializedAtOnCreate = false;
+
+ @Override
+ protected void onCreate(Bundle savedInstance) {
+ super.onCreate(savedInstance);
+ isCameraXInitializedAtOnCreate = CameraX.isInitialized();
+ }
+
+ /** Returns true if CameraX is initialized when {@link #onCreate(Bundle)} is called. */
+ public boolean isCameraXInitializedAtOnCreate() {
+ return isCameraXInitializedAtOnCreate;
+ }
+}
diff --git a/camera/core/src/androidTest/java/androidx/camera/core/FakeOtherUseCase.java b/camera/core/src/androidTest/java/androidx/camera/core/FakeOtherUseCase.java
new file mode 100644
index 0000000..e6b0ed5
--- /dev/null
+++ b/camera/core/src/androidTest/java/androidx/camera/core/FakeOtherUseCase.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2019 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.core;
+
+import android.util.Size;
+
+import androidx.camera.core.CameraX.LensFacing;
+
+import java.util.Map;
+
+/**
+ * A second fake {@link BaseUseCase}.
+ *
+ * <p>This is used to complement the {@link FakeUseCase} for testing instances where a use case of
+ * different type is created.
+ */
+class FakeOtherUseCase extends BaseUseCase {
+ private volatile boolean isCleared = false;
+
+ /** Creates a new instance of a {@link FakeOtherUseCase} with a given configuration. */
+ FakeOtherUseCase(FakeOtherUseCaseConfiguration configuration) {
+ super(configuration);
+ }
+
+ /** Creates a new instance of a {@link FakeOtherUseCase} with a default configuration. */
+ FakeOtherUseCase() {
+ this(new FakeOtherUseCaseConfiguration.Builder().build());
+ }
+
+ @Override
+ public void clear() {
+ super.clear();
+ isCleared = true;
+ }
+
+ @Override
+ protected UseCaseConfiguration.Builder<?, ?, ?> getDefaultBuilder() {
+ return new FakeOtherUseCaseConfiguration.Builder().setLensFacing(LensFacing.BACK);
+ }
+
+ @Override
+ protected Map<String, Size> onSuggestedResolutionUpdated(
+ Map<String, Size> suggestedResolutionMap) {
+ return suggestedResolutionMap;
+ }
+
+ /** Returns true if {@link #clear()} has been called previously. */
+ public boolean isCleared() {
+ return isCleared;
+ }
+}
diff --git a/camera/core/src/androidTest/java/androidx/camera/core/FakeOtherUseCaseConfiguration.java b/camera/core/src/androidTest/java/androidx/camera/core/FakeOtherUseCaseConfiguration.java
new file mode 100644
index 0000000..acb0044
--- /dev/null
+++ b/camera/core/src/androidTest/java/androidx/camera/core/FakeOtherUseCaseConfiguration.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2019 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.core;
+
+/** A fake configuration for {@link FakeOtherUseCase}. */
+public class FakeOtherUseCaseConfiguration
+ implements UseCaseConfiguration<FakeOtherUseCase>, CameraDeviceConfiguration {
+
+ private final Configuration config;
+
+ private FakeOtherUseCaseConfiguration(Configuration config) {
+ this.config = config;
+ }
+
+ @Override
+ public Configuration getConfiguration() {
+ return config;
+ }
+
+ /** Builder for an empty Configuration */
+ public static final class Builder
+ implements UseCaseConfiguration.Builder<
+ FakeOtherUseCase, FakeOtherUseCaseConfiguration, Builder>,
+ CameraDeviceConfiguration.Builder<FakeOtherUseCaseConfiguration, Builder> {
+
+ private final MutableOptionsBundle optionsBundle;
+
+ public Builder() {
+ optionsBundle = MutableOptionsBundle.create();
+ setTargetClass(FakeOtherUseCase.class);
+ }
+
+ @Override
+ public MutableConfiguration getMutableConfiguration() {
+ return optionsBundle;
+ }
+
+ @Override
+ public Builder builder() {
+ return this;
+ }
+
+ @Override
+ public FakeOtherUseCaseConfiguration build() {
+ return new FakeOtherUseCaseConfiguration(OptionsBundle.from(optionsBundle));
+ }
+ }
+}
diff --git a/camera/core/src/androidTest/java/androidx/camera/core/FakeUseCase.java b/camera/core/src/androidTest/java/androidx/camera/core/FakeUseCase.java
new file mode 100644
index 0000000..4be3a63
--- /dev/null
+++ b/camera/core/src/androidTest/java/androidx/camera/core/FakeUseCase.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright (C) 2019 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.core;
+
+import android.util.Size;
+
+import androidx.annotation.RestrictTo;
+import androidx.annotation.RestrictTo.Scope;
+import androidx.camera.core.CameraX.LensFacing;
+
+import java.util.Map;
+
+/** A fake {@link BaseUseCase}. */
+@RestrictTo(Scope.LIBRARY_GROUP)
+public class FakeUseCase extends BaseUseCase {
+ private volatile boolean isCleared = false;
+
+ /** Creates a new instance of a {@link FakeUseCase} with a given configuration. */
+ protected FakeUseCase(FakeUseCaseConfiguration configuration) {
+ super(configuration);
+ }
+
+ /** Creates a new instance of a {@link FakeUseCase} with a default configuration. */
+ protected FakeUseCase() {
+ this(new FakeUseCaseConfiguration.Builder().build());
+ }
+
+ @Override
+ protected UseCaseConfiguration.Builder<?, ?, ?> getDefaultBuilder() {
+ return new FakeUseCaseConfiguration.Builder()
+ .setLensFacing(LensFacing.BACK)
+ .setOptionUnpacker((useCaseConfig, sessionConfigBuilder) -> {
+ });
+ }
+
+ @Override
+ public void clear() {
+ super.clear();
+ isCleared = true;
+ }
+
+ @Override
+ protected Map<String, Size> onSuggestedResolutionUpdated(
+ Map<String, Size> suggestedResolutionMap) {
+ return suggestedResolutionMap;
+ }
+
+ /** Returns true if {@link #clear()} has been called previously. */
+ public boolean isCleared() {
+ return isCleared;
+ }
+}
diff --git a/camera/core/src/androidTest/java/androidx/camera/core/FakeUseCaseConfiguration.java b/camera/core/src/androidTest/java/androidx/camera/core/FakeUseCaseConfiguration.java
new file mode 100644
index 0000000..270a194
--- /dev/null
+++ b/camera/core/src/androidTest/java/androidx/camera/core/FakeUseCaseConfiguration.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright (C) 2019 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.core;
+
+/** A fake configuration for {@link FakeUseCase}. */
+public class FakeUseCaseConfiguration
+ implements UseCaseConfiguration<FakeUseCase>, CameraDeviceConfiguration {
+
+ private final Configuration config;
+
+ private FakeUseCaseConfiguration(Configuration config) {
+ this.config = config;
+ }
+
+ @Override
+ public Configuration getConfiguration() {
+ return config;
+ }
+
+ /** Builder for an empty Configuration */
+ public static final class Builder
+ implements UseCaseConfiguration.Builder<FakeUseCase, FakeUseCaseConfiguration, Builder>,
+ CameraDeviceConfiguration.Builder<FakeUseCaseConfiguration, Builder> {
+
+ private final MutableOptionsBundle optionsBundle;
+
+ public Builder() {
+ optionsBundle = MutableOptionsBundle.create();
+ setTargetClass(FakeUseCase.class);
+ }
+
+ @Override
+ public MutableConfiguration getMutableConfiguration() {
+ return optionsBundle;
+ }
+
+ @Override
+ public Builder builder() {
+ return this;
+ }
+
+ @Override
+ public FakeUseCaseConfiguration build() {
+ return new FakeUseCaseConfiguration(OptionsBundle.from(optionsBundle));
+ }
+ }
+}
diff --git a/camera/core/src/androidTest/java/androidx/camera/core/ForwardingImageProxyAndroidTest.java b/camera/core/src/androidTest/java/androidx/camera/core/ForwardingImageProxyAndroidTest.java
new file mode 100644
index 0000000..0dca471
--- /dev/null
+++ b/camera/core/src/androidTest/java/androidx/camera/core/ForwardingImageProxyAndroidTest.java
@@ -0,0 +1,141 @@
+/*
+ * Copyright (C) 2019 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.core;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.graphics.ImageFormat;
+import android.graphics.Rect;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.concurrent.Semaphore;
+import java.util.concurrent.atomic.AtomicReference;
+
+@RunWith(AndroidJUnit4.class)
+public final class ForwardingImageProxyAndroidTest {
+
+ private final ImageProxy baseImageProxy = mock(ImageProxy.class);
+ private final ImageProxy.PlaneProxy yPlane = mock(ImageProxy.PlaneProxy.class);
+ private final ImageProxy.PlaneProxy uPlane = mock(ImageProxy.PlaneProxy.class);
+ private final ImageProxy.PlaneProxy vPlane = mock(ImageProxy.PlaneProxy.class);
+ private ForwardingImageProxy imageProxy;
+
+ @Before
+ public void setUp() {
+ imageProxy = new ConcreteImageProxy(baseImageProxy);
+ }
+
+ @Test
+ public void close_closesWrappedImage() {
+ imageProxy.close();
+
+ verify(baseImageProxy).close();
+ }
+
+ @Test(timeout = 2000)
+ public void close_notifiesOnImageCloseListener_afterSetOnImageCloseListener()
+ throws InterruptedException {
+ Semaphore closedImageSemaphore = new Semaphore(/*permits=*/ 0);
+ AtomicReference<ImageProxy> closedImage = new AtomicReference<>();
+ imageProxy.addOnImageCloseListener(
+ image -> {
+ closedImage.set(image);
+ closedImageSemaphore.release();
+ });
+
+ imageProxy.close();
+
+ closedImageSemaphore.acquire();
+ assertThat(closedImage.get()).isSameAs(imageProxy);
+ }
+
+ @Test
+ public void getCropRect_returnsCropRectForWrappedImage() {
+ when(baseImageProxy.getCropRect()).thenReturn(new Rect(0, 0, 20, 20));
+
+ assertThat(imageProxy.getCropRect()).isEqualTo(new Rect(0, 0, 20, 20));
+ }
+
+ @Test
+ public void setCropRect_setsCropRectForWrappedImage() {
+ imageProxy.setCropRect(new Rect(0, 0, 40, 40));
+
+ verify(baseImageProxy).setCropRect(new Rect(0, 0, 40, 40));
+ }
+
+ @Test
+ public void getFormat_returnsFormatForWrappedImage() {
+ when(baseImageProxy.getFormat()).thenReturn(ImageFormat.YUV_420_888);
+
+ assertThat(imageProxy.getFormat()).isEqualTo(ImageFormat.YUV_420_888);
+ }
+
+ @Test
+ public void getHeight_returnsHeightForWrappedImage() {
+ when(baseImageProxy.getHeight()).thenReturn(480);
+
+ assertThat(imageProxy.getHeight()).isEqualTo(480);
+ }
+
+ @Test
+ public void getWidth_returnsWidthForWrappedImage() {
+ when(baseImageProxy.getWidth()).thenReturn(640);
+
+ assertThat(imageProxy.getWidth()).isEqualTo(640);
+ }
+
+ @Test
+ public void getTimestamp_returnsTimestampForWrappedImage() {
+ when(baseImageProxy.getTimestamp()).thenReturn(138990020L);
+
+ assertThat(imageProxy.getTimestamp()).isEqualTo(138990020L);
+ }
+
+ @Test
+ public void setTimestamp_setsTimestampForWrappedImage() {
+ imageProxy.setTimestamp(138990020L);
+
+ verify(baseImageProxy).setTimestamp(138990020L);
+ }
+
+ @Test
+ public void getPlanes_returnsPlanesForWrappedImage() {
+ when(baseImageProxy.getPlanes())
+ .thenReturn(new ImageProxy.PlaneProxy[]{yPlane, uPlane, vPlane});
+
+ ImageProxy.PlaneProxy[] planes = imageProxy.getPlanes();
+ assertThat(planes.length).isEqualTo(3);
+ assertThat(planes[0]).isEqualTo(yPlane);
+ assertThat(planes[1]).isEqualTo(uPlane);
+ assertThat(planes[2]).isEqualTo(vPlane);
+ }
+
+ private static final class ConcreteImageProxy extends ForwardingImageProxy {
+ private ConcreteImageProxy(ImageProxy baseImageProxy) {
+ super(baseImageProxy);
+ }
+ }
+}
diff --git a/camera/core/src/androidTest/java/androidx/camera/core/ForwardingImageReaderListenerAndroidTest.java b/camera/core/src/androidTest/java/androidx/camera/core/ForwardingImageReaderListenerAndroidTest.java
new file mode 100644
index 0000000..6c098aa
--- /dev/null
+++ b/camera/core/src/androidTest/java/androidx/camera/core/ForwardingImageReaderListenerAndroidTest.java
@@ -0,0 +1,184 @@
+/*
+ * Copyright (C) 2019 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.core;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.timeout;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.graphics.ImageFormat;
+import android.media.Image;
+import android.media.ImageReader;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.view.Surface;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.Semaphore;
+
+@RunWith(AndroidJUnit4.class)
+public final class ForwardingImageReaderListenerAndroidTest {
+ private static final int IMAGE_WIDTH = 640;
+ private static final int IMAGE_HEIGHT = 480;
+ private static final int IMAGE_FORMAT = ImageFormat.YUV_420_888;
+ private static final int MAX_IMAGES = 10;
+
+ private final ImageReader imageReader = mock(ImageReader.class);
+ private final Surface surface = mock(Surface.class);
+ private HandlerThread handlerThread;
+ private Handler handler;
+ private List<QueuedImageReaderProxy> imageReaderProxys;
+ private ForwardingImageReaderListener forwardingListener;
+
+ private static Image createMockImage() {
+ Image image = mock(Image.class);
+ when(image.getWidth()).thenReturn(IMAGE_WIDTH);
+ when(image.getHeight()).thenReturn(IMAGE_HEIGHT);
+ when(image.getFormat()).thenReturn(IMAGE_FORMAT);
+ return image;
+ }
+
+ private static ImageReaderProxy.OnImageAvailableListener createMockListener() {
+ return mock(ImageReaderProxy.OnImageAvailableListener.class);
+ }
+
+ /**
+ * Returns a listener which immediately acquires the next image, closes the image, and releases
+ * a semaphore.
+ */
+ private static ImageReaderProxy.OnImageAvailableListener
+ createSemaphoreReleasingClosingListener(Semaphore semaphore) {
+ return imageReaderProxy -> {
+ imageReaderProxy.acquireNextImage().close();
+ semaphore.release();
+ };
+ }
+
+ @Before
+ public void setUp() {
+ handlerThread = new HandlerThread("listener");
+ handlerThread.start();
+ handler = new Handler(handlerThread.getLooper());
+ imageReaderProxys = new ArrayList<>(3);
+ for (int i = 0; i < 3; ++i) {
+ imageReaderProxys.add(
+ new QueuedImageReaderProxy(
+ IMAGE_WIDTH, IMAGE_HEIGHT, IMAGE_FORMAT, MAX_IMAGES, surface));
+ }
+ forwardingListener = new ForwardingImageReaderListener(imageReaderProxys);
+ }
+
+ @After
+ public void tearDown() {
+ handlerThread.quitSafely();
+ }
+
+ @Test
+ public void newImageIsForwardedToAllListeners() {
+ Image baseImage = createMockImage();
+ when(imageReader.acquireNextImage()).thenReturn(baseImage);
+ List<ImageReaderProxy.OnImageAvailableListener> listeners = new ArrayList<>();
+ for (ImageReaderProxy imageReaderProxy : imageReaderProxys) {
+ ImageReaderProxy.OnImageAvailableListener listener = createMockListener();
+ imageReaderProxy.setOnImageAvailableListener(listener, handler);
+ listeners.add(listener);
+ }
+
+ final int availableImages = 5;
+ for (int i = 0; i < availableImages; ++i) {
+ forwardingListener.onImageAvailable(imageReader);
+ }
+
+ for (int i = 0; i < imageReaderProxys.size(); ++i) {
+ // Listener should be notified about every available image.
+ verify(listeners.get(i), timeout(2000).times(availableImages))
+ .onImageAvailable(imageReaderProxys.get(i));
+ }
+ }
+
+ @Test(timeout = 2000)
+ public void baseImageIsClosed_allQueuesAreCleared_whenAllForwardedCopiesAreClosed()
+ throws InterruptedException {
+ Semaphore onCloseSemaphore = new Semaphore(/*permits=*/ 0);
+ Image baseImage = createMockImage();
+ when(imageReader.acquireNextImage()).thenReturn(baseImage);
+ for (ImageReaderProxy imageReaderProxy : imageReaderProxys) {
+ // Close the image for every listener.
+ imageReaderProxy.setOnImageAvailableListener(
+ createSemaphoreReleasingClosingListener(onCloseSemaphore), handler);
+ }
+
+ final int availableImages = 5;
+ for (int i = 0; i < availableImages; ++i) {
+ forwardingListener.onImageAvailable(imageReader);
+ }
+ onCloseSemaphore.acquire(availableImages * imageReaderProxys.size());
+
+ // Base image should be closed every time.
+ verify(baseImage, times(availableImages)).close();
+ // All queues should be cleared.
+ for (QueuedImageReaderProxy imageReaderProxy : imageReaderProxys) {
+ assertThat(imageReaderProxy.getCurrentImages()).isEqualTo(0);
+ }
+ }
+
+ @Test(timeout = 2000)
+ public void baseImageIsNotClosed_someQueuesAreCleared_whenNotAllForwardedCopiesAreClosed()
+ throws InterruptedException {
+ Semaphore onCloseSemaphore = new Semaphore(/*permits=*/ 0);
+ Image baseImage = createMockImage();
+ when(imageReader.acquireNextImage()).thenReturn(baseImage);
+ // Don't close the image for the first listener.
+ imageReaderProxys.get(0).setOnImageAvailableListener(createMockListener(), handler);
+ // Close the image for the other listeners.
+ imageReaderProxys
+ .get(1)
+ .setOnImageAvailableListener(
+ createSemaphoreReleasingClosingListener(onCloseSemaphore), handler);
+ imageReaderProxys
+ .get(2)
+ .setOnImageAvailableListener(
+ createSemaphoreReleasingClosingListener(onCloseSemaphore), handler);
+
+ final int availableImages = 5;
+ for (int i = 0; i < availableImages; ++i) {
+ forwardingListener.onImageAvailable(imageReader);
+ }
+ onCloseSemaphore.acquire(availableImages * (imageReaderProxys.size() - 1));
+
+ // Base image should not be closed every time.
+ verify(baseImage, never()).close();
+ // First reader's queue should not be cleared.
+ assertThat(imageReaderProxys.get(0).getCurrentImages()).isEqualTo(availableImages);
+ // Other readers' queues should be cleared.
+ assertThat(imageReaderProxys.get(1).getCurrentImages()).isEqualTo(0);
+ assertThat(imageReaderProxys.get(2).getCurrentImages()).isEqualTo(0);
+ }
+}
diff --git a/camera/core/src/androidTest/java/androidx/camera/core/ImageProxyDownsamplerAndroidTest.java b/camera/core/src/androidTest/java/androidx/camera/core/ImageProxyDownsamplerAndroidTest.java
new file mode 100644
index 0000000..87abf93
--- /dev/null
+++ b/camera/core/src/androidTest/java/androidx/camera/core/ImageProxyDownsamplerAndroidTest.java
@@ -0,0 +1,227 @@
+/*
+ * Copyright (C) 2019 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.core;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import android.graphics.ImageFormat;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.nio.ByteBuffer;
+
+@RunWith(AndroidJUnit4.class)
+public final class ImageProxyDownsamplerAndroidTest {
+ private static final int WIDTH = 8;
+ private static final int HEIGHT = 8;
+
+ private static ImageProxy createYuv420Image(int uvPixelStride) {
+ ImageProxy image = mock(ImageProxy.class);
+ ImageProxy.PlaneProxy[] planes = new ImageProxy.PlaneProxy[3];
+
+ when(image.getWidth()).thenReturn(WIDTH);
+ when(image.getHeight()).thenReturn(HEIGHT);
+ when(image.getFormat()).thenReturn(ImageFormat.YUV_420_888);
+ when(image.getPlanes()).thenReturn(planes);
+
+ planes[0] =
+ createPlaneWithRampPattern(WIDTH, HEIGHT, /*pixelStride=*/ 1, /*initialValue=*/ 0);
+ planes[1] =
+ createPlaneWithRampPattern(
+ WIDTH / 2, HEIGHT / 2, uvPixelStride, /*initialValue=*/ 1);
+ planes[2] =
+ createPlaneWithRampPattern(
+ WIDTH / 2, HEIGHT / 2, uvPixelStride, /*initialValue=*/ 2);
+
+ return image;
+ }
+
+ private static ImageProxy.PlaneProxy createPlaneWithRampPattern(
+ int width, int height, int pixelStride, int initialValue) {
+ return new ImageProxy.PlaneProxy() {
+ final ByteBuffer buffer =
+ createBufferWithRampPattern(width, height, pixelStride, initialValue);
+
+ @Override
+ public int getRowStride() {
+ return width * pixelStride;
+ }
+
+ @Override
+ public int getPixelStride() {
+ return pixelStride;
+ }
+
+ @Override
+ public ByteBuffer getBuffer() {
+ return buffer;
+ }
+ };
+ }
+
+ private static ByteBuffer createBufferWithRampPattern(
+ int width, int height, int pixelStride, int initialValue) {
+ int rowStride = width * pixelStride;
+ ByteBuffer buffer = ByteBuffer.allocateDirect(rowStride * height);
+ int value = initialValue;
+ for (int y = 0; y < height; ++y) {
+ for (int x = 0; x < width; ++x) {
+ buffer.position(y * rowStride + x * pixelStride);
+ buffer.put((byte) (value++ & 0xFF));
+ }
+ }
+ return buffer;
+ }
+
+ private static void checkOutputIsNearestNeighborDownsampledInput(
+ ImageProxy inputImage, ImageProxy outputImage, int downsamplingFactor) {
+ ImageProxy.PlaneProxy[] inputPlanes = inputImage.getPlanes();
+ ImageProxy.PlaneProxy[] outputPlanes = outputImage.getPlanes();
+ for (int c = 0; c < 3; ++c) {
+ ByteBuffer inputBuffer = inputPlanes[c].getBuffer();
+ ByteBuffer outputBuffer = outputPlanes[c].getBuffer();
+ inputBuffer.rewind();
+ outputBuffer.rewind();
+ int divisor = (c == 0) ? 1 : 2;
+ int inputRowStride = inputPlanes[c].getRowStride();
+ int inputPixelStride = inputPlanes[c].getPixelStride();
+ int outputRowStride = outputPlanes[c].getRowStride();
+ int outputPixelStride = outputPlanes[c].getPixelStride();
+ for (int y = 0; y < outputImage.getHeight() / divisor; ++y) {
+ for (int x = 0; x < outputImage.getWidth() / divisor; ++x) {
+ byte inputPixel =
+ inputBuffer.get(
+ y * downsamplingFactor * inputRowStride
+ + x * downsamplingFactor * inputPixelStride);
+ byte outputPixel =
+ outputBuffer.get(y * outputRowStride + x * outputPixelStride);
+ assertThat(outputPixel).isEqualTo(inputPixel);
+ }
+ }
+ }
+ }
+
+ private static void checkOutputIsAveragingDownsampledInput(
+ ImageProxy inputImage, ImageProxy outputImage, int downsamplingFactor) {
+ ImageProxy.PlaneProxy[] inputPlanes = inputImage.getPlanes();
+ ImageProxy.PlaneProxy[] outputPlanes = outputImage.getPlanes();
+ for (int c = 0; c < 3; ++c) {
+ ByteBuffer inputBuffer = inputPlanes[c].getBuffer();
+ ByteBuffer outputBuffer = outputPlanes[c].getBuffer();
+ inputBuffer.rewind();
+ outputBuffer.rewind();
+ int divisor = (c == 0) ? 1 : 2;
+ int inputRowStride = inputPlanes[c].getRowStride();
+ int inputPixelStride = inputPlanes[c].getPixelStride();
+ int outputRowStride = outputPlanes[c].getRowStride();
+ int outputPixelStride = outputPlanes[c].getPixelStride();
+ for (int y = 0; y < outputImage.getHeight() / divisor; ++y) {
+ for (int x = 0; x < outputImage.getWidth() / divisor; ++x) {
+ byte inputPixelA =
+ inputBuffer.get(
+ y * downsamplingFactor * inputRowStride
+ + x * downsamplingFactor * inputPixelStride);
+ byte inputPixelB =
+ inputBuffer.get(
+ y * downsamplingFactor * inputRowStride
+ + (x * downsamplingFactor + 1) * inputPixelStride);
+ byte inputPixelC =
+ inputBuffer.get(
+ (y * downsamplingFactor + 1) * inputRowStride
+ + x * downsamplingFactor * inputPixelStride);
+ byte inputPixelD =
+ inputBuffer.get(
+ (y * downsamplingFactor + 1) * inputRowStride
+ + (x * downsamplingFactor + 1) * inputPixelStride);
+ byte averaged =
+ (byte)
+ ((((inputPixelA & 0xFF)
+ + (inputPixelB & 0xFF)
+ + (inputPixelC & 0xFF)
+ + (inputPixelD & 0xFF))
+ / 4)
+ & 0xFF);
+ byte outputPixel =
+ outputBuffer.get(y * outputRowStride + x * outputPixelStride);
+ assertThat(outputPixel).isEqualTo(averaged);
+ }
+ }
+ }
+ }
+
+ @Test
+ public void nearestNeighborDownsamplingBy2X_whenUVPlanesHavePixelStride1() {
+ ImageProxy inputImage = createYuv420Image(/*uvPixelStride=*/ 1);
+ int downsamplingFactor = 2;
+ ImageProxy outputImage =
+ ImageProxyDownsampler.downsample(
+ inputImage,
+ WIDTH / downsamplingFactor,
+ HEIGHT / downsamplingFactor,
+ ImageProxyDownsampler.DownsamplingMethod.NEAREST_NEIGHBOR);
+
+ checkOutputIsNearestNeighborDownsampledInput(inputImage, outputImage, downsamplingFactor);
+ }
+
+ @Test
+ public void nearestNeighborDownsamplingBy2X_whenUVPlanesHavePixelStride2() {
+ ImageProxy inputImage = createYuv420Image(/*uvPixelStride=*/ 2);
+ int downsamplingFactor = 2;
+ ImageProxy outputImage =
+ ImageProxyDownsampler.downsample(
+ inputImage,
+ WIDTH / downsamplingFactor,
+ HEIGHT / downsamplingFactor,
+ ImageProxyDownsampler.DownsamplingMethod.NEAREST_NEIGHBOR);
+
+ checkOutputIsNearestNeighborDownsampledInput(inputImage, outputImage, downsamplingFactor);
+ }
+
+ @Test
+ public void averagingDownsamplingBy2X_whenUVPlanesHavePixelStride1() {
+ ImageProxy inputImage = createYuv420Image(/*uvPixelStride=*/ 1);
+ int downsamplingFactor = 2;
+ ImageProxy outputImage =
+ ImageProxyDownsampler.downsample(
+ inputImage,
+ WIDTH / downsamplingFactor,
+ HEIGHT / downsamplingFactor,
+ ImageProxyDownsampler.DownsamplingMethod.AVERAGING);
+
+ checkOutputIsAveragingDownsampledInput(inputImage, outputImage, downsamplingFactor);
+ }
+
+ @Test
+ public void averagingDownsamplingBy2X_whenUVPlanesHavePixelStride2() {
+ ImageProxy inputImage = createYuv420Image(/*uvPixelStride=*/ 2);
+ int downsamplingFactor = 2;
+ ImageProxy outputImage =
+ ImageProxyDownsampler.downsample(
+ inputImage,
+ WIDTH / downsamplingFactor,
+ HEIGHT / downsamplingFactor,
+ ImageProxyDownsampler.DownsamplingMethod.AVERAGING);
+
+ checkOutputIsAveragingDownsampledInput(inputImage, outputImage, downsamplingFactor);
+ }
+}
diff --git a/camera/core/src/androidTest/java/androidx/camera/core/ImageSaverAndroidTest.java b/camera/core/src/androidTest/java/androidx/camera/core/ImageSaverAndroidTest.java
new file mode 100644
index 0000000..85a7ba8
--- /dev/null
+++ b/camera/core/src/androidTest/java/androidx/camera/core/ImageSaverAndroidTest.java
@@ -0,0 +1,279 @@
+/*
+ * Copyright (C) 2019 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.core;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Matchers.anyObject;
+import static org.mockito.Matchers.anyString;
+import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.ImageFormat;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.util.Base64;
+import android.util.Rational;
+
+import androidx.annotation.Nullable;
+import androidx.camera.core.ImageSaver.OnImageSavedListener;
+import androidx.camera.core.ImageSaver.SaveError;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.util.concurrent.Semaphore;
+
+@RunWith(AndroidJUnit4.class)
+public class ImageSaverAndroidTest {
+
+ private static final int WIDTH = 160;
+ private static final int HEIGHT = 120;
+ private static final int Y_PIXEL_STRIDE = 1;
+ private static final int Y_ROW_STRIDE = WIDTH;
+ private static final int UV_PIXEL_STRIDE = 1;
+ private static final int UV_ROW_STRIDE = WIDTH / 2;
+
+ // The image used here has a YUV_420_888 format.
+ @Mock
+ private final ImageProxy mockYuvImage = mock(ImageProxy.class);
+ @Mock
+ private final ImageProxy.PlaneProxy yPlane = mock(ImageProxy.PlaneProxy.class);
+ @Mock
+ private final ImageProxy.PlaneProxy uPlane = mock(ImageProxy.PlaneProxy.class);
+ @Mock
+ private final ImageProxy.PlaneProxy vPlane = mock(ImageProxy.PlaneProxy.class);
+ private final ByteBuffer yBuffer = ByteBuffer.allocateDirect(WIDTH * HEIGHT);
+ private final ByteBuffer uBuffer = ByteBuffer.allocateDirect(WIDTH * HEIGHT / 4);
+ private final ByteBuffer vBuffer = ByteBuffer.allocateDirect(WIDTH * HEIGHT / 4);
+
+ @Mock
+ private final ImageProxy mockJpegImage = mock(ImageProxy.class);
+ @Mock
+ private final ImageProxy.PlaneProxy jpegDataPlane = mock(ImageProxy.PlaneProxy.class);
+ private final String jpegImageDataBase64 =
+ "/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEB"
+ + "AQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQH/2wBDAQEBAQEBAQEBAQEBAQEBAQEBAQEB"
+ + "AQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQH/wAARCAB4AKADASIA"
+ + "AhEBAxEB/8QAHwAAAQUBAQEBAQEAAAAAAAAAAAECAwQFBgcICQoL/8QAtRAAAgEDAwIEAwUFBAQA"
+ + "AAF9AQIDAAQRBRIhMUEGE1FhByJxFDKBkaEII0KxwRVS0fAkM2JyggkKFhcYGRolJicoKSo0NTY3"
+ + "ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqDhIWGh4iJipKTlJWWl5iZmqKjpKWm"
+ + "p6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uHi4+Tl5ufo6erx8vP09fb3+Pn6/8QAHwEA"
+ + "AwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoL/8QAtREAAgECBAQDBAcFBAQAAQJ3AAECAxEEBSEx"
+ + "BhJBUQdhcRMiMoEIFEKRobHBCSMzUvAVYnLRChYkNOEl8RcYGRomJygpKjU2Nzg5OkNERUZHSElK"
+ + "U1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6goOEhYaHiImKkpOUlZaXmJmaoqOkpaanqKmqsrO0tba3"
+ + "uLm6wsPExcbHyMnK0tPU1dbX2Nna4uPk5ebn6Onq8vP09fb3+Pn6/9oADAMBAAIRAxEAPwD/AD/6"
+ + "KKK/8/8AP/P/AAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiii"
+ + "gAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKA"
+ + "CiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAK"
+ + "KKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAoo"
+ + "ooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiii"
+ + "gAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooA//9k=";
+ private final ByteBuffer jpegDataBuffer =
+ ByteBuffer.wrap(Base64.decode(jpegImageDataBase64, Base64.DEFAULT));
+
+ private final Semaphore semaphore = new Semaphore(0);
+ private final ImageSaver.OnImageSavedListener mockListener =
+ mock(ImageSaver.OnImageSavedListener.class);
+ private final ImageSaver.OnImageSavedListener syncListener =
+ new OnImageSavedListener() {
+ @Override
+ public void onImageSaved(File file) {
+ mockListener.onImageSaved(file);
+ semaphore.release();
+ }
+
+ @Override
+ public void onError(
+ SaveError saveError, String message, @Nullable Throwable cause) {
+ mockListener.onError(saveError, message, cause);
+ semaphore.release();
+ }
+ };
+
+ private HandlerThread backgroundThread;
+ private Handler backgroundHandler;
+
+ @Before
+ public void setup() {
+ // The YUV image's behavior.
+ when(mockYuvImage.getFormat()).thenReturn(ImageFormat.YUV_420_888);
+ when(mockYuvImage.getWidth()).thenReturn(WIDTH);
+ when(mockYuvImage.getHeight()).thenReturn(HEIGHT);
+
+ when(yPlane.getBuffer()).thenReturn(yBuffer);
+ when(yPlane.getPixelStride()).thenReturn(Y_PIXEL_STRIDE);
+ when(yPlane.getRowStride()).thenReturn(Y_ROW_STRIDE);
+
+ when(uPlane.getBuffer()).thenReturn(uBuffer);
+ when(uPlane.getPixelStride()).thenReturn(UV_PIXEL_STRIDE);
+ when(uPlane.getRowStride()).thenReturn(UV_ROW_STRIDE);
+
+ when(vPlane.getBuffer()).thenReturn(vBuffer);
+ when(vPlane.getPixelStride()).thenReturn(UV_PIXEL_STRIDE);
+ when(vPlane.getRowStride()).thenReturn(UV_ROW_STRIDE);
+ when(mockYuvImage.getPlanes())
+ .thenReturn(new ImageProxy.PlaneProxy[]{yPlane, uPlane, vPlane});
+
+ // The JPEG image's behavior
+ when(mockJpegImage.getFormat()).thenReturn(ImageFormat.JPEG);
+ when(mockJpegImage.getWidth()).thenReturn(WIDTH);
+ when(mockJpegImage.getHeight()).thenReturn(HEIGHT);
+
+ when(jpegDataPlane.getBuffer()).thenReturn(jpegDataBuffer);
+ when(mockJpegImage.getPlanes()).thenReturn(new ImageProxy.PlaneProxy[]{jpegDataPlane});
+
+ // Set up a background thread/handler for callbacks
+ backgroundThread = new HandlerThread("CallbackThread");
+ backgroundThread.start();
+ backgroundHandler = new Handler(backgroundThread.getLooper());
+ }
+
+ @After
+ public void tearDown() {
+ backgroundThread.quitSafely();
+ }
+
+ private ImageSaver getDefaultImageSaver(ImageProxy image, File file) {
+ return new ImageSaver(
+ image,
+ file,
+ /*orientation=*/ 0,
+ /*reversedHorizontal=*/ false,
+ /*reversedVertical=*/ false,
+ /*location=*/ null,
+ /*cropAspectRatio=*/ null,
+ syncListener,
+ backgroundHandler);
+ }
+
+ @Test
+ public void canSaveYuvImage() throws InterruptedException, IOException {
+ File saveLocation = File.createTempFile("test", ".jpg");
+ saveLocation.deleteOnExit();
+
+ ImageSaver imageSaver = getDefaultImageSaver(mockYuvImage, saveLocation);
+
+ imageSaver.run();
+
+ semaphore.acquire();
+
+ verify(mockListener).onImageSaved(anyObject());
+ }
+
+ @Test
+ public void canSaveJpegImage() throws InterruptedException, IOException {
+ File saveLocation = File.createTempFile("test", ".jpg");
+ saveLocation.deleteOnExit();
+
+ ImageSaver imageSaver = getDefaultImageSaver(mockJpegImage, saveLocation);
+
+ imageSaver.run();
+
+ semaphore.acquire();
+
+ verify(mockListener).onImageSaved(anyObject());
+ }
+
+ @Test
+ public void errorCallbackWillBeCalledOnInvalidPath() throws InterruptedException, IOException {
+ // Invalid filename should cause error
+ File saveLocation = new File("/not/a/real/path.jpg");
+
+ ImageSaver imageSaver = getDefaultImageSaver(mockJpegImage, saveLocation);
+
+ imageSaver.run();
+
+ semaphore.acquire();
+
+ verify(mockListener).onError(eq(SaveError.FILE_IO_FAILED), anyString(), anyObject());
+ }
+
+ @Test
+ public void imageIsClosedOnSuccess() throws InterruptedException, IOException {
+ File saveLocation = File.createTempFile("test", ".jpg");
+ saveLocation.deleteOnExit();
+
+ ImageSaver imageSaver = getDefaultImageSaver(mockJpegImage, saveLocation);
+
+ imageSaver.run();
+
+ semaphore.acquire();
+
+ verify(mockJpegImage).close();
+ }
+
+ @Test
+ public void imageIsClosedOnError() throws InterruptedException, IOException {
+ // Invalid filename should cause error
+ File saveLocation = new File("/not/a/real/path.jpg");
+
+ ImageSaver imageSaver = getDefaultImageSaver(mockJpegImage, saveLocation);
+
+ imageSaver.run();
+
+ semaphore.acquire();
+
+ verify(mockJpegImage).close();
+ }
+
+ private void imageCanBeCropped(ImageProxy image) throws InterruptedException, IOException {
+ File saveLocation = File.createTempFile("test", ".jpg");
+ saveLocation.deleteOnExit();
+
+ Rational viewRatio = new Rational(1, 1);
+
+ ImageSaver imageSaver =
+ new ImageSaver(
+ image,
+ saveLocation,
+ /*orientation=*/ 0,
+ /*reversedHorizontal=*/ false,
+ /*reversedVertical=*/ false,
+ /*location=*/ null,
+ /*cropAspectRatio=*/ viewRatio,
+ syncListener,
+ backgroundHandler);
+ imageSaver.run();
+
+ semaphore.acquire();
+
+ Bitmap bitmap = BitmapFactory.decodeFile(saveLocation.getPath());
+ assertThat(bitmap.getWidth()).isEqualTo(bitmap.getHeight());
+ }
+
+ @Test
+ public void jpegImageCanBeCropped() throws InterruptedException, IOException {
+ imageCanBeCropped(mockJpegImage);
+ }
+
+ @Test
+ public void yuvImageCanBeCropped() throws InterruptedException, IOException {
+ imageCanBeCropped(mockYuvImage);
+ }
+}
diff --git a/camera/core/src/androidTest/java/androidx/camera/core/ImmediateSurfaceAndroidTest.java b/camera/core/src/androidTest/java/androidx/camera/core/ImmediateSurfaceAndroidTest.java
new file mode 100644
index 0000000..0ae4238
--- /dev/null
+++ b/camera/core/src/androidTest/java/androidx/camera/core/ImmediateSurfaceAndroidTest.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2019 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.core;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.view.Surface;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import com.google.common.util.concurrent.ListenableFuture;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mockito;
+
+import java.util.concurrent.ExecutionException;
+
+@RunWith(AndroidJUnit4.class)
+public final class ImmediateSurfaceAndroidTest {
+ Surface mockSurface = Mockito.mock(Surface.class);
+
+ @Test
+ public void getSurface_returnsInstance() throws ExecutionException, InterruptedException {
+ ImmediateSurface immediateSurface = new ImmediateSurface(mockSurface);
+
+ ListenableFuture<Surface> surfaceListenableFuture = immediateSurface.getSurface();
+
+ assertThat(surfaceListenableFuture.get()).isSameAs(mockSurface);
+ }
+}
diff --git a/camera/core/src/androidTest/java/androidx/camera/core/IoExecutorAndroidTest.java b/camera/core/src/androidTest/java/androidx/camera/core/IoExecutorAndroidTest.java
new file mode 100644
index 0000000..5ed3e6d
--- /dev/null
+++ b/camera/core/src/androidTest/java/androidx/camera/core/IoExecutorAndroidTest.java
@@ -0,0 +1,142 @@
+/*
+ * Copyright (C) 2019 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.core;
+
+import androidx.annotation.GuardedBy;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.concurrent.Executor;
+import java.util.concurrent.locks.Condition;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReentrantLock;
+
+@RunWith(AndroidJUnit4.class)
+public final class IoExecutorAndroidTest {
+
+ private Executor ioExecutor;
+ private Lock lock = new ReentrantLock();
+ private Condition condition = lock.newCondition();
+ @GuardedBy("lock")
+ private RunnableState state = RunnableState.CLEAR;
+ private final Runnable runnable1 =
+ () -> {
+ lock.lock();
+ try {
+ state = RunnableState.RUNNABLE1_WAITING;
+ condition.signalAll();
+ while (state != RunnableState.CLEAR) {
+ condition.await();
+ }
+
+ state = RunnableState.RUNNABLE1_FINISHED;
+ condition.signalAll();
+ } catch (InterruptedException e) {
+ throw new RuntimeException("Thread interrupted unexpectedly", e);
+ } finally {
+ lock.unlock();
+ }
+ };
+ private final Runnable runnable2 =
+ () -> {
+ lock.lock();
+ try {
+ while (state != RunnableState.RUNNABLE1_WAITING) {
+ condition.await();
+ }
+
+ state = RunnableState.RUNNABLE2_FINISHED;
+ condition.signalAll();
+ } catch (InterruptedException e) {
+ throw new RuntimeException("Thread interrupted unexpectedly", e);
+ } finally {
+ lock.unlock();
+ }
+ };
+ private final Runnable simpleRunnable1 =
+ () -> {
+ lock.lock();
+ try {
+ state = RunnableState.RUNNABLE1_FINISHED;
+ condition.signalAll();
+ } finally {
+ lock.unlock();
+ }
+ };
+
+ @Before
+ public void setup() {
+ lock.lock();
+ try {
+ state = RunnableState.CLEAR;
+ } finally {
+ lock.unlock();
+ }
+ ioExecutor = IoExecutor.getInstance();
+ }
+
+ @Test(timeout = 2000)
+ public void canRunRunnable() throws InterruptedException {
+ ioExecutor.execute(simpleRunnable1);
+ lock.lock();
+ try {
+ while (state != RunnableState.RUNNABLE1_FINISHED) {
+ condition.await();
+ }
+ } finally {
+ lock.unlock();
+ }
+
+ // No need to check anything here. Completing this method should signal success.
+ }
+
+ @Test(timeout = 2000)
+ public void canRunMultipleRunnableInParallel() throws InterruptedException {
+ ioExecutor.execute(runnable1);
+ ioExecutor.execute(runnable2);
+
+ lock.lock();
+ try {
+ // runnable2 cannot finish until runnable1 has started
+ while (state != RunnableState.RUNNABLE2_FINISHED) {
+ condition.await();
+ }
+
+ // Allow runnable1 to finish
+ state = RunnableState.CLEAR;
+ condition.signalAll();
+
+ while (state != RunnableState.RUNNABLE1_FINISHED) {
+ condition.await();
+ }
+ } finally {
+ lock.unlock();
+ }
+
+ // No need to check anything here. Completing this method should signal success.
+ }
+
+ private enum RunnableState {
+ CLEAR,
+ RUNNABLE1_WAITING,
+ RUNNABLE1_FINISHED,
+ RUNNABLE2_FINISHED
+ }
+}
diff --git a/camera/core/src/androidTest/java/androidx/camera/core/QueuedImageReaderProxyAndroidTest.java b/camera/core/src/androidTest/java/androidx/camera/core/QueuedImageReaderProxyAndroidTest.java
new file mode 100644
index 0000000..8500148
--- /dev/null
+++ b/camera/core/src/androidTest/java/androidx/camera/core/QueuedImageReaderProxyAndroidTest.java
@@ -0,0 +1,314 @@
+/*
+ * Copyright (C) 2019 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.core;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.timeout;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.graphics.ImageFormat;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.view.Surface;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.Semaphore;
+
+@RunWith(AndroidJUnit4.class)
+public final class QueuedImageReaderProxyAndroidTest {
+ private static final int IMAGE_WIDTH = 640;
+ private static final int IMAGE_HEIGHT = 480;
+ private static final int IMAGE_FORMAT = ImageFormat.YUV_420_888;
+ private static final int MAX_IMAGES = 10;
+
+ private final Surface surface = mock(Surface.class);
+ private HandlerThread handlerThread;
+ private Handler handler;
+ private QueuedImageReaderProxy imageReaderProxy;
+
+ private static ImageProxy createMockImageProxy() {
+ ImageProxy image = mock(ImageProxy.class);
+ when(image.getWidth()).thenReturn(IMAGE_WIDTH);
+ when(image.getHeight()).thenReturn(IMAGE_HEIGHT);
+ when(image.getFormat()).thenReturn(IMAGE_FORMAT);
+ return image;
+ }
+
+ private static ConcreteImageProxy createSemaphoreReleasingOnCloseImageProxy(
+ Semaphore semaphore) {
+ ConcreteImageProxy image = createForwardingImageProxy();
+ image.addOnImageCloseListener(
+ closedImage -> {
+ semaphore.release();
+ });
+ return image;
+ }
+
+ private static ConcreteImageProxy createForwardingImageProxy() {
+ return new ConcreteImageProxy(createMockImageProxy());
+ }
+
+ @Before
+ public void setUp() {
+ handlerThread = new HandlerThread("background");
+ handlerThread.start();
+ handler = new Handler(handlerThread.getLooper());
+ imageReaderProxy =
+ new QueuedImageReaderProxy(
+ IMAGE_WIDTH, IMAGE_HEIGHT, IMAGE_FORMAT, MAX_IMAGES, surface);
+ }
+
+ @After
+ public void tearDown() {
+ handlerThread.quitSafely();
+ }
+
+ @Test
+ public void enqueueImage_incrementsQueueSize() {
+ imageReaderProxy.enqueueImage(createForwardingImageProxy());
+ imageReaderProxy.enqueueImage(createForwardingImageProxy());
+
+ assertThat(imageReaderProxy.getCurrentImages()).isEqualTo(2);
+ }
+
+ @Test
+ public void enqueueImage_doesNotIncreaseSizeBeyondMaxImages() {
+ // Exceed the queue's capacity by 2.
+ for (int i = 0; i < MAX_IMAGES + 2; ++i) {
+ imageReaderProxy.enqueueImage(createForwardingImageProxy());
+ }
+
+ assertThat(imageReaderProxy.getCurrentImages()).isEqualTo(MAX_IMAGES);
+ }
+
+ @Test
+ public void enqueueImage_closesImagesWhichAreNotEnqueued_doesNotCloseOtherImages() {
+ // Exceed the queue's capacity by 2.
+ List<ConcreteImageProxy> images = new ArrayList<>(MAX_IMAGES + 2);
+ for (int i = 0; i < MAX_IMAGES + 2; ++i) {
+ images.add(createForwardingImageProxy());
+ imageReaderProxy.enqueueImage(images.get(i));
+ }
+
+ // Last two images should not be enqueued and should be closed.
+ assertThat(images.get(MAX_IMAGES).isClosed()).isTrue();
+ assertThat(images.get(MAX_IMAGES + 1).isClosed()).isTrue();
+ // All other images should be enqueued and open.
+ for (int i = 0; i < MAX_IMAGES; ++i) {
+ assertThat(images.get(i).isClosed()).isFalse();
+ }
+ }
+
+ @Test(timeout = 2000)
+ public void closedImages_reduceQueueSize() throws InterruptedException {
+ // Fill up to the queue's capacity.
+ Semaphore onCloseSemaphore = new Semaphore(/*permits=*/ 0);
+ for (int i = 0; i < MAX_IMAGES; ++i) {
+ ForwardingImageProxy image =
+ createSemaphoreReleasingOnCloseImageProxy(onCloseSemaphore);
+ imageReaderProxy.enqueueImage(image);
+ }
+
+ imageReaderProxy.acquireNextImage().close();
+ imageReaderProxy.acquireNextImage().close();
+ onCloseSemaphore.acquire(/*permits=*/ 2);
+
+ assertThat(imageReaderProxy.getCurrentImages()).isEqualTo(MAX_IMAGES - 2);
+ }
+
+ @Test(timeout = 2000)
+ public void closedImage_allowsNewImageToBeEnqueued() throws InterruptedException {
+ // Fill up to the queue's capacity.
+ Semaphore onCloseSemaphore = new Semaphore(/*permits=*/ 0);
+ for (int i = 0; i < MAX_IMAGES; ++i) {
+ ForwardingImageProxy image =
+ createSemaphoreReleasingOnCloseImageProxy(onCloseSemaphore);
+ imageReaderProxy.enqueueImage(image);
+ }
+
+ imageReaderProxy.acquireNextImage().close();
+ onCloseSemaphore.acquire();
+
+ ConcreteImageProxy lastImageProxy = createForwardingImageProxy();
+ imageReaderProxy.enqueueImage(lastImageProxy);
+
+ // Last image should be enqueued and open.
+ assertThat(lastImageProxy.isClosed()).isFalse();
+ }
+
+ @Test
+ public void enqueueImage_invokesListenerCallback() {
+ ImageReaderProxy.OnImageAvailableListener listener =
+ mock(ImageReaderProxy.OnImageAvailableListener.class);
+ imageReaderProxy.setOnImageAvailableListener(listener, handler);
+
+ imageReaderProxy.enqueueImage(createForwardingImageProxy());
+ imageReaderProxy.enqueueImage(createForwardingImageProxy());
+
+ verify(listener, timeout(2000).times(2)).onImageAvailable(imageReaderProxy);
+ }
+
+ @Test
+ public void acquireLatestImage_returnsNull_whenQueueIsEmpty() {
+ assertThat(imageReaderProxy.acquireLatestImage()).isNull();
+ }
+
+ @Test
+ public void acquireLatestImage_returnsLastImage_reducesQueueSizeToOne() {
+ final int availableImages = 5;
+ List<ForwardingImageProxy> images = new ArrayList<>(availableImages);
+ for (int i = 0; i < availableImages; ++i) {
+ images.add(createForwardingImageProxy());
+ imageReaderProxy.enqueueImage(images.get(i));
+ }
+
+ ImageProxy lastImage = images.get(availableImages - 1);
+ assertThat(imageReaderProxy.acquireLatestImage()).isEqualTo(lastImage);
+ assertThat(imageReaderProxy.getCurrentImages()).isEqualTo(1);
+ }
+
+ @Test
+ public void acquireLatestImage_throwsException_whenAllImagesWerePreviouslyAcquired() {
+ imageReaderProxy.enqueueImage(createForwardingImageProxy());
+ imageReaderProxy.acquireNextImage();
+
+ assertThrows(IllegalStateException.class, () -> imageReaderProxy.acquireLatestImage());
+ }
+
+ @Test
+ public void acquireNextImage_returnsNull_whenQueueIsEmpty() {
+ assertThat(imageReaderProxy.acquireNextImage()).isNull();
+ }
+
+ @Test
+ public void acquireNextImage_returnsNextImage_doesNotChangeQueueSize() {
+ final int availableImages = 5;
+ List<ForwardingImageProxy> images = new ArrayList<>(availableImages);
+ for (int i = 0; i < availableImages; ++i) {
+ images.add(createForwardingImageProxy());
+ imageReaderProxy.enqueueImage(images.get(i));
+ }
+
+ for (int i = 0; i < availableImages; ++i) {
+ assertThat(imageReaderProxy.acquireNextImage()).isEqualTo(images.get(i));
+ }
+ assertThat(imageReaderProxy.getCurrentImages()).isEqualTo(availableImages);
+ }
+
+ @Test
+ public void acquireNextImage_throwsException_whenAllImagesWerePreviouslyAcquired() {
+ imageReaderProxy.enqueueImage(createForwardingImageProxy());
+ imageReaderProxy.acquireNextImage();
+
+ assertThrows(IllegalStateException.class, () -> imageReaderProxy.acquireNextImage());
+ }
+
+ @Test
+ public void close_closesAnyImagesStillInQueue() {
+ ConcreteImageProxy image0 = createForwardingImageProxy();
+ ConcreteImageProxy image1 = createForwardingImageProxy();
+ imageReaderProxy.enqueueImage(image0);
+ imageReaderProxy.enqueueImage(image1);
+
+ imageReaderProxy.close();
+
+ assertThat(image0.isClosed()).isTrue();
+ assertThat(image1.isClosed()).isTrue();
+ }
+
+ @Test
+ public void close_notifiesOnCloseListeners() {
+ QueuedImageReaderProxy.OnReaderCloseListener listenerA =
+ mock(QueuedImageReaderProxy.OnReaderCloseListener.class);
+ QueuedImageReaderProxy.OnReaderCloseListener listenerB =
+ mock(QueuedImageReaderProxy.OnReaderCloseListener.class);
+ imageReaderProxy.addOnReaderCloseListener(listenerA);
+ imageReaderProxy.addOnReaderCloseListener(listenerB);
+
+ imageReaderProxy.close();
+
+ verify(listenerA, times(1)).onReaderClose(imageReaderProxy);
+ verify(listenerB, times(1)).onReaderClose(imageReaderProxy);
+ }
+
+ @Test
+ public void acquireLatestImage_throwsException_afterReaderIsClosed() {
+ imageReaderProxy.enqueueImage(createForwardingImageProxy());
+ imageReaderProxy.close();
+
+ assertThrows(IllegalStateException.class, () -> imageReaderProxy.acquireLatestImage());
+ }
+
+ @Test
+ public void acquireNextImage_throwsException_afterReaderIsClosed() {
+ imageReaderProxy.enqueueImage(createForwardingImageProxy());
+ imageReaderProxy.close();
+
+ assertThrows(IllegalStateException.class, () -> imageReaderProxy.acquireNextImage());
+ }
+
+ @Test
+ public void getHeight_returnsFixedHeight() {
+ assertThat(imageReaderProxy.getHeight()).isEqualTo(IMAGE_HEIGHT);
+ }
+
+ @Test
+ public void getWidth_returnsFixedWidth() {
+ assertThat(imageReaderProxy.getWidth()).isEqualTo(IMAGE_WIDTH);
+ }
+
+ @Test
+ public void getImageFormat_returnsFixedFormat() {
+ assertThat(imageReaderProxy.getImageFormat()).isEqualTo(IMAGE_FORMAT);
+ }
+
+ @Test
+ public void getMaxImages_returnsFixedCapacity() {
+ assertThat(imageReaderProxy.getMaxImages()).isEqualTo(MAX_IMAGES);
+ }
+
+ private static final class ConcreteImageProxy extends ForwardingImageProxy {
+ private boolean isClosed = false;
+
+ ConcreteImageProxy(ImageProxy image) {
+ super(image);
+ }
+
+ @Override
+ public synchronized void close() {
+ super.close();
+ isClosed = true;
+ }
+
+ public synchronized boolean isClosed() {
+ return isClosed;
+ }
+ }
+}
diff --git a/camera/core/src/androidTest/java/androidx/camera/core/ReferenceCountedImageProxyAndroidTest.java b/camera/core/src/androidTest/java/androidx/camera/core/ReferenceCountedImageProxyAndroidTest.java
new file mode 100644
index 0000000..74ae7b6
--- /dev/null
+++ b/camera/core/src/androidTest/java/androidx/camera/core/ReferenceCountedImageProxyAndroidTest.java
@@ -0,0 +1,159 @@
+/*
+ * Copyright (C) 2019 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.core;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.nio.ByteBuffer;
+
+@RunWith(AndroidJUnit4.class)
+public class ReferenceCountedImageProxyAndroidTest {
+ private static final int WIDTH = 640;
+ private static final int HEIGHT = 480;
+
+ // Assume the image has YUV_420_888 format.
+ private final ImageProxy image = mock(ImageProxy.class);
+ private final ImageProxy.PlaneProxy yPlane = mock(ImageProxy.PlaneProxy.class);
+ private final ImageProxy.PlaneProxy uPlane = mock(ImageProxy.PlaneProxy.class);
+ private final ImageProxy.PlaneProxy vPlane = mock(ImageProxy.PlaneProxy.class);
+ private final ByteBuffer yBuffer = ByteBuffer.allocateDirect(WIDTH * HEIGHT);
+ private final ByteBuffer uBuffer = ByteBuffer.allocateDirect(WIDTH * HEIGHT / 4);
+ private final ByteBuffer vBuffer = ByteBuffer.allocateDirect(WIDTH * HEIGHT / 4);
+ private ReferenceCountedImageProxy imageProxy;
+
+ @Before
+ public void setUp() {
+ when(image.getWidth()).thenReturn(WIDTH);
+ when(image.getHeight()).thenReturn(HEIGHT);
+ when(yPlane.getBuffer()).thenReturn(yBuffer);
+ when(uPlane.getBuffer()).thenReturn(uBuffer);
+ when(vPlane.getBuffer()).thenReturn(vBuffer);
+ when(image.getPlanes()).thenReturn(new ImageProxy.PlaneProxy[]{yPlane, uPlane, vPlane});
+ imageProxy = new ReferenceCountedImageProxy(image);
+ }
+
+ @Test
+ public void getReferenceCount_returnsOne_afterConstruction() {
+ assertThat(imageProxy.getReferenceCount()).isEqualTo(1);
+ }
+
+ @Test
+ public void fork_incrementsReferenceCount() {
+ imageProxy.fork();
+ imageProxy.fork();
+
+ assertThat(imageProxy.getReferenceCount()).isEqualTo(3);
+ }
+
+ @Test
+ public void close_decrementsReferenceCount() {
+ ImageProxy forkedImage0 = imageProxy.fork();
+ ImageProxy forkedImage1 = imageProxy.fork();
+
+ forkedImage0.close();
+ forkedImage1.close();
+
+ assertThat(imageProxy.getReferenceCount()).isEqualTo(1);
+ verify(image, never()).close();
+ }
+
+ @Test
+ public void close_closesBaseImage_whenReferenceCountHitsZero() {
+ ImageProxy forkedImage0 = imageProxy.fork();
+ ImageProxy forkedImage1 = imageProxy.fork();
+
+ forkedImage0.close();
+ forkedImage1.close();
+ imageProxy.close();
+
+ assertThat(imageProxy.getReferenceCount()).isEqualTo(0);
+ verify(image, times(1)).close();
+ }
+
+ @Test
+ public void close_decrementsReferenceCountOnlyOnce() {
+ ImageProxy forkedImage = imageProxy.fork();
+
+ forkedImage.close();
+ forkedImage.close();
+
+ assertThat(imageProxy.getReferenceCount()).isEqualTo(1);
+ }
+
+ @Test
+ public void fork_returnsNull_whenBaseImageIsClosed() {
+ imageProxy.close();
+
+ ImageProxy forkedImage = imageProxy.fork();
+
+ assertThat(forkedImage).isNull();
+ }
+
+ @Test
+ public void concurrentAccessForTwoForkedImagesOnTwoThreads() throws InterruptedException {
+ final ImageProxy forkedImage0 = imageProxy.fork();
+ final ImageProxy forkedImage1 = imageProxy.fork();
+
+ Thread thread0 =
+ new Thread() {
+ @Override
+ public void run() {
+ forkedImage0.getWidth();
+ forkedImage0.getHeight();
+ ImageProxy.PlaneProxy[] planes = forkedImage0.getPlanes();
+ for (ImageProxy.PlaneProxy plane : planes) {
+ ByteBuffer buffer = plane.getBuffer();
+ for (int i = 0; i < buffer.capacity(); ++i) {
+ buffer.get(i);
+ }
+ }
+ }
+ };
+ Thread thread1 =
+ new Thread() {
+ @Override
+ public void run() {
+ forkedImage1.getWidth();
+ forkedImage1.getHeight();
+ ImageProxy.PlaneProxy[] planes = forkedImage1.getPlanes();
+ for (ImageProxy.PlaneProxy plane : planes) {
+ ByteBuffer buffer = plane.getBuffer();
+ for (int i = 0; i < buffer.capacity(); ++i) {
+ buffer.get(i);
+ }
+ }
+ }
+ };
+
+ thread0.start();
+ thread1.start();
+ thread0.join();
+ thread1.join();
+ }
+}
diff --git a/camera/core/src/androidTest/java/androidx/camera/core/SessionConfigurationAndroidTest.java b/camera/core/src/androidTest/java/androidx/camera/core/SessionConfigurationAndroidTest.java
new file mode 100644
index 0000000..c6eca95
--- /dev/null
+++ b/camera/core/src/androidTest/java/androidx/camera/core/SessionConfigurationAndroidTest.java
@@ -0,0 +1,295 @@
+/*
+ * Copyright (C) 2019 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.core;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.hardware.camera2.CameraDevice;
+import android.hardware.camera2.CaptureRequest;
+import android.hardware.camera2.CaptureRequest.Key;
+import android.view.Surface;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mockito;
+
+import java.util.List;
+import java.util.Map;
+
+@RunWith(AndroidJUnit4.class)
+public class SessionConfigurationAndroidTest {
+ private DeferrableSurface mockSurface0;
+ private DeferrableSurface mockSurface1;
+
+ @Before
+ public void setup() {
+ mockSurface0 = new ImmediateSurface(Mockito.mock(Surface.class));
+ mockSurface1 = new ImmediateSurface(Mockito.mock(Surface.class));
+ }
+
+ @Test
+ public void builderSetTemplate() {
+ SessionConfiguration.Builder builder = new SessionConfiguration.Builder();
+
+ builder.setTemplateType(CameraDevice.TEMPLATE_PREVIEW);
+ SessionConfiguration sessionConfiguration = builder.build();
+
+ assertThat(sessionConfiguration.getTemplateType()).isEqualTo(CameraDevice.TEMPLATE_PREVIEW);
+ }
+
+ @Test
+ public void builderAddSurface() {
+ SessionConfiguration.Builder builder = new SessionConfiguration.Builder();
+
+ builder.addSurface(mockSurface0);
+ SessionConfiguration sessionConfiguration = builder.build();
+
+ List<DeferrableSurface> surfaces = sessionConfiguration.getSurfaces();
+
+ assertThat(surfaces).hasSize(1);
+ assertThat(surfaces).contains(mockSurface0);
+ }
+
+ @Test
+ public void builderAddNonRepeatingSurface() {
+ SessionConfiguration.Builder builder = new SessionConfiguration.Builder();
+
+ builder.addNonRepeatingSurface(mockSurface0);
+ SessionConfiguration sessionConfiguration = builder.build();
+
+ List<DeferrableSurface> surfaces = sessionConfiguration.getSurfaces();
+ List<DeferrableSurface> repeatingSurfaces =
+ sessionConfiguration.getCaptureRequestConfiguration().getSurfaces();
+
+ assertThat(surfaces).hasSize(1);
+ assertThat(surfaces).contains(mockSurface0);
+ assertThat(repeatingSurfaces).isEmpty();
+ assertThat(repeatingSurfaces).doesNotContain(mockSurface0);
+ }
+
+ @Test
+ public void builderAddSurfaceContainsRepeatingSurface() {
+ SessionConfiguration.Builder builder = new SessionConfiguration.Builder();
+
+ builder.addSurface(mockSurface0);
+ builder.addNonRepeatingSurface(mockSurface1);
+ SessionConfiguration sessionConfiguration = builder.build();
+
+ List<Surface> surfaces = DeferrableSurfaces.surfaceList(sessionConfiguration.getSurfaces());
+ List<Surface> repeatingSurfaces =
+ DeferrableSurfaces.surfaceList(
+ sessionConfiguration.getCaptureRequestConfiguration().getSurfaces());
+
+ assertThat(surfaces.size()).isAtLeast(repeatingSurfaces.size());
+ assertThat(surfaces).containsAllIn(repeatingSurfaces);
+ }
+
+ @Test
+ public void builderRemoveSurface() {
+ SessionConfiguration.Builder builder = new SessionConfiguration.Builder();
+
+ builder.addSurface(mockSurface0);
+ builder.removeSurface(mockSurface0);
+ SessionConfiguration sessionConfiguration = builder.build();
+
+ List<Surface> surfaces = DeferrableSurfaces.surfaceList(sessionConfiguration.getSurfaces());
+ assertThat(surfaces).isEmpty();
+ }
+
+ @Test
+ public void builderClearSurface() {
+ SessionConfiguration.Builder builder = new SessionConfiguration.Builder();
+
+ builder.addSurface(mockSurface0);
+ builder.clearSurfaces();
+ SessionConfiguration sessionConfiguration = builder.build();
+
+ List<Surface> surfaces = DeferrableSurfaces.surfaceList(sessionConfiguration.getSurfaces());
+ assertThat(surfaces.size()).isEqualTo(0);
+ }
+
+ @Test
+ public void builderAddCharacteristic() {
+ SessionConfiguration.Builder builder = new SessionConfiguration.Builder();
+
+ builder.addCharacteristic(
+ CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_AUTO);
+ SessionConfiguration sessionConfiguration = builder.build();
+
+ Map<Key<?>, CaptureRequestParameter<?>> parameterMap =
+ sessionConfiguration.getCameraCharacteristics();
+
+ assertThat(parameterMap.containsKey(CaptureRequest.CONTROL_AF_MODE)).isTrue();
+ assertThat(parameterMap)
+ .containsEntry(
+ CaptureRequest.CONTROL_AF_MODE,
+ CaptureRequestParameter.create(
+ CaptureRequest.CONTROL_AF_MODE,
+ CaptureRequest.CONTROL_AF_MODE_AUTO));
+ }
+
+ @Test
+ public void conflictingTemplate() {
+ SessionConfiguration.Builder builderPreview = new SessionConfiguration.Builder();
+ builderPreview.setTemplateType(CameraDevice.TEMPLATE_PREVIEW);
+ SessionConfiguration sessionConfigurationPreview = builderPreview.build();
+ SessionConfiguration.Builder builderZsl = new SessionConfiguration.Builder();
+ builderZsl.setTemplateType(CameraDevice.TEMPLATE_ZERO_SHUTTER_LAG);
+ SessionConfiguration sessionConfigurationZsl = builderZsl.build();
+
+ SessionConfiguration.ValidatingBuilder validatingBuilder =
+ new SessionConfiguration.ValidatingBuilder();
+
+ validatingBuilder.add(sessionConfigurationPreview);
+ validatingBuilder.add(sessionConfigurationZsl);
+
+ assertThat(validatingBuilder.isValid()).isFalse();
+ }
+
+ @Test
+ public void conflictingCharacteristics() {
+ SessionConfiguration.Builder builderAfAuto = new SessionConfiguration.Builder();
+ builderAfAuto.addCharacteristic(
+ CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_AUTO);
+ SessionConfiguration sessionConfigurationAfAuto = builderAfAuto.build();
+ SessionConfiguration.Builder builderAfOff = new SessionConfiguration.Builder();
+ builderAfOff.addCharacteristic(
+ CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_OFF);
+ SessionConfiguration sessionConfigurationAfOff = builderAfOff.build();
+
+ SessionConfiguration.ValidatingBuilder validatingBuilder =
+ new SessionConfiguration.ValidatingBuilder();
+
+ validatingBuilder.add(sessionConfigurationAfAuto);
+ validatingBuilder.add(sessionConfigurationAfOff);
+
+ assertThat(validatingBuilder.isValid()).isFalse();
+ }
+
+ @Test
+ public void combineTwoSessionsValid() {
+ SessionConfiguration.Builder builder0 = new SessionConfiguration.Builder();
+ builder0.addSurface(mockSurface0);
+ builder0.setTemplateType(CameraDevice.TEMPLATE_PREVIEW);
+ builder0.addCharacteristic(
+ CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_AUTO);
+
+ SessionConfiguration.Builder builder1 = new SessionConfiguration.Builder();
+ builder1.addSurface(mockSurface1);
+ builder1.setTemplateType(CameraDevice.TEMPLATE_PREVIEW);
+ builder1.addCharacteristic(
+ CaptureRequest.CONTROL_AE_ANTIBANDING_MODE,
+ CaptureRequest.CONTROL_AE_ANTIBANDING_MODE_AUTO);
+
+ SessionConfiguration.ValidatingBuilder validatingBuilder =
+ new SessionConfiguration.ValidatingBuilder();
+ validatingBuilder.add(builder0.build());
+ validatingBuilder.add(builder1.build());
+
+ assertThat(validatingBuilder.isValid()).isTrue();
+ }
+
+ @Test
+ public void combineTwoSessionsTemplate() {
+ SessionConfiguration.Builder builder0 = new SessionConfiguration.Builder();
+ builder0.addSurface(mockSurface0);
+ builder0.setTemplateType(CameraDevice.TEMPLATE_PREVIEW);
+ builder0.addCharacteristic(
+ CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_AUTO);
+
+ SessionConfiguration.Builder builder1 = new SessionConfiguration.Builder();
+ builder1.addSurface(mockSurface1);
+ builder1.setTemplateType(CameraDevice.TEMPLATE_PREVIEW);
+ builder1.addCharacteristic(
+ CaptureRequest.CONTROL_AE_ANTIBANDING_MODE,
+ CaptureRequest.CONTROL_AE_ANTIBANDING_MODE_AUTO);
+
+ SessionConfiguration.ValidatingBuilder validatingBuilder =
+ new SessionConfiguration.ValidatingBuilder();
+ validatingBuilder.add(builder0.build());
+ validatingBuilder.add(builder1.build());
+
+ SessionConfiguration sessionConfiguration = validatingBuilder.build();
+
+ assertThat(sessionConfiguration.getTemplateType()).isEqualTo(CameraDevice.TEMPLATE_PREVIEW);
+ }
+
+ @Test
+ public void combineTwoSessionsSurfaces() {
+ SessionConfiguration.Builder builder0 = new SessionConfiguration.Builder();
+ builder0.addSurface(mockSurface0);
+ builder0.setTemplateType(CameraDevice.TEMPLATE_PREVIEW);
+ builder0.addCharacteristic(
+ CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_AUTO);
+
+ SessionConfiguration.Builder builder1 = new SessionConfiguration.Builder();
+ builder1.addSurface(mockSurface1);
+ builder1.setTemplateType(CameraDevice.TEMPLATE_PREVIEW);
+ builder1.addCharacteristic(
+ CaptureRequest.CONTROL_AE_ANTIBANDING_MODE,
+ CaptureRequest.CONTROL_AE_ANTIBANDING_MODE_AUTO);
+
+ SessionConfiguration.ValidatingBuilder validatingBuilder =
+ new SessionConfiguration.ValidatingBuilder();
+ validatingBuilder.add(builder0.build());
+ validatingBuilder.add(builder1.build());
+
+ SessionConfiguration sessionConfiguration = validatingBuilder.build();
+
+ List<DeferrableSurface> surfaces = sessionConfiguration.getSurfaces();
+ assertThat(surfaces).containsExactly(mockSurface0, mockSurface1);
+ }
+
+ @Test
+ public void combineTwoSessionsCharacteristics() {
+ SessionConfiguration.Builder builder0 = new SessionConfiguration.Builder();
+ builder0.addSurface(mockSurface0);
+ builder0.setTemplateType(CameraDevice.TEMPLATE_PREVIEW);
+ builder0.addCharacteristic(
+ CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_AUTO);
+
+ SessionConfiguration.Builder builder1 = new SessionConfiguration.Builder();
+ builder1.addSurface(mockSurface1);
+ builder1.setTemplateType(CameraDevice.TEMPLATE_PREVIEW);
+ builder1.addCharacteristic(
+ CaptureRequest.CONTROL_AE_ANTIBANDING_MODE,
+ CaptureRequest.CONTROL_AE_ANTIBANDING_MODE_AUTO);
+
+ SessionConfiguration.ValidatingBuilder validatingBuilder =
+ new SessionConfiguration.ValidatingBuilder();
+ validatingBuilder.add(builder0.build());
+ validatingBuilder.add(builder1.build());
+
+ SessionConfiguration sessionConfiguration = validatingBuilder.build();
+
+ Map<Key<?>, CaptureRequestParameter<?>> parameterMap =
+ sessionConfiguration.getCameraCharacteristics();
+ assertThat(parameterMap)
+ .containsExactly(
+ CaptureRequest.CONTROL_AF_MODE,
+ CaptureRequestParameter.create(
+ CaptureRequest.CONTROL_AF_MODE,
+ CaptureRequest.CONTROL_AF_MODE_AUTO),
+ CaptureRequest.CONTROL_AE_ANTIBANDING_MODE,
+ CaptureRequestParameter.create(
+ CaptureRequest.CONTROL_AE_ANTIBANDING_MODE,
+ CaptureRequest.CONTROL_AE_ANTIBANDING_MODE_AUTO));
+ }
+}
diff --git a/camera/core/src/androidTest/java/androidx/camera/core/SingleCloseImageProxyAndroidTest.java b/camera/core/src/androidTest/java/androidx/camera/core/SingleCloseImageProxyAndroidTest.java
new file mode 100644
index 0000000..09f3175
--- /dev/null
+++ b/camera/core/src/androidTest/java/androidx/camera/core/SingleCloseImageProxyAndroidTest.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 2019 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.core;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+public final class SingleCloseImageProxyAndroidTest {
+
+ private final ImageProxy imageProxy = mock(ImageProxy.class);
+ private SingleCloseImageProxy singleCloseImageProxy;
+
+ @Before
+ public void setUp() {
+ singleCloseImageProxy = new SingleCloseImageProxy(imageProxy);
+ }
+
+ @Test
+ public void wrappedImageIsClosedOnce_whenWrappingImageIsClosedOnce() {
+ singleCloseImageProxy.close();
+
+ verify(imageProxy, times(1)).close();
+ }
+
+ @Test
+ public void wrappedImageIsClosedOnce_whenWrappingImageIsClosedTwice() {
+ singleCloseImageProxy.close();
+ singleCloseImageProxy.close();
+
+ verify(imageProxy, times(1)).close();
+ }
+}
diff --git a/camera/core/src/androidTest/java/androidx/camera/core/StreamConfigurationMapUtil.java b/camera/core/src/androidTest/java/androidx/camera/core/StreamConfigurationMapUtil.java
new file mode 100644
index 0000000..0bfd39f
--- /dev/null
+++ b/camera/core/src/androidTest/java/androidx/camera/core/StreamConfigurationMapUtil.java
@@ -0,0 +1,219 @@
+/*
+ * Copyright (C) 2019 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.core;
+
+import android.graphics.ImageFormat;
+import android.hardware.camera2.params.StreamConfigurationMap;
+import android.os.Build;
+import android.util.Size;
+
+import java.lang.reflect.Array;
+import java.lang.reflect.Constructor;
+import java.lang.reflect.InvocationTargetException;
+
+/** Utility functions to obtain fake {@link StreamConfigurationMap} for testing */
+public final class StreamConfigurationMapUtil {
+ /**
+ * Generates fake StreamConfigurationMap for testing usage.
+ *
+ * @return a fake {@link StreamConfigurationMap} object
+ */
+ public static StreamConfigurationMap generateFakeStreamConfigurationMap() {
+ /**
+ * Defined in StreamConfigurationMap.java: 0x21 is internal defined legal format
+ * corresponding to ImageFormat.JPEG. 0x22 is internal defined legal format
+ * IMPLEMENTATION_DEFINED and at least one stream configuration for
+ * IMPLEMENTATION_DEFINED(0x22) must exist, otherwise, there will be AssertionError threw.
+ * 0x22 is also mapped to ImageFormat.PRIVATE after Android level 23.
+ */
+ int[] supportedFormats =
+ new int[]{
+ ImageFormat.YUV_420_888,
+ ImageFormatConstants.INTERNAL_DEFINED_IMAGE_FORMAT_JPEG,
+ ImageFormatConstants.INTERNAL_DEFINED_IMAGE_FORMAT_PRIVATE
+ };
+ Size[] supportedSizes =
+ new Size[]{
+ new Size(4032, 3024),
+ new Size(3840, 2160),
+ new Size(1920, 1080),
+ new Size(640, 480),
+ new Size(320, 240),
+ new Size(320, 180)
+ };
+
+ return generateFakeStreamConfigurationMap(supportedFormats, supportedSizes);
+ }
+
+ /**
+ * Generates fake StreamConfigurationMap for testing usage.
+ *
+ * @param supportedFormats The supported {@link ImageFormat} list to be added
+ * @param supportedSizes The supported sizes to be added
+ * @return a fake {@link StreamConfigurationMap} object
+ */
+ public static StreamConfigurationMap generateFakeStreamConfigurationMap(
+ int[] supportedFormats, Size[] supportedSizes) {
+ StreamConfigurationMap map;
+
+ // TODO(b/123938482): Remove usage of reflection in this class
+ Class<?> streamConfigurationClass;
+ Class<?> streamConfigurationDurationClass;
+ Class<?> highSpeedVideoConfigurationClass;
+ Class<?> reprocessFormatsMapClass;
+
+ try {
+ streamConfigurationClass =
+ Class.forName("android.hardware.camera2.params.StreamConfiguration");
+ streamConfigurationDurationClass =
+ Class.forName("android.hardware.camera2.params.StreamConfigurationDuration");
+ highSpeedVideoConfigurationClass =
+ Class.forName("android.hardware.camera2.params.HighSpeedVideoConfiguration");
+ reprocessFormatsMapClass =
+ Class.forName("android.hardware.camera2.params.ReprocessFormatsMap");
+ } catch (ClassNotFoundException e) {
+ throw new AssertionError(
+ "Class can not be found when trying to generate a StreamConfigurationMap "
+ + "object.",
+ e);
+ }
+
+ Constructor<?> streamConfigurationMapConstructor;
+ Constructor<?> streamConfigurationConstructor;
+ Constructor<?> streamConfigurationDurationConstructor;
+
+ try {
+ if (Build.VERSION.SDK_INT >= 23) {
+ streamConfigurationMapConstructor =
+ StreamConfigurationMap.class.getDeclaredConstructor(
+ Array.newInstance(streamConfigurationClass, 1).getClass(),
+ Array.newInstance(streamConfigurationDurationClass, 1).getClass(),
+ Array.newInstance(streamConfigurationDurationClass, 1).getClass(),
+ Array.newInstance(streamConfigurationClass, 1).getClass(),
+ Array.newInstance(streamConfigurationDurationClass, 1).getClass(),
+ Array.newInstance(streamConfigurationDurationClass, 1).getClass(),
+ Array.newInstance(highSpeedVideoConfigurationClass, 1).getClass(),
+ reprocessFormatsMapClass,
+ boolean.class);
+ } else {
+ streamConfigurationMapConstructor =
+ StreamConfigurationMap.class.getDeclaredConstructor(
+ Array.newInstance(streamConfigurationClass, 1).getClass(),
+ Array.newInstance(streamConfigurationDurationClass, 1).getClass(),
+ Array.newInstance(streamConfigurationDurationClass, 1).getClass(),
+ Array.newInstance(highSpeedVideoConfigurationClass, 1).getClass());
+ }
+
+ streamConfigurationConstructor =
+ streamConfigurationClass.getDeclaredConstructor(
+ int.class, int.class, int.class, boolean.class);
+
+ streamConfigurationDurationConstructor =
+ streamConfigurationDurationClass.getDeclaredConstructor(
+ int.class, int.class, int.class, long.class);
+ } catch (NoSuchMethodException e) {
+ throw new AssertionError(
+ "Constructor can not be found when trying to generate a "
+ + "StreamConfigurationMap object.",
+ e);
+ }
+
+ Object configurationArray =
+ Array.newInstance(
+ streamConfigurationClass, supportedFormats.length * supportedSizes.length);
+ Object minFrameDurationArray = Array.newInstance(streamConfigurationDurationClass, 1);
+ Object stallDurationArray = Array.newInstance(streamConfigurationDurationClass, 1);
+ Object depthConfigurationArray = Array.newInstance(streamConfigurationClass, 1);
+ Object depthMinFrameDurationArray = Array.newInstance(streamConfigurationDurationClass, 1);
+ Object depthStallDurationArray = Array.newInstance(streamConfigurationDurationClass, 1);
+
+ try {
+ for (int i = 0; i < supportedFormats.length; i++) {
+ for (int j = 0; j < supportedSizes.length; j++) {
+ Array.set(
+ configurationArray,
+ i * supportedSizes.length + j,
+ streamConfigurationConstructor.newInstance(
+ supportedFormats[i],
+ supportedSizes[j].getWidth(),
+ supportedSizes[j].getHeight(),
+ false));
+ }
+ }
+
+ Array.set(
+ minFrameDurationArray,
+ 0,
+ streamConfigurationDurationConstructor.newInstance(
+ ImageFormat.YUV_420_888, 1920, 1080, 0));
+
+ Array.set(
+ stallDurationArray,
+ 0,
+ streamConfigurationDurationConstructor.newInstance(
+ ImageFormat.YUV_420_888, 1920, 1080, 0));
+
+ // Need depth configuration to create the object successfully
+ // 0x24 is internal format type of HAL_PIXEL_FORMAT_RAW_OPAQUE
+ Array.set(
+ depthConfigurationArray,
+ 0,
+ streamConfigurationConstructor.newInstance(0x24, 1920, 1080, false));
+
+ Array.set(
+ depthMinFrameDurationArray,
+ 0,
+ streamConfigurationDurationConstructor.newInstance(0x24, 1920, 1080, 0));
+
+ Array.set(
+ depthStallDurationArray,
+ 0,
+ streamConfigurationDurationConstructor.newInstance(0x24, 1920, 1080, 0));
+
+ if (Build.VERSION.SDK_INT >= 23) {
+ map =
+ (StreamConfigurationMap)
+ streamConfigurationMapConstructor.newInstance(
+ configurationArray,
+ minFrameDurationArray,
+ stallDurationArray,
+ depthConfigurationArray,
+ depthMinFrameDurationArray,
+ depthStallDurationArray,
+ null,
+ null,
+ false);
+ } else {
+ map =
+ (StreamConfigurationMap)
+ streamConfigurationMapConstructor.newInstance(
+ configurationArray,
+ minFrameDurationArray,
+ stallDurationArray,
+ null);
+ }
+
+ } catch (IllegalAccessException | InstantiationException | InvocationTargetException e) {
+ throw new AssertionError(
+ "Failed to create new instance when trying to generate a "
+ + "StreamConfigurationMap object.",
+ e);
+ }
+
+ return map;
+ }
+}
diff --git a/camera/core/src/androidTest/java/androidx/camera/core/UseCaseAttachStateAndroidTest.java b/camera/core/src/androidTest/java/androidx/camera/core/UseCaseAttachStateAndroidTest.java
new file mode 100644
index 0000000..a8d9ef0
--- /dev/null
+++ b/camera/core/src/androidTest/java/androidx/camera/core/UseCaseAttachStateAndroidTest.java
@@ -0,0 +1,344 @@
+/*
+ * Copyright (C) 2019 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.core;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import android.hardware.camera2.CameraCaptureSession;
+import android.hardware.camera2.CameraDevice;
+import android.util.Size;
+import android.view.Surface;
+
+import androidx.camera.core.CameraX.LensFacing;
+import androidx.camera.testing.fakes.FakeAppConfiguration;
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mockito;
+
+import java.util.HashMap;
+import java.util.Map;
+
+@RunWith(AndroidJUnit4.class)
+public class UseCaseAttachStateAndroidTest {
+ private final LensFacing cameraLensFacing0 = LensFacing.BACK;
+ private final LensFacing cameraLensFacing1 = LensFacing.FRONT;
+ private final CameraDevice mockCameraDevice = Mockito.mock(CameraDevice.class);
+ private final CameraCaptureSession mockCameraCaptureSession =
+ Mockito.mock(CameraCaptureSession.class);
+
+ private String cameraId;
+
+ @Before
+ public void setUp() {
+ AppConfiguration appConfiguration = FakeAppConfiguration.create();
+ CameraFactory cameraFactory = appConfiguration.getCameraFactory(/*valueIfMissing=*/ null);
+ try {
+ cameraId = cameraFactory.cameraIdForLensFacing(LensFacing.BACK);
+ } catch (Exception e) {
+ throw new IllegalArgumentException(
+ "Unable to attach to camera with LensFacing " + LensFacing.BACK, e);
+ }
+ CameraX.init(ApplicationProvider.getApplicationContext(), appConfiguration);
+ }
+
+ @Test
+ public void setSingleUseCaseOnline() {
+ UseCaseAttachState useCaseAttachState = new UseCaseAttachState(cameraId);
+ FakeUseCaseConfiguration configuration =
+ new FakeUseCaseConfiguration.Builder()
+ .setTargetName("UseCase")
+ .setLensFacing(cameraLensFacing0)
+ .build();
+ TestUseCase fakeUseCase = new TestUseCase(configuration, cameraId);
+
+ useCaseAttachState.setUseCaseOnline(fakeUseCase);
+
+ SessionConfiguration.ValidatingBuilder builder = useCaseAttachState.getOnlineBuilder();
+ SessionConfiguration sessionConfiguration = builder.build();
+ assertThat(DeferrableSurfaces.surfaceList(sessionConfiguration.getSurfaces()))
+ .containsExactly(fakeUseCase.surface);
+
+ sessionConfiguration.getDeviceStateCallback().onOpened(mockCameraDevice);
+ verify(fakeUseCase.deviceStateCallback, times(1)).onOpened(mockCameraDevice);
+
+ sessionConfiguration.getSessionStateCallback().onConfigured(mockCameraCaptureSession);
+ verify(fakeUseCase.sessionStateCallback, times(1)).onConfigured(mockCameraCaptureSession);
+
+ sessionConfiguration.getCameraCaptureCallback().onCaptureCompleted(null);
+ verify(fakeUseCase.cameraCaptureCallback, times(1)).onCaptureCompleted(null);
+ }
+
+ @Test
+ public void setTwoUseCasesOnline() {
+ UseCaseAttachState useCaseAttachState = new UseCaseAttachState(cameraId);
+ FakeUseCaseConfiguration configuration0 =
+ new FakeUseCaseConfiguration.Builder()
+ .setTargetName("UseCase")
+ .setLensFacing(cameraLensFacing0)
+ .build();
+ TestUseCase fakeUseCase0 = new TestUseCase(configuration0, cameraId);
+ FakeUseCaseConfiguration configuration1 =
+ new FakeUseCaseConfiguration.Builder()
+ .setTargetName("UseCase")
+ .setLensFacing(cameraLensFacing0)
+ .build();
+ TestUseCase fakeUseCase1 = new TestUseCase(configuration1, cameraId);
+
+ useCaseAttachState.setUseCaseOnline(fakeUseCase0);
+ useCaseAttachState.setUseCaseOnline(fakeUseCase1);
+
+ SessionConfiguration.ValidatingBuilder builder = useCaseAttachState.getOnlineBuilder();
+ SessionConfiguration sessionConfiguration = builder.build();
+ assertThat(DeferrableSurfaces.surfaceList(sessionConfiguration.getSurfaces()))
+ .containsExactly(fakeUseCase0.surface, fakeUseCase1.surface);
+
+ sessionConfiguration.getDeviceStateCallback().onOpened(mockCameraDevice);
+ verify(fakeUseCase0.deviceStateCallback, times(1)).onOpened(mockCameraDevice);
+ verify(fakeUseCase1.deviceStateCallback, times(1)).onOpened(mockCameraDevice);
+
+ sessionConfiguration.getSessionStateCallback().onConfigured(mockCameraCaptureSession);
+ verify(fakeUseCase0.sessionStateCallback, times(1)).onConfigured(mockCameraCaptureSession);
+ verify(fakeUseCase1.sessionStateCallback, times(1)).onConfigured(mockCameraCaptureSession);
+
+ sessionConfiguration.getCameraCaptureCallback().onCaptureCompleted(null);
+ verify(fakeUseCase0.cameraCaptureCallback, times(1)).onCaptureCompleted(null);
+ verify(fakeUseCase1.cameraCaptureCallback, times(1)).onCaptureCompleted(null);
+ }
+
+ @Test
+ public void setUseCaseActiveOnly() {
+ UseCaseAttachState useCaseAttachState = new UseCaseAttachState(cameraId);
+ FakeUseCaseConfiguration configuration =
+ new FakeUseCaseConfiguration.Builder()
+ .setTargetName("UseCase")
+ .setLensFacing(cameraLensFacing0)
+ .build();
+ TestUseCase fakeUseCase = new TestUseCase(configuration, cameraId);
+
+ useCaseAttachState.setUseCaseActive(fakeUseCase);
+
+ SessionConfiguration.ValidatingBuilder builder =
+ useCaseAttachState.getActiveAndOnlineBuilder();
+ SessionConfiguration sessionConfiguration = builder.build();
+ assertThat(sessionConfiguration.getSurfaces()).isEmpty();
+
+ sessionConfiguration.getDeviceStateCallback().onOpened(mockCameraDevice);
+ verify(fakeUseCase.deviceStateCallback, never()).onOpened(mockCameraDevice);
+
+ sessionConfiguration.getSessionStateCallback().onConfigured(mockCameraCaptureSession);
+ verify(fakeUseCase.sessionStateCallback, never()).onConfigured(mockCameraCaptureSession);
+
+ sessionConfiguration.getCameraCaptureCallback().onCaptureCompleted(null);
+ verify(fakeUseCase.cameraCaptureCallback, never()).onCaptureCompleted(null);
+ }
+
+ @Test
+ public void setUseCaseActiveAndOnline() {
+ UseCaseAttachState useCaseAttachState = new UseCaseAttachState(cameraId);
+ FakeUseCaseConfiguration configuration =
+ new FakeUseCaseConfiguration.Builder()
+ .setTargetName("UseCase")
+ .setLensFacing(cameraLensFacing0)
+ .build();
+ TestUseCase fakeUseCase = new TestUseCase(configuration, cameraId);
+
+ useCaseAttachState.setUseCaseOnline(fakeUseCase);
+ useCaseAttachState.setUseCaseActive(fakeUseCase);
+
+ SessionConfiguration.ValidatingBuilder builder =
+ useCaseAttachState.getActiveAndOnlineBuilder();
+ SessionConfiguration sessionConfiguration = builder.build();
+ assertThat(DeferrableSurfaces.surfaceList(sessionConfiguration.getSurfaces()))
+ .containsExactly(fakeUseCase.surface);
+
+ sessionConfiguration.getDeviceStateCallback().onOpened(mockCameraDevice);
+ verify(fakeUseCase.deviceStateCallback, times(1)).onOpened(mockCameraDevice);
+
+ sessionConfiguration.getSessionStateCallback().onConfigured(mockCameraCaptureSession);
+ verify(fakeUseCase.sessionStateCallback, times(1)).onConfigured(mockCameraCaptureSession);
+
+ sessionConfiguration.getCameraCaptureCallback().onCaptureCompleted(null);
+ verify(fakeUseCase.cameraCaptureCallback, times(1)).onCaptureCompleted(null);
+ }
+
+ @Test
+ public void setUseCaseOffline() {
+ UseCaseAttachState useCaseAttachState = new UseCaseAttachState(cameraId);
+ FakeUseCaseConfiguration configuration =
+ new FakeUseCaseConfiguration.Builder()
+ .setTargetName("UseCase")
+ .setLensFacing(cameraLensFacing0)
+ .build();
+ TestUseCase fakeUseCase = new TestUseCase(configuration, cameraId);
+
+ useCaseAttachState.setUseCaseOnline(fakeUseCase);
+ useCaseAttachState.setUseCaseOffline(fakeUseCase);
+
+ SessionConfiguration.ValidatingBuilder builder = useCaseAttachState.getOnlineBuilder();
+ SessionConfiguration sessionConfiguration = builder.build();
+ assertThat(sessionConfiguration.getSurfaces()).isEmpty();
+
+ sessionConfiguration.getDeviceStateCallback().onOpened(mockCameraDevice);
+ verify(fakeUseCase.deviceStateCallback, never()).onOpened(mockCameraDevice);
+
+ sessionConfiguration.getSessionStateCallback().onConfigured(mockCameraCaptureSession);
+ verify(fakeUseCase.sessionStateCallback, never()).onConfigured(mockCameraCaptureSession);
+
+ sessionConfiguration.getCameraCaptureCallback().onCaptureCompleted(null);
+ verify(fakeUseCase.cameraCaptureCallback, never()).onCaptureCompleted(null);
+ }
+
+ @Test
+ public void setUseCaseInactive() {
+ UseCaseAttachState useCaseAttachState = new UseCaseAttachState(cameraId);
+ FakeUseCaseConfiguration configuration =
+ new FakeUseCaseConfiguration.Builder()
+ .setTargetName("UseCase")
+ .setLensFacing(cameraLensFacing0)
+ .build();
+ TestUseCase fakeUseCase = new TestUseCase(configuration, cameraId);
+
+ useCaseAttachState.setUseCaseOnline(fakeUseCase);
+ useCaseAttachState.setUseCaseActive(fakeUseCase);
+ useCaseAttachState.setUseCaseInactive(fakeUseCase);
+
+ SessionConfiguration.ValidatingBuilder builder =
+ useCaseAttachState.getActiveAndOnlineBuilder();
+ SessionConfiguration sessionConfiguration = builder.build();
+ assertThat(sessionConfiguration.getSurfaces()).isEmpty();
+
+ sessionConfiguration.getDeviceStateCallback().onOpened(mockCameraDevice);
+ verify(fakeUseCase.deviceStateCallback, never()).onOpened(mockCameraDevice);
+
+ sessionConfiguration.getSessionStateCallback().onConfigured(mockCameraCaptureSession);
+ verify(fakeUseCase.sessionStateCallback, never()).onConfigured(mockCameraCaptureSession);
+
+ sessionConfiguration.getCameraCaptureCallback().onCaptureCompleted(null);
+ verify(fakeUseCase.cameraCaptureCallback, never()).onCaptureCompleted(null);
+ }
+
+ @Test
+ public void updateUseCase() {
+ UseCaseAttachState useCaseAttachState = new UseCaseAttachState(cameraId);
+ FakeUseCaseConfiguration configuration =
+ new FakeUseCaseConfiguration.Builder()
+ .setTargetName("UseCase")
+ .setLensFacing(cameraLensFacing0)
+ .build();
+ TestUseCase fakeUseCase = new TestUseCase(configuration, cameraId);
+
+ useCaseAttachState.setUseCaseOnline(fakeUseCase);
+ useCaseAttachState.setUseCaseActive(fakeUseCase);
+
+ // The original template should be PREVIEW.
+ SessionConfiguration firstSessionConfiguration =
+ useCaseAttachState.getActiveAndOnlineBuilder().build();
+ assertThat(firstSessionConfiguration.getTemplateType())
+ .isEqualTo(CameraDevice.TEMPLATE_PREVIEW);
+
+ // Change the template to STILL_CAPTURE.
+ SessionConfiguration.Builder builder = new SessionConfiguration.Builder();
+ builder.setTemplateType(CameraDevice.TEMPLATE_STILL_CAPTURE);
+ fakeUseCase.attachToCamera(cameraId, builder.build());
+
+ useCaseAttachState.updateUseCase(fakeUseCase);
+
+ // The new template should be STILL_CAPTURE.
+ SessionConfiguration secondSessionConfiguration =
+ useCaseAttachState.getActiveAndOnlineBuilder().build();
+ assertThat(secondSessionConfiguration.getTemplateType())
+ .isEqualTo(CameraDevice.TEMPLATE_STILL_CAPTURE);
+ }
+
+ @Test
+ public void setUseCaseOnlineWithWrongCamera() {
+ UseCaseAttachState useCaseAttachState = new UseCaseAttachState(cameraId);
+ FakeUseCaseConfiguration configuration =
+ new FakeUseCaseConfiguration.Builder()
+ .setTargetName("UseCase")
+ .setLensFacing(cameraLensFacing1)
+ .build();
+ TestUseCase fakeUseCase = new TestUseCase(configuration, cameraId);
+
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> useCaseAttachState.setUseCaseOnline(fakeUseCase));
+ }
+
+ @Test
+ public void setUseCaseActiveWithWrongCamera() {
+ UseCaseAttachState useCaseAttachState = new UseCaseAttachState(cameraId);
+ FakeUseCaseConfiguration configuration =
+ new FakeUseCaseConfiguration.Builder()
+ .setTargetName("UseCase")
+ .setLensFacing(cameraLensFacing1)
+ .build();
+ TestUseCase fakeUseCase = new TestUseCase(configuration, cameraId);
+
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> useCaseAttachState.setUseCaseActive(fakeUseCase));
+ }
+
+ private static class TestUseCase extends FakeUseCase {
+ private final Surface surface = Mockito.mock(Surface.class);
+ private final CameraDevice.StateCallback deviceStateCallback =
+ Mockito.mock(CameraDevice.StateCallback.class);
+ private final CameraCaptureSession.StateCallback sessionStateCallback =
+ Mockito.mock(CameraCaptureSession.StateCallback.class);
+ private final CameraCaptureCallback cameraCaptureCallback =
+ Mockito.mock(CameraCaptureCallback.class);
+
+ TestUseCase(FakeUseCaseConfiguration configuration, String cameraId) {
+ super(configuration);
+ Map<String, Size> suggestedResolutionMap = new HashMap<>();
+ suggestedResolutionMap.put(cameraId, new Size(640, 480));
+ updateSuggestedResolution(suggestedResolutionMap);
+ }
+
+ @Override
+ protected Map<String, Size> onSuggestedResolutionUpdated(
+ Map<String, Size> suggestedResolutionMap) {
+ SessionConfiguration.Builder builder = new SessionConfiguration.Builder();
+ builder.setTemplateType(CameraDevice.TEMPLATE_PREVIEW);
+ builder.addSurface(new ImmediateSurface(surface));
+ builder.setDeviceStateCallback(deviceStateCallback);
+ builder.setSessionStateCallback(sessionStateCallback);
+ builder.setCameraCaptureCallback(cameraCaptureCallback);
+
+ LensFacing lensFacing =
+ ((CameraDeviceConfiguration) getUseCaseConfiguration()).getLensFacing();
+ try {
+ String cameraId = CameraX.getCameraWithLensFacing(lensFacing);
+ attachToCamera(cameraId, builder.build());
+ } catch (Exception e) {
+ throw new IllegalArgumentException(
+ "Unable to attach to camera with LensFacing " + lensFacing, e);
+ }
+ return suggestedResolutionMap;
+ }
+ }
+}
diff --git a/camera/core/src/androidTest/java/androidx/camera/core/UseCaseGroupAndroidTest.java b/camera/core/src/androidTest/java/androidx/camera/core/UseCaseGroupAndroidTest.java
new file mode 100644
index 0000000..c218c18
--- /dev/null
+++ b/camera/core/src/androidTest/java/androidx/camera/core/UseCaseGroupAndroidTest.java
@@ -0,0 +1,127 @@
+/*
+ * Copyright (C) 2019 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.core;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mockito;
+
+@RunWith(AndroidJUnit4.class)
+public final class UseCaseGroupAndroidTest {
+ private final UseCaseGroup.StateChangeListener mockListener =
+ Mockito.mock(UseCaseGroup.StateChangeListener.class);
+ private FakeUseCaseConfiguration fakeUseCaseConfiguration;
+ private FakeOtherUseCaseConfiguration fakeOtherUseCaseConfiguration;
+ private UseCaseGroup useCaseGroup;
+ private FakeUseCase fakeUseCase;
+ private FakeOtherUseCase fakeOtherUseCase;
+
+ @Before
+ public void setUp() {
+ fakeUseCaseConfiguration =
+ new FakeUseCaseConfiguration.Builder()
+ .setTargetName("fakeUseCaseConfiguration")
+ .build();
+ fakeOtherUseCaseConfiguration =
+ new FakeOtherUseCaseConfiguration.Builder()
+ .setTargetName("fakeOtherUseCaseConfiguration")
+ .build();
+ useCaseGroup = new UseCaseGroup();
+ fakeUseCase = new FakeUseCase(fakeUseCaseConfiguration);
+ fakeOtherUseCase = new FakeOtherUseCase(fakeOtherUseCaseConfiguration);
+ }
+
+ @Test
+ public void groupStartsEmpty() {
+ assertThat(useCaseGroup.getUseCases()).isEmpty();
+ }
+
+ @Test
+ public void newUseCaseIsAdded_whenNoneExistsInGroup() {
+ assertThat(useCaseGroup.addUseCase(fakeUseCase)).isTrue();
+ assertThat(useCaseGroup.getUseCases()).containsExactly(fakeUseCase);
+ }
+
+ @Test
+ public void multipleUseCases_canBeAdded() {
+ assertThat(useCaseGroup.addUseCase(fakeUseCase)).isTrue();
+ assertThat(useCaseGroup.addUseCase(fakeOtherUseCase)).isTrue();
+
+ assertThat(useCaseGroup.getUseCases()).containsExactly(fakeUseCase, fakeOtherUseCase);
+ }
+
+ @Test
+ public void groupBecomesEmpty_afterGroupIsCleared() {
+ useCaseGroup.addUseCase(fakeUseCase);
+ useCaseGroup.clear();
+
+ assertThat(useCaseGroup.getUseCases()).isEmpty();
+ }
+
+ @Test
+ public void useCaseIsCleared_afterGroupIsCleared() {
+ useCaseGroup.addUseCase(fakeUseCase);
+ assertThat(fakeUseCase.isCleared()).isFalse();
+
+ useCaseGroup.clear();
+
+ assertThat(fakeUseCase.isCleared()).isTrue();
+ }
+
+ @Test
+ public void useCaseRemoved_afterRemovedCalled() {
+ useCaseGroup.addUseCase(fakeUseCase);
+
+ useCaseGroup.removeUseCase(fakeUseCase);
+
+ assertThat(useCaseGroup.getUseCases()).isEmpty();
+ }
+
+ @Test
+ public void listenerOnGroupActive_ifUseCaseGroupStarted() {
+ useCaseGroup.setListener(mockListener);
+ useCaseGroup.start();
+
+ verify(mockListener, times(1)).onGroupActive(useCaseGroup);
+ }
+
+ @Test
+ public void listenerOnGroupInactive_ifUseCaseGroupStopped() {
+ useCaseGroup.setListener(mockListener);
+ useCaseGroup.stop();
+
+ verify(mockListener, times(1)).onGroupInactive(useCaseGroup);
+ }
+
+ @Test
+ public void setListener_replacesPreviousListener() {
+ useCaseGroup.setListener(mockListener);
+ useCaseGroup.setListener(null);
+
+ useCaseGroup.start();
+ verify(mockListener, never()).onGroupActive(useCaseGroup);
+ }
+}
diff --git a/camera/core/src/androidTest/java/androidx/camera/core/UseCaseGroupLifecycleControllerAndroidTest.java b/camera/core/src/androidTest/java/androidx/camera/core/UseCaseGroupLifecycleControllerAndroidTest.java
new file mode 100644
index 0000000..065a53d
--- /dev/null
+++ b/camera/core/src/androidTest/java/androidx/camera/core/UseCaseGroupLifecycleControllerAndroidTest.java
@@ -0,0 +1,103 @@
+/*
+ * Copyright (C) 2019 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.core;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import androidx.camera.testing.fakes.FakeLifecycleOwner;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mockito;
+
+@RunWith(AndroidJUnit4.class)
+public class UseCaseGroupLifecycleControllerAndroidTest {
+ private final UseCaseGroup.StateChangeListener mockListener =
+ Mockito.mock(UseCaseGroup.StateChangeListener.class);
+ private UseCaseGroupLifecycleController useCaseGroupLifecycleController;
+ private FakeLifecycleOwner lifecycleOwner;
+
+ @Before
+ public void setUp() {
+ lifecycleOwner = new FakeLifecycleOwner();
+ }
+
+ @Test
+ public void groupCanBeMadeObserverOfLifecycle() {
+ assertThat(lifecycleOwner.getObserverCount()).isEqualTo(0);
+
+ useCaseGroupLifecycleController =
+ new UseCaseGroupLifecycleController(
+ lifecycleOwner.getLifecycle(), new UseCaseGroup());
+
+ assertThat(lifecycleOwner.getObserverCount()).isEqualTo(1);
+ }
+
+ @Test
+ public void groupCanStopObservingALifeCycle() {
+ useCaseGroupLifecycleController =
+ new UseCaseGroupLifecycleController(
+ lifecycleOwner.getLifecycle(), new UseCaseGroup());
+ assertThat(lifecycleOwner.getObserverCount()).isEqualTo(1);
+
+ useCaseGroupLifecycleController.release();
+
+ assertThat(lifecycleOwner.getObserverCount()).isEqualTo(0);
+ }
+
+ @Test
+ public void groupCanBeReleasedMultipleTimes() {
+ useCaseGroupLifecycleController =
+ new UseCaseGroupLifecycleController(
+ lifecycleOwner.getLifecycle(), new UseCaseGroup());
+
+ useCaseGroupLifecycleController.release();
+ useCaseGroupLifecycleController.release();
+ }
+
+ @Test
+ public void lifecycleStart_triggersOnActive() {
+ useCaseGroupLifecycleController =
+ new UseCaseGroupLifecycleController(
+ lifecycleOwner.getLifecycle(), new UseCaseGroup());
+ useCaseGroupLifecycleController.getUseCaseGroup().setListener(mockListener);
+
+ lifecycleOwner.start();
+
+ verify(mockListener, times(1))
+ .onGroupActive(useCaseGroupLifecycleController.getUseCaseGroup());
+ }
+
+ @Test
+ public void lifecycleStop_triggersOnInactive() {
+ useCaseGroupLifecycleController =
+ new UseCaseGroupLifecycleController(
+ lifecycleOwner.getLifecycle(), new UseCaseGroup());
+ useCaseGroupLifecycleController.getUseCaseGroup().setListener(mockListener);
+ lifecycleOwner.start();
+
+ lifecycleOwner.stop();
+
+ verify(mockListener, times(1))
+ .onGroupInactive(useCaseGroupLifecycleController.getUseCaseGroup());
+ }
+}
diff --git a/camera/core/src/androidTest/java/androidx/camera/core/UseCaseGroupRepositoryAndroidTest.java b/camera/core/src/androidTest/java/androidx/camera/core/UseCaseGroupRepositoryAndroidTest.java
new file mode 100644
index 0000000..dc961da
--- /dev/null
+++ b/camera/core/src/androidTest/java/androidx/camera/core/UseCaseGroupRepositoryAndroidTest.java
@@ -0,0 +1,117 @@
+/*
+ * Copyright (C) 2019 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.core;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+
+import androidx.camera.testing.fakes.FakeLifecycleOwner;
+import androidx.lifecycle.LifecycleOwner;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.Map;
+
+@RunWith(AndroidJUnit4.class)
+public final class UseCaseGroupRepositoryAndroidTest {
+
+ private FakeLifecycleOwner lifecycle;
+ private UseCaseGroupRepository repository;
+ private Map<LifecycleOwner, UseCaseGroupLifecycleController> useCasesMap;
+
+ @Before
+ public void setUp() {
+ lifecycle = new FakeLifecycleOwner();
+ repository = new UseCaseGroupRepository();
+ useCasesMap = repository.getUseCasesMap();
+ }
+
+ @Test
+ public void repositoryStartsEmpty() {
+ assertThat(useCasesMap).isEmpty();
+ }
+
+ @Test
+ public void newUseCaseGroupIsCreated_whenNoGroupExistsForLifecycleInRepository() {
+ UseCaseGroupLifecycleController group = repository.getOrCreateUseCaseGroup(lifecycle);
+
+ assertThat(useCasesMap).containsExactly(lifecycle, group);
+ }
+
+ @Test
+ public void existingUseCaseGroupIsReturned_whenGroupExistsForLifecycleInRepository() {
+ UseCaseGroupLifecycleController firstGroup = repository.getOrCreateUseCaseGroup(lifecycle);
+ UseCaseGroupLifecycleController secondGroup = repository.getOrCreateUseCaseGroup(lifecycle);
+
+ assertThat(firstGroup).isSameAs(secondGroup);
+ assertThat(useCasesMap).containsExactly(lifecycle, firstGroup);
+ }
+
+ @Test
+ public void differentUseCaseGroupsAreCreated_forDifferentLifecycles() {
+ UseCaseGroupLifecycleController firstGroup = repository.getOrCreateUseCaseGroup(lifecycle);
+ FakeLifecycleOwner secondLifecycle = new FakeLifecycleOwner();
+ UseCaseGroupLifecycleController secondGroup =
+ repository.getOrCreateUseCaseGroup(secondLifecycle);
+
+ assertThat(useCasesMap)
+ .containsExactly(lifecycle, firstGroup, secondLifecycle, secondGroup);
+ }
+
+ @Test
+ public void useCaseGroupObservesLifecycle() {
+ repository.getOrCreateUseCaseGroup(lifecycle);
+
+ // One observer is the use case group. The other observer removes the use case from the
+ // repository when the lifecycle is destroyed.
+ assertThat(lifecycle.getObserverCount()).isEqualTo(2);
+ }
+
+ @Test
+ public void useCaseGroupIsRemovedFromRepository_whenLifecycleIsDestroyed() {
+ repository.getOrCreateUseCaseGroup(lifecycle);
+ lifecycle.destroy();
+
+ assertThat(useCasesMap).isEmpty();
+ }
+
+ @Test
+ public void useCaseIsCleared_whenLifecycleIsDestroyed() {
+ UseCaseGroupLifecycleController group = repository.getOrCreateUseCaseGroup(lifecycle);
+ FakeUseCase useCase = new FakeUseCase();
+ group.getUseCaseGroup().addUseCase(useCase);
+
+ assertThat(useCase.isCleared()).isFalse();
+
+ lifecycle.destroy();
+
+ assertThat(useCase.isCleared()).isTrue();
+ }
+
+ @Test
+ public void exception_whenCreatingWithDestroyedLifecycle() {
+ lifecycle.destroy();
+
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> repository.getOrCreateUseCaseGroup(lifecycle));
+ }
+}
diff --git a/camera/core/src/main/AndroidManifest.xml b/camera/core/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..b8f5fd2
--- /dev/null
+++ b/camera/core/src/main/AndroidManifest.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2019 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.
+-->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="androidx.camera.core">
+
+ <uses-sdk
+ android:minSdkVersion="21"
+ android:targetSdkVersion="27" />
+</manifest>
diff --git a/camera/core/src/main/java/androidx/camera/core/AndroidImageProxy.java b/camera/core/src/main/java/androidx/camera/core/AndroidImageProxy.java
new file mode 100644
index 0000000..a538d1d
--- /dev/null
+++ b/camera/core/src/main/java/androidx/camera/core/AndroidImageProxy.java
@@ -0,0 +1,147 @@
+/*
+ * Copyright (C) 2019 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.core;
+
+import android.annotation.SuppressLint;
+import android.graphics.Rect;
+import android.media.Image;
+import android.os.Build;
+
+import androidx.annotation.GuardedBy;
+
+import java.nio.ByteBuffer;
+
+/** An {@link ImageProxy} which wraps around an {@link Image}. */
+final class AndroidImageProxy implements ImageProxy {
+ /**
+ * Image.setTimestamp(long) was added in M. On lower API levels, we use our own timestamp field
+ * to provide a more consistent behavior across more devices.
+ */
+ // TODO(b/124267925): Remove @SuppressLint once we target API 21
+ @SuppressLint("ObsoleteSdkInt")
+ private static final boolean SET_TIMESTAMP_AVAILABLE_IN_FRAMEWORK =
+ Build.VERSION.SDK_INT >= Build.VERSION_CODES.M;
+
+ @GuardedBy("this")
+ private final Image image;
+
+ @GuardedBy("this")
+ private final PlaneProxy[] planes;
+
+ @GuardedBy("this")
+ private long timestamp;
+
+ /**
+ * Creates a new instance which wraps the given image.
+ *
+ * @param image to wrap
+ * @return new {@link AndroidImageProxy} instance
+ */
+ AndroidImageProxy(Image image) {
+ this.image = image;
+
+ Image.Plane[] originalPlanes = image.getPlanes();
+ if (originalPlanes != null) {
+ this.planes = new PlaneProxy[originalPlanes.length];
+ for (int i = 0; i < originalPlanes.length; ++i) {
+ this.planes[i] = new PlaneProxy(originalPlanes[i]);
+ }
+ } else {
+ this.planes = new PlaneProxy[0];
+ }
+
+ this.timestamp = image.getTimestamp();
+ }
+
+ @Override
+ public synchronized void close() {
+ image.close();
+ }
+
+ @Override
+ public synchronized Rect getCropRect() {
+ return image.getCropRect();
+ }
+
+ @Override
+ public synchronized void setCropRect(Rect rect) {
+ image.setCropRect(rect);
+ }
+
+ @Override
+ public synchronized int getFormat() {
+ return image.getFormat();
+ }
+
+ @Override
+ public synchronized int getHeight() {
+ return image.getHeight();
+ }
+
+ @Override
+ public synchronized int getWidth() {
+ return image.getWidth();
+ }
+
+ @Override
+ public synchronized long getTimestamp() {
+ if (SET_TIMESTAMP_AVAILABLE_IN_FRAMEWORK) {
+ return image.getTimestamp();
+ } else {
+ return timestamp;
+ }
+ }
+
+ @Override
+ public synchronized void setTimestamp(long timestamp) {
+ if (SET_TIMESTAMP_AVAILABLE_IN_FRAMEWORK) {
+ image.setTimestamp(timestamp);
+ } else {
+ this.timestamp = timestamp;
+ }
+ }
+
+ @Override
+ public synchronized ImageProxy.PlaneProxy[] getPlanes() {
+ return planes;
+ }
+
+ /** An {@link ImageProxy.PlaneProxy} which wraps around an {@link Image.Plane}. */
+ private static final class PlaneProxy implements ImageProxy.PlaneProxy {
+ @GuardedBy("this")
+ private final Image.Plane plane;
+
+ PlaneProxy(Image.Plane plane) {
+ this.plane = plane;
+ }
+
+ @Override
+ public synchronized int getRowStride() {
+ return plane.getRowStride();
+ }
+
+ @Override
+ public synchronized int getPixelStride() {
+ return plane.getPixelStride();
+ }
+
+ @Override
+ public synchronized ByteBuffer getBuffer() {
+ return plane.getBuffer();
+ }
+ }
+}
diff --git a/camera/core/src/main/java/androidx/camera/core/AndroidImageReaderProxy.java b/camera/core/src/main/java/androidx/camera/core/AndroidImageReaderProxy.java
new file mode 100644
index 0000000..2a7b579f
--- /dev/null
+++ b/camera/core/src/main/java/androidx/camera/core/AndroidImageReaderProxy.java
@@ -0,0 +1,107 @@
+/*
+ * Copyright (C) 2019 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.core;
+
+import android.media.Image;
+import android.media.ImageReader;
+import android.os.Handler;
+import android.view.Surface;
+
+import androidx.annotation.GuardedBy;
+import androidx.annotation.Nullable;
+
+/**
+ * An {@link ImageReaderProxy} which wraps around an {@link ImageReader}.
+ *
+ * <p>All methods map one-to-one between this {@link ImageReaderProxy} and the wrapped {@link
+ * ImageReader}.
+ */
+final class AndroidImageReaderProxy implements ImageReaderProxy {
+ @GuardedBy("this")
+ private final ImageReader imageReader;
+
+ /**
+ * Creates a new instance which wraps the given image reader.
+ *
+ * @param imageReader to wrap
+ * @return new {@link AndroidImageReaderProxy} instance
+ */
+ AndroidImageReaderProxy(ImageReader imageReader) {
+ this.imageReader = imageReader;
+ }
+
+ @Override
+ @Nullable
+ public synchronized ImageProxy acquireLatestImage() {
+ Image image = imageReader.acquireLatestImage();
+ if (image == null) {
+ return null;
+ }
+ return new AndroidImageProxy(image);
+ }
+
+ @Override
+ @Nullable
+ public synchronized ImageProxy acquireNextImage() {
+ Image image = imageReader.acquireNextImage();
+ if (image == null) {
+ return null;
+ }
+ return new AndroidImageProxy(image);
+ }
+
+ @Override
+ public synchronized void close() {
+ imageReader.close();
+ }
+
+ @Override
+ public synchronized int getHeight() {
+ return imageReader.getHeight();
+ }
+
+ @Override
+ public synchronized int getWidth() {
+ return imageReader.getWidth();
+ }
+
+ @Override
+ public synchronized int getImageFormat() {
+ return imageReader.getImageFormat();
+ }
+
+ @Override
+ public synchronized int getMaxImages() {
+ return imageReader.getMaxImages();
+ }
+
+ @Override
+ public synchronized Surface getSurface() {
+ return imageReader.getSurface();
+ }
+
+ @Override
+ public synchronized void setOnImageAvailableListener(
+ @Nullable ImageReaderProxy.OnImageAvailableListener listener,
+ @Nullable Handler handler) {
+ ImageReader.OnImageAvailableListener transformedListener =
+ reader -> {
+ listener.onImageAvailable(AndroidImageReaderProxy.this);
+ };
+ imageReader.setOnImageAvailableListener(transformedListener, handler);
+ }
+}
diff --git a/camera/core/src/main/java/androidx/camera/core/AppConfiguration.java b/camera/core/src/main/java/androidx/camera/core/AppConfiguration.java
new file mode 100644
index 0000000..8847cc5
--- /dev/null
+++ b/camera/core/src/main/java/androidx/camera/core/AppConfiguration.java
@@ -0,0 +1,179 @@
+/*
+ * Copyright (C) 2019 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.core;
+
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.annotation.RestrictTo.Scope;
+
+/**
+ * Configuration for adding implementation and user-specific behavior to CameraX.
+ *
+ * <p>The AppConfiguration
+ *
+ * @hide
+ */
+public final class AppConfiguration implements TargetConfiguration<CameraX> {
+
+ static final Option<CameraFactory> OPTION_CAMERA_FACTORY =
+ Option.create("camerax.core.appConfig.cameraFactory", CameraFactory.class);
+ static final Option<CameraDeviceSurfaceManager> OPTION_DEVICE_SURFACE_MANAGER =
+ Option.create(
+ "camerax.core.appConfig.deviceSurfaceManager",
+ CameraDeviceSurfaceManager.class);
+ static final Option<UseCaseConfigurationFactory> OPTION_USECASE_CONFIG_FACTORY =
+ Option.create(
+ "camerax.core.appConfig.useCaseConfigFactory",
+ UseCaseConfigurationFactory.class);
+ private final OptionsBundle config;
+
+ AppConfiguration(OptionsBundle options) {
+ this.config = options;
+ }
+
+ /**
+ * Returns the {@link CameraFactory} implementation for the application.
+ *
+ * @hide
+ */
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ public CameraFactory getCameraFactory(@Nullable CameraFactory valueIfMissing) {
+ return getConfiguration().retrieveOption(OPTION_CAMERA_FACTORY, valueIfMissing);
+ }
+
+ /**
+ * Returns the {@link CameraDeviceSurfaceManager} implementation for the application.
+ *
+ * @hide
+ */
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ public CameraDeviceSurfaceManager getDeviceSurfaceManager(
+ @Nullable CameraDeviceSurfaceManager valueIfMissing) {
+ return getConfiguration().retrieveOption(OPTION_DEVICE_SURFACE_MANAGER, valueIfMissing);
+ }
+
+ // Option Declarations:
+ // ***********************************************************************************************
+
+ /**
+ * Returns the {@link UseCaseConfigurationFactory} implementation for the application.
+ *
+ * <p>This factory should produce all default configurations for the application's use cases.
+ *
+ * @hide
+ */
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ public UseCaseConfigurationFactory getUseCaseConfigRepository(
+ @Nullable UseCaseConfigurationFactory valueIfMissing) {
+ return getConfiguration().retrieveOption(OPTION_USECASE_CONFIG_FACTORY, valueIfMissing);
+ }
+
+ @Override
+ public Configuration getConfiguration() {
+ return config;
+ }
+
+ /** A builder for generating {@link AppConfiguration} objects. */
+ public static final class Builder
+ implements TargetConfiguration.Builder<CameraX, AppConfiguration, Builder> {
+
+ private final MutableOptionsBundle mutableConfig;
+
+ /** Creates a new Builder object. */
+ public Builder() {
+ this(MutableOptionsBundle.create());
+ }
+
+ private Builder(MutableOptionsBundle mutableConfig) {
+ this.mutableConfig = mutableConfig;
+
+ Class<?> oldConfigClass =
+ mutableConfig.retrieveOption(TargetConfiguration.OPTION_TARGET_CLASS, null);
+ if (oldConfigClass != null && !oldConfigClass.equals(CameraX.class)) {
+ throw new IllegalArgumentException(
+ "Invalid target class configuration for "
+ + AppConfiguration.Builder.this
+ + ": "
+ + oldConfigClass);
+ }
+
+ setTargetClass(CameraX.class);
+ }
+
+ /**
+ * Generates a Builder from another Configuration object
+ *
+ * @param configuration An immutable configuration to pre-populate this builder.
+ * @return The new Builder.
+ */
+ public static Builder fromConfig(Configuration configuration) {
+ return new Builder(MutableOptionsBundle.from(configuration));
+ }
+
+ /**
+ * Sets the {@link CameraFactory} implementation for the application.
+ *
+ * @hide
+ */
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ public Builder setCameraFactory(CameraFactory cameraFactory) {
+ getMutableConfiguration().insertOption(OPTION_CAMERA_FACTORY, cameraFactory);
+ return builder();
+ }
+
+ /**
+ * Sets the {@link CameraDeviceSurfaceManager} implementation for the application.
+ *
+ * @hide
+ */
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ public Builder setDeviceSurfaceManager(CameraDeviceSurfaceManager repository) {
+ getMutableConfiguration().insertOption(OPTION_DEVICE_SURFACE_MANAGER, repository);
+ return builder();
+ }
+
+ /**
+ * Sets the {@link UseCaseConfigurationFactory} implementation for the application.
+ *
+ * <p>This factory should produce all default configurations for the application's use
+ * cases.
+ *
+ * @hide
+ */
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ public Builder setUseCaseConfigFactory(UseCaseConfigurationFactory repository) {
+ getMutableConfiguration().insertOption(OPTION_USECASE_CONFIG_FACTORY, repository);
+ return builder();
+ }
+
+ @Override
+ public MutableConfiguration getMutableConfiguration() {
+ return mutableConfig;
+ }
+
+ /** The solution for the unchecked cast warning. */
+ @Override
+ public Builder builder() {
+ return this;
+ }
+
+ @Override
+ public AppConfiguration build() {
+ return new AppConfiguration(OptionsBundle.from(mutableConfig));
+ }
+ }
+}
diff --git a/camera/core/src/main/java/androidx/camera/core/BaseCamera.java b/camera/core/src/main/java/androidx/camera/core/BaseCamera.java
new file mode 100644
index 0000000..5aa5cc7
--- /dev/null
+++ b/camera/core/src/main/java/androidx/camera/core/BaseCamera.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright (C) 2019 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.core;
+
+import java.util.Collection;
+
+/**
+ * The base camera interface. It is controlled by the change of state in use cases.
+ *
+ * @hide
+ */
+public interface BaseCamera extends BaseUseCase.StateChangeListener {
+ /**
+ * Open the camera asynchronously.
+ *
+ * <p>Once the camera has been opened use case state transitions can be used to control the
+ * camera pipeline.
+ */
+ void open();
+
+ /**
+ * Close the camera asynchronously.
+ *
+ * <p>Once the camera is closed the camera will no longer produce data. The camera must be
+ * reopened for it to produce data again.
+ */
+ void close();
+
+ /**
+ * Release the camera.
+ *
+ * <p>Once the camera is released it is permanently closed. A new instance must be created to
+ * access the camera.
+ */
+ void release();
+
+ /**
+ * Sets the use case to be in the state where the capture session will be configured to handle
+ * capture requests from the use cases.
+ */
+ void addOnlineUseCase(Collection<BaseUseCase> baseUseCases);
+
+ /**
+ * Removes the use case to be in the state where the capture session will be configured to
+ * handle capture requests from the use cases.
+ */
+ void removeOnlineUseCase(Collection<BaseUseCase> baseUseCases);
+
+ /** Returns the global CameraControl attached to this camera. */
+ default CameraControl getCameraControl() {
+ return CameraControl.defaultEmptyInstance();
+ }
+
+ /** Returns an interface to retrieve characteristics of the camera. */
+ default CameraInfo getCameraInfo() throws CameraInfoUnavailableException {
+ throw new CameraInfoUnavailableException("Camera info not implemented.");
+ }
+}
diff --git a/camera/core/src/main/java/androidx/camera/core/BaseUseCase.java b/camera/core/src/main/java/androidx/camera/core/BaseUseCase.java
new file mode 100644
index 0000000..66f537c
--- /dev/null
+++ b/camera/core/src/main/java/androidx/camera/core/BaseUseCase.java
@@ -0,0 +1,467 @@
+/*
+ * Copyright (C) 2019 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.core;
+
+import android.util.Log;
+import android.util.Size;
+
+import androidx.annotation.CallSuper;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.annotation.RestrictTo.Scope;
+import androidx.camera.core.Configuration.Option;
+import androidx.camera.core.UseCaseConfiguration.Builder;
+import androidx.lifecycle.LifecycleOwner;
+
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Set;
+
+/**
+ * The use case which all other use cases are built on top of.
+ *
+ * <p>A BaseUseCase provides functionality to map a {@link BaseCamera} to a {@link
+ * SessionConfiguration} and the communication of the active/inactive state to the Camera.
+ */
+public abstract class BaseUseCase {
+ private static final String TAG = "BaseUseCase";
+
+ /**
+ * The set of {@link StateChangeListener} that are currently listening state transitions of this
+ * use case.
+ */
+ private final Set<StateChangeListener> listeners = new HashSet<>();
+
+ /**
+ * A map of camera id and CameraControl. A CameraControl will be attached into the usecase after
+ * usecase is bound to lifecycle. It is used for controlling zoom/focus/flash/triggering Af or
+ * AE.
+ */
+ private final Map<String, CameraControl> attachedCameraControlMap = new HashMap<>();
+
+ /**
+ * A map of the names of the {@link android.hardware.camera2.CameraDevice} to the {@link
+ * SessionConfiguration} that have been attached to this BaseUseCase
+ */
+ private final Map<String, SessionConfiguration> attachedCameraIdToSessionConfigurationMap =
+ new HashMap<>();
+
+ /**
+ * A map of the names of the {@link android.hardware.camera2.CameraDevice} to the surface
+ * resolution that have been attached to this BaseUseCase
+ */
+ private final Map<String, Size> attachedSurfaceResolutionMap = new HashMap<>();
+
+ private State state = State.INACTIVE;
+
+ private UseCaseConfiguration<?> useCaseConfiguration;
+
+ /**
+ * Except for ImageFormat.JPEG or ImageFormat.YUV, other image formats like SurfaceTexture or
+ * MediaCodec classes will be mapped to internal format HAL_PIXEL_FORMAT_IMPLEMENTATION_DEFINED
+ * (0x22) in StreamConfigurationMap.java. 0x22 is also the code for ImageFormat.PRIVATE. But
+ * there is no ImageFormat.PRIVATE supported before Android level 23. There is same internal
+ * code 0x22 for internal corresponding format HAL_PIXEL_FORMAT_IMPLEMENTATION_DEFINED.
+ * Therefore, setting 0x22 as default image format.
+ */
+ private int imageFormat = ImageFormatConstants.INTERNAL_DEFINED_IMAGE_FORMAT_PRIVATE;
+
+ /**
+ * Creates a named instance of the use case.
+ *
+ * @param useCaseConfiguration the configuration object used for this use case
+ */
+ protected BaseUseCase(UseCaseConfiguration<?> useCaseConfiguration) {
+ updateUseCaseConfiguration(useCaseConfiguration);
+ }
+
+ /**
+ * Returns a {@link UseCaseConfiguration.Builder} pre-populated with default configuration
+ * options.
+ *
+ * <p>This is used to generate a final configuration by combining the user-supplied
+ * configuration with the default configuration. Subclasses can override this method to provide
+ * the pre-populated builder. If <code>null</code> is returned, then the user-supplied
+ * configuration will be used directly.
+ *
+ * @return A builder pre-populated with use case default options.
+ */
+ @Nullable
+ protected UseCaseConfiguration.Builder<?, ?, ?> getDefaultBuilder() {
+ return null;
+ }
+
+ /**
+ * Updates the stored use case configuration.
+ *
+ * <p>This configuration will be combined with the default configuration that is contained in
+ * the pre-populated builder supplied by {@link #getDefaultBuilder()}, if it exists and the
+ * behavior of {@link #applyDefaults(UseCaseConfiguration, Builder)} is not overridden. Once
+ * this method returns, the combined use case configuration can be retrieved with {@link
+ * #getUseCaseConfiguration()}.
+ *
+ * <p>This method alone will not make any changes to the {@link SessionConfiguration}, it is up
+ * to the use case to decide when to modify the session configuration.
+ *
+ * @param useCaseConfiguration Configuration which will be applied on top of use case defaults,
+ * if a default builder is provided by {@link #getDefaultBuilder()}.
+ */
+ protected void updateUseCaseConfiguration(UseCaseConfiguration<?> useCaseConfiguration) {
+ UseCaseConfiguration.Builder<?, ?, ?> defaultBuilder = getDefaultBuilder();
+ if (defaultBuilder == null) {
+ Log.w(
+ TAG,
+ "No default configuration available. Relying solely on user-supplied options.");
+ this.useCaseConfiguration = useCaseConfiguration;
+ } else {
+ this.useCaseConfiguration = applyDefaults(useCaseConfiguration, defaultBuilder);
+ }
+ }
+
+ /**
+ * Combines user-supplied configuration with use case default configuration.
+ *
+ * <p>This is called during initialization of the class. Subclassess can override this method to
+ * modify the behavior of combining user-supplied values and default values.
+ *
+ * @param userConfiguration The user-supplied configuration.
+ * @param defaultConfigBuilder A builder containing use-case default values.
+ * @return The configuration that will be used by this use case.
+ */
+ protected UseCaseConfiguration<?> applyDefaults(
+ UseCaseConfiguration<?> userConfiguration,
+ UseCaseConfiguration.Builder<?, ?, ?> defaultConfigBuilder) {
+
+ // If any options need special handling, this is the place to do it. For now we'll just copy
+ // over all options.
+ for (Option<?> opt : userConfiguration.listOptions()) {
+ @SuppressWarnings("unchecked") // Options/values are being copied directly
+ Option<Object> objectOpt = (Option<Object>) opt;
+ defaultConfigBuilder.insertOption(
+ objectOpt, userConfiguration.retrieveOption(objectOpt));
+ }
+
+ @SuppressWarnings(
+ "unchecked") // Since builder is a UseCaseConfiguration.Builder, it should produce a
+ // UseCaseConfiguration
+ UseCaseConfiguration<?> defaultConfig =
+ (UseCaseConfiguration<?>) defaultConfigBuilder.build();
+ return defaultConfig;
+ }
+
+ /**
+ * Get the names of the cameras which are attached to this use case.
+ *
+ * <p>The names will correspond to those of the camera as defined by {@link
+ * android.hardware.camera2.CameraManager}.
+ */
+ Set<String> getAttachedCameraIds() {
+ return attachedCameraIdToSessionConfigurationMap.keySet();
+ }
+
+ /**
+ * Attaches the BaseUseCase to a {@link android.hardware.camera2.CameraDevice} with the
+ * corresponding name.
+ *
+ * @param cameraId The name of the camera as defined by {@link
+ * android.hardware.camera2.CameraManager#getCameraIdList()}.
+ */
+ protected void attachToCamera(String cameraId, SessionConfiguration sessionConfiguration) {
+ attachedCameraIdToSessionConfigurationMap.put(cameraId, sessionConfiguration);
+ }
+
+ /**
+ * Add a {@link StateChangeListener}, which listens to this BaseUseCase's active and inactive
+ * transition events.
+ */
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ public void addStateChangeListener(StateChangeListener listener) {
+ listeners.add(listener);
+ }
+
+ /**
+ * Attach a CameraControl to this use case.
+ *
+ * @hide
+ */
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ public final void attachCameraControl(String cameraId, CameraControl cameraControl) {
+ attachedCameraControlMap.put(cameraId, cameraControl);
+ onCameraControlReady(cameraId);
+ }
+
+ /** Detach a CameraControl from this use case. */
+ final void detachCameraControl(String cameraId) {
+ attachedCameraControlMap.remove(cameraId);
+ }
+
+ /**
+ * Remove a {@link StateChangeListener} from listening to this BaseUseCase's active and inactive
+ * transition events.
+ *
+ * <p>If the listener isn't currently listening to the BaseUseCase then this call does nothing.
+ */
+ void removeStateChangeListener(StateChangeListener listener) {
+ listeners.remove(listener);
+ }
+
+ /**
+ * Get the {@link SessionConfiguration} for the specified camera id.
+ *
+ * @param cameraId the id of the camera as referred to be {@link
+ * android.hardware.camera2.CameraManager}
+ * @throws IllegalArgumentException if no camera with the specified cameraId is attached
+ */
+ public SessionConfiguration getSessionConfiguration(String cameraId) {
+ SessionConfiguration sessionConfiguration =
+ attachedCameraIdToSessionConfigurationMap.get(cameraId);
+ if (sessionConfiguration == null) {
+ throw new IllegalArgumentException("Invalid camera: " + cameraId);
+ } else {
+ return sessionConfiguration;
+ }
+ }
+
+ /**
+ * Notify all {@link StateChangeListener} that are listening to this BaseUseCase that it has
+ * transitioned to an active state.
+ */
+ protected final void notifyActive() {
+ state = State.ACTIVE;
+ notifyState();
+ }
+
+ /**
+ * Notify all {@link StateChangeListener} that are listening to this BaseUseCase that it has
+ * transitioned to an inactive state.
+ */
+ protected final void notifyInactive() {
+ state = State.INACTIVE;
+ notifyState();
+ }
+
+ /**
+ * Notify all {@link StateChangeListener} that are listening to this BaseUseCase that it has a
+ * single capture request.
+ */
+ protected final void notifySingleCapture(CaptureRequestConfiguration requestConfiguration) {
+ for (StateChangeListener listener : listeners) {
+ listener.onUseCaseSingleRequest(this, requestConfiguration);
+ }
+ }
+
+ /**
+ * Notify all {@link StateChangeListener} that are listening to this BaseUseCase that the
+ * settings have been updated.
+ */
+ protected final void notifyUpdated() {
+ for (StateChangeListener listener : listeners) {
+ listener.onUseCaseUpdated(this);
+ }
+ }
+
+ /**
+ * Notify all {@link StateChangeListener} that are listening to this BaseUseCase that the use
+ * case needs to be completely reset.
+ */
+ protected final void notifyReset() {
+ for (StateChangeListener listener : listeners) {
+ listener.onUseCaseReset(this);
+ }
+ }
+
+ /**
+ * Notify all {@link StateChangeListener} that are listening to this BaseUseCase of its current
+ * state.
+ */
+ protected final void notifyState() {
+ switch (state) {
+ case INACTIVE:
+ for (StateChangeListener listener : listeners) {
+ listener.onUseCaseInactive(this);
+ }
+ break;
+ case ACTIVE:
+ for (StateChangeListener listener : listeners) {
+ listener.onUseCaseActive(this);
+ }
+ break;
+ }
+ }
+
+ /** Clear out all {@link StateChangeListener} from listening to this BaseUseCase. */
+ @CallSuper
+ protected void clear() {
+ listeners.clear();
+ }
+
+ public String getName() {
+ return useCaseConfiguration.getTargetName("<UnknownUseCase-" + this.hashCode() + ">");
+ }
+
+ /**
+ * Retrieves the configuration used by this use case.
+ *
+ * @return the configuration used by this use case.
+ */
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ public UseCaseConfiguration<?> getUseCaseConfiguration() {
+ return useCaseConfiguration;
+ }
+
+ /**
+ * Retrieves the currently attached surface resolution.
+ *
+ * @param cameraId the camera id for the desired surface.
+ * @return the currently attached surface resolution for the given camera id.
+ */
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ public Size getAttachedSurfaceResolution(String cameraId) {
+ return attachedSurfaceResolutionMap.get(cameraId);
+ }
+
+ /**
+ * Offers suggested resolutions.
+ *
+ * <p>The keys of suggestedResolutionMap should only be cameraIds that are valid for this use
+ * case.
+ *
+ * @hide
+ */
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ public void updateSuggestedResolution(Map<String, Size> suggestedResolutionMap) {
+ Map<String, Size> resolutionMap = onSuggestedResolutionUpdated(suggestedResolutionMap);
+
+ for (Entry<String, Size> entry : resolutionMap.entrySet()) {
+ attachedSurfaceResolutionMap.put(entry.getKey(), entry.getValue());
+ }
+ }
+
+ /**
+ * Called when binding new use cases via {@link CameraX#bindToLifecycle(LifecycleOwner,
+ * BaseUseCase...)}. Need to override this function to create {@link SessionConfiguration} or
+ * other necessary objects like {@link android.media.ImageReader} depending on the resolution.
+ *
+ * @param suggestedResolutionMap A map of the names of the {@link
+ * android.hardware.camera2.CameraDevice} to the suggested
+ * resolution that depends on camera
+ * device capability and what and how many use cases will be
+ * bound.
+ * @return The map with the resolutions that finally used to create the SessionConfiguration to
+ * attach to the camera device.
+ */
+ protected abstract Map<String, Size> onSuggestedResolutionUpdated(
+ Map<String, Size> suggestedResolutionMap);
+
+ /**
+ * Called when CameraControl is attached into the UseCase. UseCase may need to override this
+ * method to configure the CameraControl here. Ex. Setting correct flash mode by
+ * CameraControl.setFlashMode to enable correct AE mode and flash state.
+ *
+ * @hide
+ */
+ protected void onCameraControlReady(String cameraId) {
+ }
+
+ /**
+ * Retrieves a previously attached {@link CameraControl}.
+ *
+ * @hide
+ */
+ protected CameraControl getCameraControl(String cameraId) {
+ CameraControl cameraControl = attachedCameraControlMap.get(cameraId);
+ if (cameraControl == null) {
+ return CameraControl.defaultEmptyInstance();
+ }
+ return cameraControl;
+ }
+
+ /**
+ * Get image format for the use case.
+ *
+ * @return image format for the use case
+ * @hide
+ */
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ public int getImageFormat() {
+ return imageFormat;
+ }
+
+ protected void setImageFormat(int imageFormat) {
+ this.imageFormat = imageFormat;
+ }
+
+ enum State {
+ /** Currently waiting for image data. */
+ ACTIVE,
+ /** Currently not waiting for image data. */
+ INACTIVE
+ }
+
+ /**
+ * Listener called when a {@link BaseUseCase} transitions between active/inactive states.
+ *
+ * @hide
+ */
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ public interface StateChangeListener {
+ /**
+ * Called when a {@link BaseUseCase} becomes active.
+ *
+ * <p>When a BaseUseCase is active it expects that all data producers attached to itself
+ * should start producing data for it to consume. In addition the BaseUseCase will start
+ * producing data that other classes can be consumed.
+ */
+ void onUseCaseActive(BaseUseCase useCase);
+
+ /**
+ * Called when a {@link BaseUseCase} becomes inactive.
+ *
+ * <p>When a BaseUseCase is inactive it no longer expects data to be produced for it. In
+ * addition the BaseUseCase will stop producing data for other classes to consume.
+ */
+ void onUseCaseInactive(BaseUseCase useCase);
+
+ /**
+ * Called when a {@link BaseUseCase} has updated settings.
+ *
+ * <p>When a {@link BaseUseCase} has updated settings, it is expected that the listener will
+ * use these updated settings to reconfigure the listener's own state. A settings update is
+ * orthogonal to the active/inactive state change.
+ */
+ void onUseCaseUpdated(BaseUseCase useCase);
+
+ /**
+ * Called when a {@link BaseUseCase} has updated settings that require complete reset of the
+ * camera.
+ *
+ * <p>Updating certain parameters of the use case require a full reset of the camera. This
+ * includes updating the {@link android.view.Surface} used by the use case.
+ */
+ void onUseCaseReset(BaseUseCase useCase);
+
+ /**
+ * Called when a {@link BaseUseCase} need a single capture request
+ *
+ * @param captureRequestConfiguration used to construct the single capture request
+ */
+ void onUseCaseSingleRequest(
+ BaseUseCase useCase, CaptureRequestConfiguration captureRequestConfiguration);
+ }
+}
diff --git a/camera/core/src/main/java/androidx/camera/core/CameraCaptureCallback.java b/camera/core/src/main/java/androidx/camera/core/CameraCaptureCallback.java
new file mode 100644
index 0000000..9adb0ca
--- /dev/null
+++ b/camera/core/src/main/java/androidx/camera/core/CameraCaptureCallback.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2019 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.core;
+
+import androidx.annotation.NonNull;
+
+/**
+ * A callback object for tracking the progress of a capture request submitted to the camera device.
+ *
+ * @hide
+ */
+public abstract class CameraCaptureCallback {
+
+ /**
+ * This method is called when an image capture has fully completed and all the result metadata
+ * is available.
+ *
+ * @param cameraCaptureResult The output metadata from the capture.
+ */
+ public void onCaptureCompleted(@NonNull CameraCaptureResult cameraCaptureResult) {
+ }
+
+ /**
+ * This method is called instead of {@link #onCaptureCompleted} when the camera device failed to
+ * produce a {@link CameraCaptureResult} for the request.
+ *
+ * @param failure The output failure from the capture, including the failure reason.
+ */
+ public void onCaptureFailed(@NonNull CameraCaptureFailure failure) {
+ }
+}
diff --git a/camera/core/src/main/java/androidx/camera/core/CameraCaptureCallbacks.java b/camera/core/src/main/java/androidx/camera/core/CameraCaptureCallbacks.java
new file mode 100644
index 0000000..fab0041
--- /dev/null
+++ b/camera/core/src/main/java/androidx/camera/core/CameraCaptureCallbacks.java
@@ -0,0 +1,97 @@
+/*
+ * Copyright (C) 2019 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.core;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
+import androidx.annotation.RestrictTo.Scope;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * Different implementations of {@link CameraCaptureCallback}.
+ *
+ * @hide
+ */
+@RestrictTo(Scope.LIBRARY_GROUP)
+public final class CameraCaptureCallbacks {
+ private CameraCaptureCallbacks() {
+ }
+
+ /** Returns a camera capture callback which does nothing. */
+ public static CameraCaptureCallback createNoOpCallback() {
+ return new NoOpCameraCaptureCallback();
+ }
+
+ /** Returns a camera capture callback which calls a list of other callbacks. */
+ static CameraCaptureCallback createComboCallback(List<CameraCaptureCallback> callbacks) {
+ return new ComboCameraCaptureCallback(callbacks);
+ }
+
+ /** Returns a camera capture callback which calls a list of other callbacks. */
+ public static CameraCaptureCallback createComboCallback(CameraCaptureCallback... callbacks) {
+ return createComboCallback(Arrays.asList(callbacks));
+ }
+
+ static final class NoOpCameraCaptureCallback extends CameraCaptureCallback {
+ @Override
+ public void onCaptureCompleted(CameraCaptureResult cameraCaptureResult) {
+ }
+
+ @Override
+ public void onCaptureFailed(CameraCaptureFailure failure) {
+ }
+ }
+
+ /**
+ * A CameraCaptureCallback which contains a list of CameraCaptureCallback and will propagate
+ * received callback to the list.
+ */
+ public static final class ComboCameraCaptureCallback extends CameraCaptureCallback {
+ private final List<CameraCaptureCallback> callbacks = new ArrayList<>();
+
+ ComboCameraCaptureCallback(List<CameraCaptureCallback> callbacks) {
+ for (CameraCaptureCallback callback : callbacks) {
+ // A no-op callback doesn't do anything, so avoid adding it to the final list.
+ if (!(callback instanceof NoOpCameraCaptureCallback)) {
+ this.callbacks.add(callback);
+ }
+ }
+ }
+
+ @Override
+ public void onCaptureCompleted(CameraCaptureResult result) {
+ for (CameraCaptureCallback callback : callbacks) {
+ callback.onCaptureCompleted(result);
+ }
+ }
+
+ @Override
+ public void onCaptureFailed(CameraCaptureFailure failure) {
+ for (CameraCaptureCallback callback : callbacks) {
+ callback.onCaptureFailed(failure);
+ }
+ }
+
+ @NonNull
+ public List<CameraCaptureCallback> getCallbacks() {
+ return callbacks;
+ }
+ }
+}
diff --git a/camera/core/src/main/java/androidx/camera/core/CameraCaptureFailure.java b/camera/core/src/main/java/androidx/camera/core/CameraCaptureFailure.java
new file mode 100644
index 0000000..66018f6
--- /dev/null
+++ b/camera/core/src/main/java/androidx/camera/core/CameraCaptureFailure.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2019 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.core;
+
+/**
+ * A report of failed capture for a single image capture.
+ *
+ * @hide
+ */
+public final class CameraCaptureFailure {
+
+ private final Reason reason;
+
+ /** @hide */
+ public CameraCaptureFailure(Reason reason) {
+ this.reason = reason;
+ }
+
+ /**
+ * Determine why the request was dropped, whether due to an error or to a user action.
+ *
+ * @return int The reason code.
+ * @see CameraCaptureFailure.Reason#ERROR
+ */
+ public Reason getReason() {
+ return reason;
+ }
+
+ /**
+ * The capture result has been dropped this frame only due to an error in the framework.
+ *
+ * @see #getReason()
+ */
+ public enum Reason {
+ ERROR,
+ }
+}
diff --git a/camera/core/src/main/java/androidx/camera/core/CameraCaptureMetaData.java b/camera/core/src/main/java/androidx/camera/core/CameraCaptureMetaData.java
new file mode 100644
index 0000000..490c568
--- /dev/null
+++ b/camera/core/src/main/java/androidx/camera/core/CameraCaptureMetaData.java
@@ -0,0 +1,133 @@
+/*
+ * Copyright (C) 2019 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.core;
+
+/**
+ * This class defines the enumeration constants used for querying the camera capture mode and
+ * results.
+ *
+ * @hide
+ */
+public final class CameraCaptureMetaData {
+
+ /** Auto focus (AF) mode. */
+ public enum AfMode {
+
+ /** AF mode is currently unknown. */
+ UNKNOWN,
+
+ /** The AF routine does not control the lens. */
+ OFF,
+
+ /**
+ * AF is triggered on demand.
+ *
+ * <p>In this mode, the lens does not move unless the auto focus trigger action is called.
+ */
+ ON_MANUAL_AUTO,
+
+ /**
+ * AF is continually scanning.
+ *
+ * <p>In this mode, the AF algorithm modifies the lens position continually to attempt to
+ * provide a constantly-in-focus stream.
+ */
+ ON_CONTINUOUS_AUTO
+ }
+
+ /** Auto focus (AF) state. */
+ public enum AfState {
+
+ /** AF state is currently unknown. */
+ UNKNOWN,
+
+ /** AF is off or not yet has been triggered. */
+ INACTIVE,
+
+ /** AF is performing an AF scan. */
+ SCANNING,
+
+ /** AF currently believes it is in focus. */
+ FOCUSED,
+
+ /** AF believes it is focused correctly and has locked focus. */
+ LOCKED_FOCUSED,
+
+ /** AF has failed to focus and has locked focus. */
+ LOCKED_NOT_FOCUSED
+ }
+
+ /** Auto exposure (AE) state. */
+ public enum AeState {
+
+ /** AE state is currently unknown. */
+ UNKNOWN,
+
+ /** AE is off or has not yet been triggered. */
+ INACTIVE,
+
+ /** AE is performing an AE search. */
+ SEARCHING,
+
+ /**
+ * AE has a good set of control values, but flash needs to be fired for good quality still
+ * capture.
+ */
+ FLASH_REQUIRED,
+
+ /** AE has a good set of control values for the current scene. */
+ CONVERGED,
+
+ /** AE has been locked. */
+ LOCKED
+ }
+
+ /** Auto white balance (AWB) state. */
+ public enum AwbState {
+
+ /** AWB state is currently unknown. */
+ UNKNOWN,
+
+ /** AWB is not in auto mode, or has not yet started metering. */
+ INACTIVE,
+
+ /** AWB is performing AWB metering. */
+ METERING,
+
+ /** AWB has a good set of control values for the current scene. */
+ CONVERGED,
+
+ /** AWB has been locked. */
+ LOCKED
+ }
+
+ /** Flash state. */
+ public enum FlashState {
+
+ /** Flash state is unknown. */
+ UNKNOWN,
+
+ /** Flash is unavailable or not ready to fire. */
+ NONE,
+
+ /** Flash is ready to fire. */
+ READY,
+
+ /** Flash has been fired. */
+ FIRED
+ }
+}
diff --git a/camera/core/src/main/java/androidx/camera/core/CameraCaptureResult.java b/camera/core/src/main/java/androidx/camera/core/CameraCaptureResult.java
new file mode 100644
index 0000000..c31815c
--- /dev/null
+++ b/camera/core/src/main/java/androidx/camera/core/CameraCaptureResult.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright (C) 2019 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.core;
+
+import androidx.annotation.NonNull;
+import androidx.camera.core.CameraCaptureMetaData.AeState;
+import androidx.camera.core.CameraCaptureMetaData.AfMode;
+import androidx.camera.core.CameraCaptureMetaData.AfState;
+import androidx.camera.core.CameraCaptureMetaData.AwbState;
+import androidx.camera.core.CameraCaptureMetaData.FlashState;
+
+/**
+ * The result of a single image capture.
+ *
+ * @hide
+ */
+public interface CameraCaptureResult {
+
+ @NonNull
+ AfMode getAfMode();
+
+ @NonNull
+ AfState getAfState();
+
+ @NonNull
+ AeState getAeState();
+
+ @NonNull
+ AwbState getAwbState();
+
+ @NonNull
+ FlashState getFlashState();
+
+ /** An implementation of CameraCaptureResult which always return default results. */
+ final class EmptyCameraCaptureResult implements CameraCaptureResult {
+
+ public static CameraCaptureResult create() {
+ return new EmptyCameraCaptureResult();
+ }
+
+ @NonNull
+ @Override
+ public AfMode getAfMode() {
+ return AfMode.UNKNOWN;
+ }
+
+ @NonNull
+ @Override
+ public AfState getAfState() {
+ return AfState.UNKNOWN;
+ }
+
+ @NonNull
+ @Override
+ public AeState getAeState() {
+ return AeState.UNKNOWN;
+ }
+
+ @NonNull
+ @Override
+ public AwbState getAwbState() {
+ return AwbState.UNKNOWN;
+ }
+
+ @NonNull
+ @Override
+ public FlashState getFlashState() {
+ return FlashState.UNKNOWN;
+ }
+ }
+}
diff --git a/camera/core/src/main/java/androidx/camera/core/CameraCaptureSessionStateCallbacks.java b/camera/core/src/main/java/androidx/camera/core/CameraCaptureSessionStateCallbacks.java
new file mode 100644
index 0000000..2a55ec1
--- /dev/null
+++ b/camera/core/src/main/java/androidx/camera/core/CameraCaptureSessionStateCallbacks.java
@@ -0,0 +1,154 @@
+/*
+ * Copyright (C) 2019 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.core;
+
+import android.hardware.camera2.CameraCaptureSession;
+import android.os.Build;
+import android.view.Surface;
+
+import androidx.annotation.RequiresApi;
+import androidx.annotation.RestrictTo;
+import androidx.annotation.RestrictTo.Scope;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * Different implementations of {@link CameraCaptureSession.StateCallback}.
+ *
+ * @hide
+ */
+@RestrictTo(Scope.LIBRARY_GROUP)
+public final class CameraCaptureSessionStateCallbacks {
+ private CameraCaptureSessionStateCallbacks() {
+ }
+
+ /** Returns a session state callback which does nothing. */
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ public static CameraCaptureSession.StateCallback createNoOpCallback() {
+ return new NoOpSessionStateCallback();
+ }
+
+ /** Returns a session state callback which calls a list of other callbacks. */
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ public static CameraCaptureSession.StateCallback createComboCallback(
+ List<CameraCaptureSession.StateCallback> callbacks) {
+ return new ComboSessionStateCallback(callbacks);
+ }
+
+ /** Returns a session state callback which calls a list of other callbacks. */
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ public static CameraCaptureSession.StateCallback createComboCallback(
+ CameraCaptureSession.StateCallback... callbacks) {
+ return createComboCallback(Arrays.asList(callbacks));
+ }
+
+ static final class NoOpSessionStateCallback extends CameraCaptureSession.StateCallback {
+ @Override
+ public void onConfigured(CameraCaptureSession session) {
+ }
+
+ @Override
+ public void onActive(CameraCaptureSession session) {
+ }
+
+ @Override
+ public void onClosed(CameraCaptureSession session) {
+ }
+
+ @Override
+ public void onReady(CameraCaptureSession session) {
+ }
+
+ @Override
+ public void onCaptureQueueEmpty(CameraCaptureSession session) {
+ }
+
+ @Override
+ public void onSurfacePrepared(CameraCaptureSession session, Surface surface) {
+ }
+
+ @Override
+ public void onConfigureFailed(CameraCaptureSession session) {
+ }
+ }
+
+ private static final class ComboSessionStateCallback
+ extends CameraCaptureSession.StateCallback {
+ private final List<CameraCaptureSession.StateCallback> callbacks = new ArrayList<>();
+
+ ComboSessionStateCallback(List<CameraCaptureSession.StateCallback> callbacks) {
+ for (CameraCaptureSession.StateCallback callback : callbacks) {
+ // A no-op callback doesn't do anything, so avoid adding it to the final list.
+ if (!(callback instanceof NoOpSessionStateCallback)) {
+ this.callbacks.add(callback);
+ }
+ }
+ }
+
+ @Override
+ public void onConfigured(CameraCaptureSession session) {
+ for (CameraCaptureSession.StateCallback callback : callbacks) {
+ callback.onConfigured(session);
+ }
+ }
+
+ @Override
+ public void onActive(CameraCaptureSession session) {
+ for (CameraCaptureSession.StateCallback callback : callbacks) {
+ callback.onActive(session);
+ }
+ }
+
+ @Override
+ public void onClosed(CameraCaptureSession session) {
+ for (CameraCaptureSession.StateCallback callback : callbacks) {
+ callback.onClosed(session);
+ }
+ }
+
+ @Override
+ public void onReady(CameraCaptureSession session) {
+ for (CameraCaptureSession.StateCallback callback : callbacks) {
+ callback.onReady(session);
+ }
+ }
+
+ @RequiresApi(api = Build.VERSION_CODES.O)
+ @Override
+ public void onCaptureQueueEmpty(CameraCaptureSession session) {
+ for (CameraCaptureSession.StateCallback callback : callbacks) {
+ callback.onCaptureQueueEmpty(session);
+ }
+ }
+
+ @Override
+ public void onSurfacePrepared(CameraCaptureSession session, Surface surface) {
+ for (CameraCaptureSession.StateCallback callback : callbacks) {
+ callback.onSurfacePrepared(session, surface);
+ }
+ }
+
+ @Override
+ public void onConfigureFailed(CameraCaptureSession session) {
+ for (CameraCaptureSession.StateCallback callback : callbacks) {
+ callback.onConfigureFailed(session);
+ }
+ }
+ }
+}
diff --git a/camera/core/src/main/java/androidx/camera/core/CameraControl.java b/camera/core/src/main/java/androidx/camera/core/CameraControl.java
new file mode 100644
index 0000000..1798de7
--- /dev/null
+++ b/camera/core/src/main/java/androidx/camera/core/CameraControl.java
@@ -0,0 +1,171 @@
+/*
+ * Copyright (C) 2019 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.core;
+
+import android.graphics.Rect;
+import android.os.Handler;
+
+import androidx.annotation.Nullable;
+
+/**
+ * The CameraControl Interface.
+ *
+ * <p>CameraControl is used for global camera operations like zoom, focus, flash and triggering
+ * AF/AE. To control the global camera status across different UseCases,
+ * getSingleRequestImplOptions() is used to attach the common request parameter to all SINGLE
+ * CaptureRequests and getControlSessionConfiguration() is used to hook a {@link
+ * SessionConfiguration} alongside with other use cases to determine the final sessionConfiguration.
+ * A CameraControl implementation can use getControlSessionConfiguration to modify parameter for
+ * repeating request or add a listener to check the capture result.
+ *
+ * @hide
+ */
+public interface CameraControl {
+ static CameraControl defaultEmptyInstance() {
+ return new CameraControl() {
+ @Override
+ public void setCropRegion(Rect crop) {
+ }
+
+ @Override
+ public void focus(
+ Rect focus,
+ Rect metering,
+ @Nullable OnFocusCompletedListener listener,
+ @Nullable Handler handler) {
+ }
+
+ @Override
+ public FlashMode getFlashMode() {
+ return null;
+ }
+
+ @Override
+ public void setFlashMode(FlashMode flashMode) {
+ }
+
+ @Override
+ public void enableTorch(boolean torch) {
+ }
+
+ @Override
+ public boolean isTorchOn() {
+ return false;
+ }
+
+ @Override
+ public boolean isFocusLocked() {
+ return false;
+ }
+
+ @Override
+ public void triggerAf() {
+ }
+
+ @Override
+ public void triggerAePrecapture() {
+ }
+
+ @Override
+ public void cancelAfAeTrigger(
+ boolean cancelAfTrigger, boolean cancelAePrecaptureTrigger) {
+ }
+
+ @Override
+ public SessionConfiguration getControlSessionConfiguration() {
+ return SessionConfiguration.defaultEmptySessionConfiguration();
+ }
+
+ @Override
+ public Configuration getSingleRequestImplOptions() {
+ return OptionsBundle.emptyBundle();
+ }
+ };
+ }
+
+ /**
+ * Set the desired crop region of the sensor to read out for all capture requests.
+ *
+ * <p>This crop region can be used to implement digital zoom. It is applied to every single and
+ * re peating requests.
+ *
+ * @param crop rectangle with dimensions in sensor pixel coordinate.
+ */
+ void setCropRegion(Rect crop);
+
+ /**
+ * Adjusts the camera output according to the properties in some local regions with a callback
+ * called once focus scan has completed.
+ *
+ * <p>The auto-focus (AF), auto-exposure (AE) and auto-whitebalance (AWB) properties will be
+ * recalculated from the local regions.
+ *
+ * @param focus rectangle with dimensions in sensor coordinate frame for focus
+ * @param metering rectangle with dimensions in sensor coordinate frame for metering
+ * @param listener listener for when focus has completed
+ * @param handler the handler where the listener will execute.
+ */
+ void focus(
+ Rect focus,
+ Rect metering,
+ @Nullable OnFocusCompletedListener listener,
+ @Nullable Handler handler);
+
+ default void focus(Rect focus, Rect metering) {
+ focus(focus, metering, null, null);
+ }
+
+ FlashMode getFlashMode();
+
+ /**
+ * Sets current flash mode
+ *
+ * @param flashMode the {@link FlashMode}.
+ */
+ void setFlashMode(FlashMode flashMode);
+
+ /**
+ * Enable the torch or disable the torch
+ *
+ * @param torch true to open the torch, false to close it.
+ */
+ void enableTorch(boolean torch);
+
+ /** Returns if current torch is enabled or not. */
+ boolean isTorchOn();
+
+ boolean isFocusLocked();
+
+ /** Performs a AF trigger. */
+ void triggerAf();
+
+ /** Performs a AE Precapture trigger. */
+ void triggerAePrecapture();
+
+ /** Cancel AF trigger AND/OR AE Precapture trigger.* */
+ void cancelAfAeTrigger(boolean cancelAfTrigger, boolean cancelAePrecaptureTrigger);
+
+ /**
+ * Hooks a SessionConfiguration into the final SessionConfiguration ValidatingBuilder.
+ * CameraControl can modify SessionConfiguration to add implementation options or add a listener
+ * to check the capture result.
+ */
+ SessionConfiguration getControlSessionConfiguration();
+
+ /** Attaches the common request implementation options to every SINGLE requests. */
+ Configuration getSingleRequestImplOptions();
+}
diff --git a/camera/core/src/main/java/androidx/camera/core/CameraDeviceConfiguration.java b/camera/core/src/main/java/androidx/camera/core/CameraDeviceConfiguration.java
new file mode 100644
index 0000000..6895d2c
--- /dev/null
+++ b/camera/core/src/main/java/androidx/camera/core/CameraDeviceConfiguration.java
@@ -0,0 +1,88 @@
+/*
+ * Copyright (C) 2019 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.core;
+
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.annotation.RestrictTo.Scope;
+import androidx.camera.core.CameraX.LensFacing;
+
+/**
+ * Configuration containing options for configuring a Camera device.
+ *
+ * <p>This includes options for camera device intrinsics, such as the lens facing direction.
+ */
+public interface CameraDeviceConfiguration extends Configuration.Reader {
+
+ /**
+ * Option: camerax.core.camera.lensFacing
+ *
+ * @hide
+ */
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ Option<LensFacing> OPTION_LENS_FACING =
+ Option.create("camerax.core.camera.lensFacing", CameraX.LensFacing.class);
+
+ /**
+ * Retrieves the lens facing direction for the primary camera to be configured.
+ *
+ * @param valueIfMissing The value to return if this configuration option has not been set.
+ * @return The stored value or <code>valueIfMissing</code> if the value does not exist in this
+ * configuration.
+ */
+ @Nullable
+ default CameraX.LensFacing getLensFacing(@Nullable LensFacing valueIfMissing) {
+ return retrieveOption(OPTION_LENS_FACING, valueIfMissing);
+ }
+
+ /**
+ * Retrieves the lens facing direction for the primary camera to be configured.
+ *
+ * @return The stored value, if it exists in this configuration.
+ * @throws IllegalArgumentException if the option does not exist in this configuration.
+ */
+ default CameraX.LensFacing getLensFacing() {
+ return retrieveOption(OPTION_LENS_FACING);
+ }
+
+ // Option Declarations:
+ // ***********************************************************************************************
+
+ /**
+ * Builder for a {@link CameraDeviceConfiguration}.
+ *
+ * @param <C> The top level configuration which will be generated by {@link #build()}.
+ * @param <B> The top level builder type for which this builder is composed with.
+ */
+ interface Builder<C extends Configuration, B extends Builder<C, B>>
+ extends Configuration.Builder<C, B> {
+
+ /**
+ * Sets the primary camera to be configured based on the direction the lens is facing.
+ *
+ * <p>If multiple cameras exist with equivalent lens facing direction, the first ("primary")
+ * camera for that direction will be chosen.
+ *
+ * @param lensFacing The direction of the camera's lens.
+ * @return the current Builder.
+ */
+ default B setLensFacing(CameraX.LensFacing lensFacing) {
+ getMutableConfiguration().insertOption(OPTION_LENS_FACING, lensFacing);
+ return builder();
+ }
+ }
+}
diff --git a/camera/core/src/main/java/androidx/camera/core/CameraDeviceStateCallbacks.java b/camera/core/src/main/java/androidx/camera/core/CameraDeviceStateCallbacks.java
new file mode 100644
index 0000000..9831938
--- /dev/null
+++ b/camera/core/src/main/java/androidx/camera/core/CameraDeviceStateCallbacks.java
@@ -0,0 +1,116 @@
+/*
+ * Copyright (C) 2019 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.core;
+
+import android.hardware.camera2.CameraDevice;
+
+import androidx.annotation.RestrictTo;
+import androidx.annotation.RestrictTo.Scope;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * Different implementations of {@link CameraDevice.StateCallback}.
+ *
+ * @hide
+ */
+@RestrictTo(Scope.LIBRARY_GROUP)
+public final class CameraDeviceStateCallbacks {
+ private CameraDeviceStateCallbacks() {
+ }
+
+ /** Returns a device state callback which does nothing. */
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ public static CameraDevice.StateCallback createNoOpCallback() {
+ return new NoOpDeviceStateCallback();
+ }
+
+ /** Returns a device state callback which calls a list of other callbacks. */
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ public static CameraDevice.StateCallback createComboCallback(
+ List<CameraDevice.StateCallback> callbacks) {
+ return new ComboDeviceStateCallback(callbacks);
+ }
+
+ /** Returns a device state callback which calls a list of other callbacks. */
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ public static CameraDevice.StateCallback createComboCallback(
+ CameraDevice.StateCallback... callbacks) {
+ return createComboCallback(Arrays.asList(callbacks));
+ }
+
+ static final class NoOpDeviceStateCallback extends CameraDevice.StateCallback {
+ @Override
+ public void onOpened(CameraDevice cameraDevice) {
+ }
+
+ @Override
+ public void onClosed(CameraDevice cameraDevice) {
+ }
+
+ @Override
+ public void onDisconnected(CameraDevice cameraDevice) {
+ }
+
+ @Override
+ public void onError(CameraDevice cameraDevice, int error) {
+ }
+ }
+
+ private static final class ComboDeviceStateCallback extends CameraDevice.StateCallback {
+ private final List<CameraDevice.StateCallback> callbacks = new ArrayList<>();
+
+ ComboDeviceStateCallback(List<CameraDevice.StateCallback> callbacks) {
+ for (CameraDevice.StateCallback callback : callbacks) {
+ // A no-op callback doesn't do anything, so avoid adding it to the final list.
+ if (!(callback instanceof NoOpDeviceStateCallback)) {
+ this.callbacks.add(callback);
+ }
+ }
+ }
+
+ @Override
+ public void onOpened(CameraDevice cameraDevice) {
+ for (CameraDevice.StateCallback callback : callbacks) {
+ callback.onOpened(cameraDevice);
+ }
+ }
+
+ @Override
+ public void onClosed(CameraDevice cameraDevice) {
+ for (CameraDevice.StateCallback callback : callbacks) {
+ callback.onClosed(cameraDevice);
+ }
+ }
+
+ @Override
+ public void onDisconnected(CameraDevice cameraDevice) {
+ for (CameraDevice.StateCallback callback : callbacks) {
+ callback.onDisconnected(cameraDevice);
+ }
+ }
+
+ @Override
+ public void onError(CameraDevice cameraDevice, int error) {
+ for (CameraDevice.StateCallback callback : callbacks) {
+ callback.onError(cameraDevice, error);
+ }
+ }
+ }
+}
diff --git a/camera/core/src/main/java/androidx/camera/core/CameraDeviceSurfaceManager.java b/camera/core/src/main/java/androidx/camera/core/CameraDeviceSurfaceManager.java
new file mode 100644
index 0000000..ea2e8a0
--- /dev/null
+++ b/camera/core/src/main/java/androidx/camera/core/CameraDeviceSurfaceManager.java
@@ -0,0 +1,80 @@
+/*
+ * Copyright (C) 2019 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.core;
+
+import android.util.Size;
+
+import androidx.annotation.Nullable;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Camera device manager to provide the guaranteed supported stream capabilities related info for
+ * all camera devices
+ *
+ * @hide
+ */
+public interface CameraDeviceSurfaceManager {
+ /**
+ * Check whether the input surface configuration list is under the capability of any combination
+ * of this object.
+ *
+ * @param cameraId the camera id of the camera device to be compared
+ * @param surfaceConfigurationList the surface configuration list to be compared
+ * @return the check result that whether it could be supported
+ */
+ boolean checkSupported(String cameraId, List<SurfaceConfiguration> surfaceConfigurationList);
+
+ /**
+ * Transform to a SurfaceConfiguration object with cameraId, image format and size info
+ *
+ * @param cameraId the camera id of the camera device to transform the object
+ * @param imageFormat the image format info for the surface configuration object
+ * @param size the size info for the surface configuration object
+ * @return new {@link SurfaceConfiguration} object
+ */
+ SurfaceConfiguration transformSurfaceConfiguration(String cameraId, int imageFormat, Size size);
+
+ /**
+ * Get max supported output size for specific camera device and image format
+ *
+ * @param cameraId the camera Id
+ * @param imageFormat the image format info
+ * @return the max supported output size for the image format
+ */
+ @Nullable
+ Size getMaxOutputSize(String cameraId, int imageFormat);
+
+ /**
+ * Retrieves a map of suggested resolutions for the given list of use cases.
+ *
+ * @param cameraId the camera id of the camera device used by the use cases
+ * @param originalUseCases list of use cases with existing surfaces
+ * @param newUseCases list of new use cases
+ * @return map of suggested resolutions for given use cases
+ */
+ Map<BaseUseCase, Size> getSuggestedResolutions(
+ String cameraId, List<BaseUseCase> originalUseCases, List<BaseUseCase> newUseCases);
+
+ /**
+ * Retrieves the preview size, choosing the smaller of the display size and 1080P.
+ *
+ * @return the size used for the on screen preview
+ */
+ Size getPreviewSize();
+}
diff --git a/camera/core/src/main/java/androidx/camera/core/CameraFactory.java b/camera/core/src/main/java/androidx/camera/core/CameraFactory.java
new file mode 100644
index 0000000..21767af
--- /dev/null
+++ b/camera/core/src/main/java/androidx/camera/core/CameraFactory.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2019 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.core;
+
+import androidx.annotation.Nullable;
+import androidx.camera.core.CameraX.LensFacing;
+
+import java.util.Set;
+
+/**
+ * The factory class that creates {@link BaseCamera} instances.
+ *
+ * @hide
+ */
+public interface CameraFactory {
+
+ BaseCamera getCamera(String cameraId);
+
+ Set<String> getAvailableCameraIds() throws CameraInfoUnavailableException;
+
+ @Nullable
+ String cameraIdForLensFacing(LensFacing lensFacing) throws CameraInfoUnavailableException;
+}
diff --git a/camera/core/src/main/java/androidx/camera/core/CameraInfo.java b/camera/core/src/main/java/androidx/camera/core/CameraInfo.java
new file mode 100644
index 0000000..f7f6a4d
--- /dev/null
+++ b/camera/core/src/main/java/androidx/camera/core/CameraInfo.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright (C) 2019 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.core;
+
+import android.view.Surface;
+
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.annotation.RestrictTo.Scope;
+import androidx.camera.core.CameraX.LensFacing;
+import androidx.camera.core.ImageOutputConfiguration.RotationValue;
+
+/**
+ * An interface for retrieving camera information.
+ *
+ * <p>Contains methods for retrieving characteristics for a specific camera.
+ *
+ * @hide
+ */
+@RestrictTo(Scope.LIBRARY_GROUP)
+public interface CameraInfo {
+
+ /**
+ * Returns the LensFacing of this camera.
+ *
+ * @return One of {@link LensFacing#FRONT}, {@link LensFacing#BACK}, or <code>null</code> if the
+ * LensFacing does not fall into one of these two categories.
+ */
+ // TODO(b/122975195): Remove @Nullable and null return type once we have a LensFacing type which
+ // can be used to represent non-BACK or FRONT facing lenses.
+ @Nullable
+ LensFacing getLensFacing();
+
+ /**
+ * Returns the sensor rotation, in degrees, relative to the device's "natural" rotation.
+ *
+ * @return The sensor orientation in degrees.
+ * @see Surface#ROTATION_0, the natural orientation of the device.
+ */
+ default int getSensorRotationDegrees() {
+ return getSensorRotationDegrees(Surface.ROTATION_0);
+ }
+
+ /**
+ * Returns the sensor rotation, in degrees, relative to the given rotation value.
+ *
+ * <p>Valid values for the relative rotation are {@link Surface#ROTATION_0}, {@link
+ * Surface#ROTATION_90}, {@link Surface#ROTATION_180}, {@link Surface#ROTATION_270}.
+ *
+ * @param relativeRotation The rotation relative to which the output will be calculated.
+ * @return The sensor orientation in degrees.
+ */
+ int getSensorRotationDegrees(@RotationValue int relativeRotation);
+}
diff --git a/camera/core/src/main/java/androidx/camera/core/CameraInfoUnavailableException.java b/camera/core/src/main/java/androidx/camera/core/CameraInfoUnavailableException.java
new file mode 100644
index 0000000..f3f4cc0
--- /dev/null
+++ b/camera/core/src/main/java/androidx/camera/core/CameraInfoUnavailableException.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2019 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.core;
+
+import androidx.annotation.RestrictTo;
+import androidx.annotation.RestrictTo.Scope;
+
+/** An exception thrown when unable to retrieve information about a camera. */
+public final class CameraInfoUnavailableException extends Exception {
+ /** @hide */
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ public CameraInfoUnavailableException(String s, Throwable e) {
+ super(s, e);
+ }
+
+ /** @hide */
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ public CameraInfoUnavailableException(String s) {
+ super(s);
+ }
+}
diff --git a/camera/core/src/main/java/androidx/camera/core/CameraOrientationUtil.java b/camera/core/src/main/java/androidx/camera/core/CameraOrientationUtil.java
new file mode 100644
index 0000000..2b54594
--- /dev/null
+++ b/camera/core/src/main/java/androidx/camera/core/CameraOrientationUtil.java
@@ -0,0 +1,105 @@
+/*
+ * Copyright (C) 2019 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.core;
+
+import android.util.Log;
+import android.view.Surface;
+
+import androidx.annotation.RestrictTo;
+import androidx.annotation.RestrictTo.Scope;
+import androidx.camera.core.ImageOutputConfiguration.RotationValue;
+
+/**
+ * Contains utility methods related to camera orientation.
+ *
+ * @hide
+ */
+@RestrictTo(Scope.LIBRARY_GROUP)
+public final class CameraOrientationUtil {
+ private static final String TAG = "CameraOrientationUtil";
+ private static final boolean DEBUG = false;
+
+ // Do not allow instantiation
+ private CameraOrientationUtil() {
+ }
+
+ /**
+ * Calculates the delta between a source rotation and destination rotation.
+ *
+ * <p>A typical use of this method would be calculating the angular difference between the
+ * display orientation (destRotationDegrees) and camera sensor orientation
+ * (sourceRotationDegrees).
+ *
+ * @param destRotationDegrees The destination rotation relative to the device's natural
+ * rotation.
+ * @param sourceRotationDegrees The source rotation relative to the device's natural rotation.
+ * @param isOppositeFacing Whether the source and destination planes are facing opposite
+ * directions.
+ */
+ public static int getRelativeImageRotation(
+ int destRotationDegrees, int sourceRotationDegrees, boolean isOppositeFacing) {
+ int result;
+ if (isOppositeFacing) {
+ result = (sourceRotationDegrees - destRotationDegrees + 360) % 360;
+ } else {
+ result = (sourceRotationDegrees + destRotationDegrees) % 360;
+ }
+ if (DEBUG) {
+ Log.d(
+ TAG,
+ String.format(
+ "getRelativeImageRotation: destRotationDegrees=%s, "
+ + "sourceRotationDegrees=%s, isOppositeFacing=%s, "
+ + "result=%s",
+ destRotationDegrees, sourceRotationDegrees, isOppositeFacing, result));
+ }
+ return result;
+ }
+
+ /**
+ * Converts rotation values enumerated in {@link Surface} to their equivalent in degrees.
+ *
+ * <p>Valid values for the relative rotation are {@link Surface#ROTATION_0}, {@link
+ * Surface#ROTATION_90}, {@link Surface#ROTATION_180}, {@link Surface#ROTATION_270}.
+ *
+ * @param rotationEnum One of the enumerated rotation values from {@link Surface}.
+ * @return The equivalent rotation value in degrees.
+ * @throws IllegalArgumentException If the provided rotation enum is not one of those defined in
+ * {@link Surface}.
+ */
+ public static int surfaceRotationToDegrees(@RotationValue int rotationEnum) {
+ int rotationDegrees;
+ switch (rotationEnum) {
+ case Surface.ROTATION_0:
+ rotationDegrees = 0;
+ break;
+ case Surface.ROTATION_90:
+ rotationDegrees = 90;
+ break;
+ case Surface.ROTATION_180:
+ rotationDegrees = 180;
+ break;
+ case Surface.ROTATION_270:
+ rotationDegrees = 270;
+ break;
+ default:
+ throw new IllegalArgumentException("Unsupported surface rotation: " + rotationEnum);
+ }
+
+ return rotationDegrees;
+ }
+}
diff --git a/camera/core/src/main/java/androidx/camera/core/CameraRepository.java b/camera/core/src/main/java/androidx/camera/core/CameraRepository.java
new file mode 100644
index 0000000..04ee948
--- /dev/null
+++ b/camera/core/src/main/java/androidx/camera/core/CameraRepository.java
@@ -0,0 +1,142 @@
+/*
+ * Copyright (C) 2019 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.core;
+
+import android.content.Context;
+import android.util.Log;
+
+import androidx.annotation.GuardedBy;
+import androidx.annotation.RestrictTo;
+import androidx.annotation.RestrictTo.Scope;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * A collection of {@link BaseCamera} instances.
+ *
+ * @hide
+ */
+@RestrictTo(Scope.LIBRARY_GROUP)
+public final class CameraRepository implements UseCaseGroup.StateChangeListener {
+ private static final String TAG = "CameraRepository";
+
+ private final Object camerasLock = new Object();
+
+ @GuardedBy("camerasLock")
+ private final Map<String, BaseCamera> cameras = new HashMap<>();
+
+ /**
+ * Initializes the repository from a {@link Context}.
+ *
+ * <p>All cameras queried from the {@link CameraFactory} will be added to the repository.
+ */
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ public void init(CameraFactory cameraFactory) {
+ synchronized (camerasLock) {
+ try {
+ Set<String> camerasList = cameraFactory.getAvailableCameraIds();
+ for (String id : camerasList) {
+ Log.d(TAG, "Added camera: " + id);
+ cameras.put(id, cameraFactory.getCamera(id));
+ }
+ } catch (Exception e) {
+ throw new IllegalStateException("Unable to enumerate cameras", e);
+ }
+ }
+ }
+
+ /**
+ * Gets a {@link BaseCamera} for the given id.
+ *
+ * @param cameraId id for the camera
+ * @return a {@link BaseCamera} paired to this id
+ * @throws IllegalArgumentException if there is no camera paired with the id
+ */
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ public BaseCamera getCamera(String cameraId) {
+ synchronized (camerasLock) {
+ BaseCamera camera = cameras.get(cameraId);
+
+ if (camera == null) {
+ throw new IllegalArgumentException("Invalid camera: " + cameraId);
+ }
+
+ return camera;
+ }
+ }
+
+ /**
+ * Gets the set of all camera ids.
+ *
+ * @return set of all camera ids
+ */
+ Set<String> getCameraIds() {
+ synchronized (camerasLock) {
+ return Collections.unmodifiableSet(cameras.keySet());
+ }
+ }
+
+ /**
+ * Attaches all the use cases in the {@link UseCaseGroup} and opens all the associated cameras.
+ *
+ * <p>This will start streaming data to the uses cases which are also online.
+ */
+ @Override
+ public void onGroupActive(UseCaseGroup useCaseGroup) {
+ synchronized (camerasLock) {
+ Map<String, Set<BaseUseCase>> cameraIdToUseCaseMap =
+ useCaseGroup.getCameraIdToUseCaseMap();
+ for (Map.Entry<String, Set<BaseUseCase>> cameraUseCaseEntry :
+ cameraIdToUseCaseMap.entrySet()) {
+ BaseCamera camera = getCamera(cameraUseCaseEntry.getKey());
+ attachUseCasesToCamera(camera, cameraUseCaseEntry.getValue());
+ }
+ }
+ }
+
+ /** Attaches a set of use cases to a camera. */
+ @GuardedBy("camerasLock")
+ private void attachUseCasesToCamera(BaseCamera camera, Set<BaseUseCase> baseUseCases) {
+ camera.addOnlineUseCase(baseUseCases);
+ }
+
+ /**
+ * Detaches all the use cases in the {@link UseCaseGroup} and closes the camera with no attached
+ * use cases.
+ */
+ @Override
+ public void onGroupInactive(UseCaseGroup useCaseGroup) {
+ synchronized (camerasLock) {
+ Map<String, Set<BaseUseCase>> cameraIdToUseCaseMap =
+ useCaseGroup.getCameraIdToUseCaseMap();
+ for (Map.Entry<String, Set<BaseUseCase>> cameraUseCaseEntry :
+ cameraIdToUseCaseMap.entrySet()) {
+ BaseCamera camera = getCamera(cameraUseCaseEntry.getKey());
+ detachUseCasesFromCamera(camera, cameraUseCaseEntry.getValue());
+ }
+ }
+ }
+
+ /** Detaches a set of use cases from a camera. */
+ @GuardedBy("camerasLock")
+ private void detachUseCasesFromCamera(BaseCamera camera, Set<BaseUseCase> baseUseCases) {
+ camera.removeOnlineUseCase(baseUseCases);
+ }
+}
diff --git a/camera/core/src/main/java/androidx/camera/core/CameraX.java b/camera/core/src/main/java/androidx/camera/core/CameraX.java
new file mode 100644
index 0000000..6004119
--- /dev/null
+++ b/camera/core/src/main/java/androidx/camera/core/CameraX.java
@@ -0,0 +1,530 @@
+/*
+ * Copyright (C) 2019 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.core;
+
+import android.content.Context;
+import android.os.Handler;
+import android.util.Size;
+
+import androidx.annotation.MainThread;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.annotation.RestrictTo.Scope;
+import androidx.lifecycle.LifecycleOwner;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+/**
+ * Main interface for accessing CameraX library.
+ *
+ * <p>This is a singleton class that is responsible for managing the set of {@link BaseCamera}
+ * instances and {@link BaseUseCase} instances that exist. A {@link BaseUseCase} is bound to {@link
+ * LifecycleOwner} so that the lifecycle is used to control the use case. There are 3 distinct sets
+ * lifecycle states to be aware of.
+ *
+ * <p>When the lifecycle is in the STARTED or RESUMED states the cameras are opened asynchronously
+ * and made ready for capturing. Data capture starts when triggered by the bound {@link
+ * BaseUseCase}.
+ *
+ * <p>When the lifecycle is in the CREATED state any cameras with no {@link BaseUseCase} attached
+ * will be closed asynchronously.
+ *
+ * <p>When the lifecycle transitions to the DESTROYED state the {@link BaseUseCase} will be unbound.
+ * A {@link #bindToLifecycle(LifecycleOwner, BaseUseCase...)} when the lifecycle is already in the
+ * DESTROYED state will fail. A call to {@link #bindToLifecycle(LifecycleOwner, BaseUseCase...)}
+ * will need to be made with another lifecycle to rebind the {@link BaseUseCase} that has been
+ * unbound.
+ *
+ * <pre>{@code
+ * public void setup() {
+ * // Initialize UseCase
+ * useCase = ...;
+ *
+ * // UseCase binding event
+ * CameraX.bindToLifecycle(lifecycleOwner, useCase);
+ *
+ * // Make calls on useCase
+ * }
+ *
+ * public void operateOnUseCase() {
+ * if (CameraX.isBound(useCase)) {
+ * // Make calls on useCase
+ * }
+ * }
+ *
+ * public void prematureTearDown() {
+ * // Not required, but only if we want to remove it before the lifecycle automatically removes
+ * // the use case
+ * CameraX.unbind(useCase);
+ * }
+ * }</pre>
+ *
+ * <p>All operations on a use case, including binding and unbinding, should be done on the main
+ * thread, because lifecycle events are triggered on main thread. By only accessing the use case on
+ * the main thread it is a guaranteed that the use case will not be unbound in the middle of a
+ * method call.
+ */
+@MainThread
+public final class CameraX {
+
+ private static final CameraX INSTANCE = new CameraX();
+ private final CameraRepository cameraRepository = new CameraRepository();
+ private final AtomicBoolean initialized = new AtomicBoolean(false);
+ private final UseCaseGroupRepository useCaseGroupRepository = new UseCaseGroupRepository();
+ private final ErrorHandler errorHandler = new ErrorHandler();
+ private CameraFactory cameraFactory;
+ private CameraDeviceSurfaceManager surfaceManager;
+ private UseCaseConfigurationFactory defaultConfigFactory;
+ /** Prevents construction. */
+ private CameraX() {
+ }
+
+ /**
+ * Binds the collection of {@link BaseUseCase} to a {@link LifecycleOwner}.
+ *
+ * <p>If the lifecycleOwner contains a {@link android.arch.lifecycle.Lifecycle} that is already
+ * in the STARTED state or greater than the created use cases will attach to the cameras and
+ * trigger the appropriate notifications. This will generally cause a temporary glitch in the
+ * camera as part of the reset process. This will also help to calculate suggested resolutions
+ * depending on the use cases bound to the {@link android.arch.lifecycle.Lifecycle}. If the use
+ * cases are bound separately, it will find the supported resolution with the priority depending
+ * on the binding sequence. If the use cases are bound with a single call, it will find the
+ * supported resolution with the priority in sequence of ImageCaptureUseCase,
+ * VideoCaptureUseCase, ViewFinderUseCase and then ImageAnalysisUseCase. What resolutions can be
+ * supported will depend on the camera device hardware level that there are some default
+ * guaranteed resolutions listed in {@link
+ * android.hardware.camera2.CameraDevice#createCaptureSession}.
+ *
+ * @param lifecycleOwner The lifecycleOwner which controls the lifecycle transitions of the use
+ * cases.
+ * @param useCases The use cases to bind to a lifecycle.
+ * @throws IllegalArgumentException If the use case has already been bound to another lifecycle.
+ */
+ public static void bindToLifecycle(LifecycleOwner lifecycleOwner, BaseUseCase... useCases) {
+ UseCaseGroupLifecycleController useCaseGroupLifecycleController =
+ INSTANCE.getOrCreateUseCaseGroup(lifecycleOwner);
+ UseCaseGroup useCaseGroupToBind = useCaseGroupLifecycleController.getUseCaseGroup();
+
+ Collection<UseCaseGroupLifecycleController> controllers =
+ INSTANCE.useCaseGroupRepository.getUseCaseGroups();
+ for (BaseUseCase useCase : useCases) {
+ for (UseCaseGroupLifecycleController controller : controllers) {
+ UseCaseGroup useCaseGroup = controller.getUseCaseGroup();
+ if (useCaseGroup.contains(useCase) && useCaseGroup != useCaseGroupToBind) {
+ throw new IllegalStateException(
+ String.format(
+ "Use case %s already bound to a different lifecycle.",
+ useCase));
+ }
+ }
+ }
+
+ calculateSuggestedResolutions(useCases);
+
+ for (BaseUseCase useCase : useCases) {
+ useCaseGroupToBind.addUseCase(useCase);
+ for (String cameraId : useCase.getAttachedCameraIds()) {
+ attach(cameraId, useCase);
+ }
+ }
+
+ useCaseGroupLifecycleController.notifyState();
+ }
+
+ /**
+ * Returns true if the {@link BaseUseCase} is bound to a lifecycle. Otherwise returns false.
+ *
+ * <p>It is not strictly necessary to check if a use case is bound or not. As long as the
+ * lifecycle it was bound to has not entered a DESTROYED state or if it hasn't been unbound by
+ * {@link #unbind(BaseUseCase...)} or {@link #unbindAll()} then the use case will remain bound.
+ * A use case will not be unbound in the middle of a method call as long as it is running on the
+ * main thread. This is because a lifecycle events will only automatically triggered on the main
+ * thread.
+ */
+ public static boolean isBound(BaseUseCase useCase) {
+ Collection<UseCaseGroupLifecycleController> controllers =
+ INSTANCE.useCaseGroupRepository.getUseCaseGroups();
+
+ for (UseCaseGroupLifecycleController controller : controllers) {
+ UseCaseGroup useCaseGroup = controller.getUseCaseGroup();
+ if (useCaseGroup.contains(useCase)) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Unbinds all specified use cases from the lifecycle and removes them from CameraX.
+ *
+ * <p>This will initiate a close of every open camera which has zero {@link BaseUseCase}
+ * associated with it at the end of this call.
+ *
+ * <p>If a use case in the argument list is not bound, then then it is simply ignored.
+ *
+ * @param useCases The collection of use cases to remove.
+ */
+ public static void unbind(BaseUseCase... useCases) {
+ Collection<UseCaseGroupLifecycleController> useCaseGroups =
+ INSTANCE.useCaseGroupRepository.getUseCaseGroups();
+
+ for (BaseUseCase useCase : useCases) {
+ for (UseCaseGroupLifecycleController useCaseGroupLifecycleController : useCaseGroups) {
+ UseCaseGroup useCaseGroup = useCaseGroupLifecycleController.getUseCaseGroup();
+ if (useCaseGroup.removeUseCase(useCase)) {
+ for (String cameraId : useCase.getAttachedCameraIds()) {
+ detach(cameraId, useCase);
+ }
+
+ useCase.clear();
+ }
+ }
+ }
+ }
+
+ /**
+ * Unbinds all use cases from the lifecycle and removes them from CameraX.
+ *
+ * <p>This will initiate a close of every currently open camera.
+ */
+ public static void unbindAll() {
+ Collection<UseCaseGroupLifecycleController> useCaseGroups =
+ INSTANCE.useCaseGroupRepository.getUseCaseGroups();
+
+ List<BaseUseCase> useCases = new ArrayList<>();
+ for (UseCaseGroupLifecycleController useCaseGroupLifecycleController : useCaseGroups) {
+ UseCaseGroup useCaseGroup = useCaseGroupLifecycleController.getUseCaseGroup();
+ useCases.addAll(useCaseGroup.getUseCases());
+ }
+
+ unbind(useCases.toArray(new BaseUseCase[0]));
+ }
+
+ /**
+ * Returns the camera id for a camera with the specified lens facing.
+ *
+ * <p>This only gives the first (primary) camera found with the specified facing.
+ *
+ * @param lensFacing the lens facing of the camera
+ * @return the cameraId if camera exists or {@code null} if no camera with specified facing
+ * exists
+ * @throws CameraInfoUnavailableException if unable to access cameras, perhaps due to
+ * insufficient permissions.
+ * @hide
+ */
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ @Nullable
+ public static String getCameraWithLensFacing(LensFacing lensFacing)
+ throws CameraInfoUnavailableException {
+ return INSTANCE.getCameraFactory().cameraIdForLensFacing(lensFacing);
+ }
+
+ /**
+ * Returns the camera info for the camera with the given camera id.
+ *
+ * @param cameraId the internal id of the camera
+ * @return the camera info if it can be retrieved for the given id.
+ * @throws CameraInfoUnavailableException if unable to access cameras, perhaps due to
+ * insufficient permissions.
+ * @hide
+ */
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ @Nullable
+ public static CameraInfo getCameraInfo(String cameraId) throws CameraInfoUnavailableException {
+ return INSTANCE.getCameraRepository().getCamera(cameraId).getCameraInfo();
+ }
+
+ /**
+ * Returns the {@link CameraDeviceSurfaceManager} which can be used to query for valid surface
+ * configurations.
+ *
+ * @hide
+ */
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ public static CameraDeviceSurfaceManager getSurfaceManager() {
+ return INSTANCE.getCameraDeviceSurfaceManager();
+ }
+
+ /**
+ * Returns the default configuration for the given use case configuration type.
+ *
+ * <p>The options contained in this configuration serve as fallbacks if they are not included in
+ * the user-provided configuration used to create a use case.
+ *
+ * @param configType the configuration type
+ * @return the default configuration for the given configuration type
+ * @throws IllegalStateException if Camerax has not yet been initialized.
+ * @hide
+ */
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ @Nullable
+ public static <C extends UseCaseConfiguration<?>> C getDefaultUseCaseConfiguration(
+ Class<C> configType) {
+ return INSTANCE.getDefaultConfigFactory().getConfiguration(configType);
+ }
+
+ /**
+ * Sets an {@link ErrorListener} which will get called anytime a CameraX specific error is
+ * encountered.
+ *
+ * @param errorListener the listener which will get all the error messages. If this is set to
+ * {@code null} then the default error listener will be set.
+ * @param handler the handler for the thread to run the error handling on. If this is
+ * set to
+ * {@code null} then it will default to run on the main thread.
+ */
+ public static void setErrorListener(ErrorListener errorListener, Handler handler) {
+ INSTANCE.errorHandler.setErrorListener(errorListener, handler);
+ }
+
+ /**
+ * Posts an error which can be handled by the {@link ErrorListener}.
+ *
+ * @param errorCode the type of error that occurred
+ * @param message the associated message with more details of the error
+ * @hide
+ */
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ public static void postError(ErrorCode errorCode, String message) {
+ INSTANCE.errorHandler.postError(errorCode, message);
+ }
+
+ /**
+ * Initializes CameraX with the given context and application configuration.
+ *
+ * <p>The context enables CameraX to obtain access to necessary services, including the camera
+ * service. For example, the context can be provided by the application.
+ *
+ * @param context to attach
+ * @param appConfiguration configuration options for this application session.
+ * @hide
+ */
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ public static void init(Context context, AppConfiguration appConfiguration) {
+ INSTANCE.initInternal(context, appConfiguration);
+ }
+
+ /**
+ * Returns true if CameraX is initialized.
+ *
+ * <p>Any previous call to {@link #init(Context, AppConfiguration)} would have initialized
+ * CameraX.
+ */
+ static boolean isInitialized() {
+ return INSTANCE.initialized.get();
+ }
+
+ /**
+ * Registers the callbacks for the {@link BaseCamera} to the {@link BaseUseCase}.
+ *
+ * @param cameraId the id for the {@link BaseCamera}
+ * @param useCase the use case to register the callback for
+ */
+ private static void attach(String cameraId, BaseUseCase useCase) {
+ BaseCamera camera = INSTANCE.getCameraRepository().getCamera(cameraId);
+ if (camera == null) {
+ throw new IllegalArgumentException("Invalid camera: " + cameraId);
+ } else {
+ useCase.addStateChangeListener(camera);
+ useCase.attachCameraControl(cameraId, camera.getCameraControl());
+ }
+ }
+
+ /**
+ * Removes the callbacks registered by the {@link BaseCamera} to the {@link BaseUseCase}.
+ *
+ * @param cameraId the id for the {@link BaseCamera}
+ * @param useCase the use case to remove the callback from
+ */
+ private static void detach(String cameraId, BaseUseCase useCase) {
+ BaseCamera camera = INSTANCE.getCameraRepository().getCamera(cameraId);
+ if (camera == null) {
+ throw new IllegalArgumentException("Invalid camera: " + cameraId);
+ } else {
+ useCase.notifyInactive();
+ useCase.removeStateChangeListener(camera);
+ camera.removeOnlineUseCase(Collections.singletonList(useCase));
+ useCase.detachCameraControl(cameraId);
+ }
+ }
+
+ private static void calculateSuggestedResolutions(BaseUseCase... useCases) {
+ Collection<UseCaseGroupLifecycleController> controllers =
+ INSTANCE.useCaseGroupRepository.getUseCaseGroups();
+ Map<String, List<BaseUseCase>> originalCameraIdUseCaseMap = new HashMap<>();
+ Map<String, List<BaseUseCase>> newCameraIdUseCaseMap = new HashMap<>();
+
+ // Collect original use cases for different camera devices
+ for (UseCaseGroupLifecycleController controller : controllers) {
+ UseCaseGroup useCaseGroup = controller.getUseCaseGroup();
+ for (BaseUseCase useCase : useCaseGroup.getUseCases()) {
+ for (String cameraId : useCase.getAttachedCameraIds()) {
+ List<BaseUseCase> useCaseList = originalCameraIdUseCaseMap.get(cameraId);
+ if (useCaseList == null) {
+ useCaseList = new ArrayList<>();
+ originalCameraIdUseCaseMap.put(cameraId, useCaseList);
+ }
+ useCaseList.add(useCase);
+ }
+ }
+ }
+
+ // Collect new use cases for different camera devices
+ for (BaseUseCase useCase : useCases) {
+ String cameraId = null;
+ LensFacing lensFacing =
+ useCase.getUseCaseConfiguration()
+ .retrieveOption(CameraDeviceConfiguration.OPTION_LENS_FACING);
+ try {
+ cameraId = getCameraWithLensFacing(lensFacing);
+ } catch (Exception e) {
+ throw new IllegalArgumentException("Invalid camera lens facing: " + lensFacing, e);
+ }
+
+ List<BaseUseCase> useCaseList = newCameraIdUseCaseMap.get(cameraId);
+ if (useCaseList == null) {
+ useCaseList = new ArrayList<>();
+ newCameraIdUseCaseMap.put(cameraId, useCaseList);
+ }
+ useCaseList.add(useCase);
+ }
+
+ // Get suggested resolutions and update the use case session configuration
+ for (String cameraId : newCameraIdUseCaseMap.keySet()) {
+ Map<BaseUseCase, Size> suggestResolutionsMap =
+ getSurfaceManager()
+ .getSuggestedResolutions(
+ cameraId,
+ originalCameraIdUseCaseMap.get(cameraId),
+ newCameraIdUseCaseMap.get(cameraId));
+
+ for (BaseUseCase useCase : useCases) {
+ Size resolution = suggestResolutionsMap.get(useCase);
+ Map<String, Size> suggestedCameraSurfaceResolutionMap = new HashMap<>();
+ suggestedCameraSurfaceResolutionMap.put(cameraId, resolution);
+ useCase.updateSuggestedResolution(suggestedCameraSurfaceResolutionMap);
+ }
+ }
+ }
+
+ /**
+ * Returns the {@link CameraFactory} instance.
+ *
+ * @throws IllegalStateException if the {@link CameraFactory} has not been set, due to being
+ * uninitialized.
+ */
+ private CameraFactory getCameraFactory() {
+ if (cameraFactory == null) {
+ throw new IllegalStateException("CameraX not initialized yet.");
+ }
+
+ return cameraFactory;
+ }
+
+ /**
+ * Returns the {@link CameraDeviceSurfaceManager} instance.
+ *
+ * @throws IllegalStateException if the {@link CameraDeviceSurfaceManager} has not been set, due
+ * to being uninitialized.
+ */
+ private CameraDeviceSurfaceManager getCameraDeviceSurfaceManager() {
+ if (surfaceManager == null) {
+ throw new IllegalStateException("CameraX not initialized yet.");
+ }
+
+ return surfaceManager;
+ }
+
+ private UseCaseConfigurationFactory getDefaultConfigFactory() {
+ if (defaultConfigFactory == null) {
+ throw new IllegalStateException("CameraX not initialized yet.");
+ }
+
+ return defaultConfigFactory;
+ }
+
+ @SuppressWarnings("unused") // Context will be used in a future change
+ private void initInternal(Context context, AppConfiguration appConfiguration) {
+ if (initialized.getAndSet(true)) {
+ return;
+ }
+
+ cameraFactory = appConfiguration.getCameraFactory(null);
+ if (cameraFactory == null) {
+ throw new IllegalStateException(
+ "Invalid app configuration provided. Missing CameraFactory.");
+ }
+
+ surfaceManager = appConfiguration.getDeviceSurfaceManager(null);
+ if (surfaceManager == null) {
+ throw new IllegalStateException(
+ "Invalid app configuration provided. Missing CameraDeviceSurfaceManager.");
+ }
+
+ defaultConfigFactory = appConfiguration.getUseCaseConfigRepository(null);
+ if (defaultConfigFactory == null) {
+ throw new IllegalStateException(
+ "Invalid app configuration provided. Missing UseCaseConfigurationFactory.");
+ }
+
+ cameraRepository.init(cameraFactory);
+ }
+
+ private UseCaseGroupLifecycleController getOrCreateUseCaseGroup(LifecycleOwner lifecycleOwner) {
+ return useCaseGroupRepository.getOrCreateUseCaseGroup(
+ lifecycleOwner, useCaseGroup -> useCaseGroup.setListener(cameraRepository));
+ }
+
+ private CameraRepository getCameraRepository() {
+ return cameraRepository;
+ }
+
+ /** The types of error states that can occur. */
+ public enum ErrorCode {
+ /** The camera has moved into an unexpected state from which it can not recover from. */
+ CAMERA_STATE_INCONSISTENT,
+ /** A {@link BaseUseCase} has encountered an error from which it can not recover from. */
+ USE_CASE_ERROR
+ }
+
+ /** The direction the camera faces relative to device screen. */
+ public enum LensFacing {
+ /** A camera on the device facing the same direction as the device's screen. */
+ FRONT,
+ /** A camera on the device facing the opposite direction as the device's screen. */
+ BACK
+ }
+
+ /** Listener called whenever an error condition occurs within CameraX. */
+ public interface ErrorListener {
+
+ /**
+ * Called whenever an error occurs within CameraX.
+ *
+ * @param error the type of error that occurred
+ * @param message detailed message of the error condition
+ */
+ void onError(ErrorCode error, String message);
+ }
+}
diff --git a/camera/core/src/main/java/androidx/camera/core/CameraXThreads.java b/camera/core/src/main/java/androidx/camera/core/CameraXThreads.java
new file mode 100644
index 0000000..8ca60d8
--- /dev/null
+++ b/camera/core/src/main/java/androidx/camera/core/CameraXThreads.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2019 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.core;
+
+import androidx.annotation.RestrictTo;
+import androidx.annotation.RestrictTo.Scope;
+
+/**
+ * Static tag for creating CameraX threads. TODO(b/115747543): Remove this class when migration from
+ * threads to executors is complete.
+ *
+ * @hide
+ */
+@RestrictTo(Scope.LIBRARY_GROUP)
+public final class CameraXThreads {
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ public static final String TAG = "CameraX-";
+
+ private CameraXThreads() {
+ }
+}
diff --git a/camera/core/src/main/java/androidx/camera/core/CaptureRequestConfiguration.java b/camera/core/src/main/java/androidx/camera/core/CaptureRequestConfiguration.java
new file mode 100644
index 0000000..7b9fe15
--- /dev/null
+++ b/camera/core/src/main/java/androidx/camera/core/CaptureRequestConfiguration.java
@@ -0,0 +1,278 @@
+/*
+ * Copyright (C) 2019 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.core;
+
+import android.hardware.camera2.CameraAccessException;
+import android.hardware.camera2.CameraCaptureSession;
+import android.hardware.camera2.CameraDevice;
+import android.hardware.camera2.CaptureRequest;
+import android.hardware.camera2.CaptureRequest.Key;
+import android.view.Surface;
+
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.annotation.RestrictTo.Scope;
+import androidx.camera.core.Configuration.Option;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Configurations needed for a capture request.
+ *
+ * <p>The CaptureRequestConfiguration contains all the {@link android.hardware.camera2} parameters
+ * that are required to issue a {@link CaptureRequest}.
+ *
+ * @hide
+ */
+public final class CaptureRequestConfiguration {
+
+ /** The set of {@link Surface} that data from the camera will be put into. */
+ final List<DeferrableSurface> surfaces;
+
+ /** The parameters used to configure the {@link CaptureRequest}. */
+ final Map<Key<?>, CaptureRequestParameter<?>> captureRequestParameters;
+
+ final Configuration implementationOptions;
+
+ /**
+ * The templates used for configuring a {@link CaptureRequest}. This must match the constants
+ * defined by {@link CameraDevice}
+ */
+ final int templateType;
+
+ /** The camera capture callback for a {@link CameraCaptureSession}. */
+ final CameraCaptureCallback cameraCaptureCallback;
+
+ /** True if this capture request needs a repeating surface */
+ private final boolean useRepeatingSurface;
+
+ /**
+ * Private constructor for a CaptureRequestConfiguration.
+ *
+ * <p>In practice, the {@link CaptureRequestConfiguration.Builder} will be used to construct a
+ * CaptureRequestConfiguration.
+ *
+ * @param surfaces The set of {@link Surface} where data will be put into.
+ * @param captureRequestParameters The parameters used to configure the {@link CaptureRequest}.
+ * @param implementationOptions The generic parameters to be passed to the {@link BaseCamera}
+ * class.
+ * @param templateType The template for parameters of the CaptureRequest. This
+ * must match the
+ * constants defined by {@link CameraDevice}.
+ * @param cameraCaptureCallback The camera capture callback.
+ */
+ CaptureRequestConfiguration(
+ List<DeferrableSurface> surfaces,
+ Map<Key<?>, CaptureRequestParameter<?>> captureRequestParameters,
+ Configuration implementationOptions,
+ int templateType,
+ CameraCaptureCallback cameraCaptureCallback,
+ boolean useRepeatingSurface) {
+ this.surfaces = surfaces;
+ this.captureRequestParameters = captureRequestParameters;
+ this.implementationOptions = implementationOptions;
+ this.templateType = templateType;
+ this.cameraCaptureCallback = cameraCaptureCallback;
+ this.useRepeatingSurface = useRepeatingSurface;
+ }
+
+ public void addSurface(DeferrableSurface surface) {
+ surfaces.add(surface);
+ }
+
+ public List<DeferrableSurface> getSurfaces() {
+ return Collections.unmodifiableList(surfaces);
+ }
+
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ public Map<Key<?>, CaptureRequestParameter<?>> getCameraCharacteristics() {
+ return Collections.unmodifiableMap(captureRequestParameters);
+ }
+
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ public Configuration getImplementationOptions() {
+ return implementationOptions;
+ }
+
+ int getTemplateType() {
+ return templateType;
+ }
+
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ public boolean isUseRepeatingSurface() {
+ return useRepeatingSurface;
+ }
+
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ public CameraCaptureCallback getCameraCaptureCallback() {
+ return cameraCaptureCallback;
+ }
+
+ /**
+ * Return the builder of a {@link CaptureRequest} which can be issued.
+ *
+ * <p>Returns {@code null} if a valid {@link CaptureRequest} can not be constructed.
+ */
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ @Nullable
+ public CaptureRequest.Builder buildCaptureRequest(@Nullable CameraDevice device)
+ throws CameraAccessException {
+ if (device == null) {
+ return null;
+ }
+ CaptureRequest.Builder builder = device.createCaptureRequest(templateType);
+
+ for (CaptureRequestParameter<?> captureRequestParameter :
+ captureRequestParameters.values()) {
+ captureRequestParameter.apply(builder);
+ }
+
+ List<Surface> surfaceList = DeferrableSurfaces.surfaceList(surfaces);
+
+ if (surfaceList.isEmpty()) {
+ return null;
+ }
+
+ for (Surface surface : surfaceList) {
+ builder.addTarget(surface);
+ }
+
+ return builder;
+ }
+
+ /** Builder for easy modification/rebuilding of a {@link CaptureRequestConfiguration}. */
+ public static final class Builder {
+ private final Set<DeferrableSurface> surfaces = new HashSet<>();
+ private final Map<Key<?>, CaptureRequestParameter<?>> captureRequestParameters =
+ new HashMap<>();
+ private MutableConfiguration implementationOptions = MutableOptionsBundle.create();
+ private int templateType = -1;
+ private CameraCaptureCallback cameraCaptureCallback =
+ CameraCaptureCallbacks.createNoOpCallback();
+ private boolean useRepeatingSurface = false;
+
+ public Builder() {
+ }
+
+ private Builder(CaptureRequestConfiguration base) {
+ surfaces.addAll(base.surfaces);
+ captureRequestParameters.putAll(base.captureRequestParameters);
+ implementationOptions = MutableOptionsBundle.from(base.implementationOptions);
+ templateType = base.templateType;
+ cameraCaptureCallback = base.cameraCaptureCallback;
+ useRepeatingSurface = base.isUseRepeatingSurface();
+ }
+
+ /** Create a {@link Builder} from a {@link CaptureRequestConfiguration} */
+ public static Builder from(CaptureRequestConfiguration base) {
+ return new Builder(base);
+ }
+
+ int getTemplateType() {
+ return templateType;
+ }
+
+ /**
+ * Set the template characteristics of the CaptureRequestConfiguration.
+ *
+ * @param templateType Template constant that must match those defined by {@link
+ * CameraDevice}
+ */
+ public void setTemplateType(int templateType) {
+ this.templateType = templateType;
+ }
+
+ CameraCaptureCallback getCameraCaptureCallback() {
+ return cameraCaptureCallback;
+ }
+
+ public void setCameraCaptureCallback(CameraCaptureCallback cameraCaptureCallback) {
+ this.cameraCaptureCallback = cameraCaptureCallback;
+ }
+
+ public void addSurface(DeferrableSurface surface) {
+ surfaces.add(surface);
+ }
+
+ public void removeSurface(DeferrableSurface surface) {
+ surfaces.remove(surface);
+ }
+
+ public void clearSurfaces() {
+ surfaces.clear();
+ }
+
+ Set<DeferrableSurface> getSurfaces() {
+ return surfaces;
+ }
+
+ public <T> void addCharacteristic(Key<T> key, T value) {
+ captureRequestParameters.put(key, CaptureRequestParameter.create(key, value));
+ }
+
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ public void addCharacteristics(Map<Key<?>, CaptureRequestParameter<?>> characteristics) {
+ captureRequestParameters.putAll(characteristics);
+ }
+
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ public void setImplementationOptions(Configuration config) {
+ implementationOptions = MutableOptionsBundle.from(config);
+ }
+
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ public void addImplementationOptions(Configuration config) {
+ for (Option<?> option : config.listOptions()) {
+ @SuppressWarnings("unchecked") // Options/values are being copied directly
+ Option<Object> objectOpt = (Option<Object>) option;
+ implementationOptions.insertOption(objectOpt, config.retrieveOption(objectOpt));
+ }
+ }
+
+ Map<Key<?>, CaptureRequestParameter<?>> getCharacteristic() {
+ return captureRequestParameters;
+ }
+
+ boolean isUseRepeatingSurface() {
+ return useRepeatingSurface;
+ }
+
+ public void setUseRepeatingSurface(boolean useRepeatingSurface) {
+ this.useRepeatingSurface = useRepeatingSurface;
+ }
+
+ /**
+ * Builds an instance of a CaptureRequestConfiguration that has all the combined parameters
+ * of the CaptureRequestConfiguration that have been added to the Builder.
+ */
+ public CaptureRequestConfiguration build() {
+ return new CaptureRequestConfiguration(
+ new ArrayList<>(surfaces),
+ new HashMap<>(captureRequestParameters),
+ OptionsBundle.from(implementationOptions),
+ templateType,
+ cameraCaptureCallback,
+ useRepeatingSurface);
+ }
+ }
+}
diff --git a/camera/core/src/main/java/androidx/camera/core/CaptureRequestParameter.java b/camera/core/src/main/java/androidx/camera/core/CaptureRequestParameter.java
new file mode 100644
index 0000000..79e04bf
--- /dev/null
+++ b/camera/core/src/main/java/androidx/camera/core/CaptureRequestParameter.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2019 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.core;
+
+import android.hardware.camera2.CaptureRequest;
+
+import androidx.annotation.RestrictTo;
+import androidx.annotation.RestrictTo.Scope;
+
+import com.google.auto.value.AutoValue;
+
+/**
+ * A {@link CaptureRequest.Key}-value pair.
+ *
+ * @hide
+ */
+@RestrictTo(Scope.LIBRARY_GROUP)
+@AutoValue
+public abstract class CaptureRequestParameter<T> {
+ /** Prevent subclassing. */
+ CaptureRequestParameter() {
+ }
+
+ public static <T> CaptureRequestParameter<T> create(CaptureRequest.Key<T> key, T value) {
+ return new AutoValue_CaptureRequestParameter<>(key, value);
+ }
+
+ /**
+ * Apply the parameter to the {@link CaptureRequest.Builder}
+ *
+ * <p>This provides a type safe way of setting the key-value pair since the type of the key gets
+ * erased.
+ */
+ public final void apply(CaptureRequest.Builder builder) {
+ builder.set(getKey(), getValue());
+ }
+
+ public abstract CaptureRequest.Key<T> getKey();
+
+ public abstract T getValue();
+}
diff --git a/camera/core/src/main/java/androidx/camera/core/CheckedSurfaceTexture.java b/camera/core/src/main/java/androidx/camera/core/CheckedSurfaceTexture.java
new file mode 100644
index 0000000..461e6e9
--- /dev/null
+++ b/camera/core/src/main/java/androidx/camera/core/CheckedSurfaceTexture.java
@@ -0,0 +1,150 @@
+/*
+ * Copyright (C) 2019 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.core;
+
+import android.graphics.SurfaceTexture;
+import android.opengl.GLES20;
+import android.os.Handler;
+import android.os.Looper;
+import android.util.Size;
+import android.view.Surface;
+
+import androidx.annotation.Nullable;
+import androidx.annotation.UiThread;
+
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.common.util.concurrent.SettableFuture;
+
+import java.nio.IntBuffer;
+
+/**
+ * A {@link DeferrableSurface} which verifies the {@link SurfaceTexture} that backs the {@link
+ * Surface} is unreleased before returning the Surface.
+ */
+final class CheckedSurfaceTexture implements DeferrableSurface {
+ private final OnTextureChangedListener outputChangedListener;
+ private final Handler mainThreadHandler;
+ @Nullable
+ private SurfaceTexture surfaceTexture;
+ @Nullable
+ private Surface surface;
+ @Nullable
+ private Size resolution;
+ CheckedSurfaceTexture(
+ OnTextureChangedListener outputChangedListener, Handler mainThreadHandler) {
+ this.outputChangedListener = outputChangedListener;
+ this.mainThreadHandler = mainThreadHandler;
+ }
+
+ private static SurfaceTexture createDetachedSurfaceTexture(Size resolution) {
+ IntBuffer buffer = IntBuffer.allocate(1);
+ GLES20.glGenTextures(1, buffer);
+ SurfaceTexture surfaceTexture = new FixedSizeSurfaceTexture(buffer.get(), resolution);
+ surfaceTexture.detachFromGLContext();
+ return surfaceTexture;
+ }
+
+ @UiThread
+ void setResolution(Size resolution) {
+ this.resolution = resolution;
+ }
+
+ @UiThread
+ void resetSurfaceTexture() {
+ if (resolution == null) {
+ throw new IllegalStateException(
+ "setResolution() must be called before resetSurfaceTexture()");
+ }
+
+ release();
+ surfaceTexture = createDetachedSurfaceTexture(resolution);
+ surface = new Surface(surfaceTexture);
+ outputChangedListener.onTextureChanged(surfaceTexture, resolution);
+ }
+
+ private boolean surfaceTextureReleased(SurfaceTexture surfaceTexture) {
+ boolean released = false;
+
+ // TODO(b/121196683) Refactor workaround into a compatibility module
+ if (26 <= android.os.Build.VERSION.SDK_INT) {
+ released = surfaceTexture.isReleased();
+ } else {
+ // WARNING: This relies on some implementation details of the ViewFinderOutput native
+ // code.
+ // If the ViewFinderOutput is released, we should get a RuntimeException. If not, we
+ // should
+ // get an IllegalStateException since we are not in the same EGL context as the
+ // consumer.
+ Exception exception = null;
+ try {
+ // TODO(b/121198329) Make sure updateTexImage() isn't called on consumer EGL context
+ surfaceTexture.updateTexImage();
+ } catch (IllegalStateException e) {
+ exception = e;
+ released = false;
+ } catch (RuntimeException e) {
+ exception = e;
+ released = true;
+ }
+
+ if (!released && exception == null) {
+ throw new RuntimeException("Unable to determine if ViewFinderOutput is released");
+ }
+ }
+
+ return released;
+ }
+
+ /**
+ * Returns the {@link Surface} that is backed by a {@link SurfaceTexture}.
+ *
+ * <p>If the {@link SurfaceTexture} has already been released then the surface will be reset
+ * using a new {@link SurfaceTexture}.
+ */
+ @Override
+ public ListenableFuture<Surface> getSurface() {
+ SettableFuture<Surface> deferredSurface = SettableFuture.create();
+ Runnable checkAndSetRunnable =
+ () -> {
+ if (surfaceTextureReleased(surfaceTexture)) {
+ // Reset the surface texture and notify the listener
+ resetSurfaceTexture();
+ }
+
+ deferredSurface.set(surface);
+ };
+
+ if (Looper.myLooper() == mainThreadHandler.getLooper()) {
+ checkAndSetRunnable.run();
+ } else {
+ mainThreadHandler.post(checkAndSetRunnable);
+ }
+
+ return deferredSurface;
+ }
+
+ void release() {
+ if (surface != null) {
+ surface.release();
+ surface = null;
+ }
+ }
+
+ interface OnTextureChangedListener {
+ void onTextureChanged(SurfaceTexture newOutput, Size newResolution);
+ }
+}
diff --git a/camera/core/src/main/java/androidx/camera/core/Configuration.java b/camera/core/src/main/java/androidx/camera/core/Configuration.java
new file mode 100644
index 0000000..c717b38
--- /dev/null
+++ b/camera/core/src/main/java/androidx/camera/core/Configuration.java
@@ -0,0 +1,365 @@
+/*
+ * Copyright (C) 2019 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.core;
+
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.annotation.RestrictTo.Scope;
+
+import com.google.auto.value.AutoValue;
+import com.google.auto.value.extension.memoized.Memoized;
+
+import java.util.Set;
+
+/**
+ * A Configuration is a collection of {@link Option}s and values.
+ *
+ * <p>Configuration object hold pairs of Options/Values and offer methods for querying whether
+ * Options are contained in the configuration along with methods for retrieving the associated
+ * values for options.
+ */
+public interface Configuration {
+
+ /**
+ * Returns whether this configuration contains the supplied option.
+ *
+ * @param id The {@link Option} to search for in this configuration.
+ * @return <code>true</code> if this configuration contains the supplied option; <code>false
+ * </code> otherwise.
+ * @hide
+ */
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ boolean containsOption(Option<?> id);
+
+ /**
+ * Retrieves the value for the specified option if it exists in the configuration.
+ *
+ * <p>If the option does not exist, an exception will be thrown.
+ *
+ * @param id The {@link Option} to search for in this configuration.
+ * @param <ValueT> The type for the value associated with the supplied {@link Option}.
+ * @return The value stored in this configuration, or <code>null</code> if it does not exist.
+ * @throws IllegalArgumentException if the given option does not exist in this configuration.
+ * @hide
+ */
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ <ValueT> ValueT retrieveOption(Option<ValueT> id);
+
+ /**
+ * Retrieves the value for the specified option if it exists in the configuration.
+ *
+ * <p>If the option does not exist, <code>valueIfMissing</code> will be returned.
+ *
+ * @param id The {@link Option} to search for in this configuration.
+ * @param valueIfMissing The value to return if the specified {@link Option} does not exist in
+ * this configuration.
+ * @param <ValueT> The type for the value associated with the supplied {@link Option}.
+ * @return The value stored in this configuration, or <code>null</code> if it does not exist.
+ * @hide
+ */
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ @Nullable
+ <ValueT> ValueT retrieveOption(Option<ValueT> id, @Nullable ValueT valueIfMissing);
+
+ /**
+ * Search the configuration for {@link Option}s whose id match the supplied search string.
+ *
+ * @param idSearchString The id string to search for. This could be a fully qualified id such as
+ * \"<code>camerax.core.example.option</code>\" or the stem for an
+ * option such as \"<code>
+ * camerax.core.example</code>\".
+ * @param matcher A callback used to receive results of the search. Results will be
+ * sent to
+ * {@link OptionMatcher#onOptionMatched(Option)} in the order in which
+ * they are found inside
+ * this configuration. Subsequent results will continue to be sent as
+ * long as {@link
+ * OptionMatcher#onOptionMatched(Option)} returns <code>true</code>.
+ * @hide
+ */
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ void findOptions(String idSearchString, OptionMatcher matcher);
+
+ /**
+ * Lists all options contained within this configuration.
+ *
+ * @return A {@link Set} of {@link Option}s contained within this configuration.
+ * @hide
+ */
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ Set<Option<?>> listOptions();
+
+ /**
+ * A callback for retrieving results of a {@link Configuration.Option} search.
+ *
+ * @hide
+ */
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ interface OptionMatcher {
+ /**
+ * Receives results from {@link Configuration#findOptions(String, OptionMatcher)}.
+ *
+ * <p>When searching for a specific option in a {@link Configuration}, {@link Option}s will
+ * be sent to {@link #onOptionMatched(Option)} in the order in which they are found.
+ *
+ * @param option The matched option.
+ * @return <code>false</code> if no further results are needed; <code>true</code> otherwise.
+ */
+ boolean onOptionMatched(Option<?> option);
+ }
+
+ /**
+ * The Reader interface can be extended to create APIs for reading specific options.
+ *
+ * <p>Reader objects are also {@link Configuration} objects, so can be passed to any method that
+ * expects a {@link Configuration}.
+ */
+ interface Reader extends Configuration {
+
+ /**
+ * Returns the underlying immutable {@link Configuration} object.
+ *
+ * @return The underlying {@link Configuration} object.
+ * @hide
+ */
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ Configuration getConfiguration();
+
+ /**
+ * {@inheritDoc}
+ *
+ * @hide
+ */
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ @Override
+ default boolean containsOption(Option<?> id) {
+ return getConfiguration().containsOption(id);
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * @hide
+ */
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ @Override
+ @Nullable
+ default <ValueT> ValueT retrieveOption(Option<ValueT> id) {
+ return getConfiguration().retrieveOption(id);
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * @hide
+ */
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ @Override
+ @Nullable
+ default <ValueT> ValueT retrieveOption(Option<ValueT> id, @Nullable ValueT valueIfMissing) {
+ return getConfiguration().retrieveOption(id, valueIfMissing);
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * @hide
+ */
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ @Override
+ default void findOptions(String idStem, OptionMatcher matcher) {
+ getConfiguration().findOptions(idStem, matcher);
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * @hide
+ */
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ @Override
+ default Set<Option<?>> listOptions() {
+ return getConfiguration().listOptions();
+ }
+ }
+
+ /**
+ * Builders are used to generate immutable {@link Configuration} objects.
+ *
+ * @param <C> The top-level type of the {@link Configuration} being generated.
+ * @param <T> The top-level {@link Builder} type for this Builder.
+ */
+ interface Builder<C extends Configuration, T extends Builder<C, T>> {
+
+ /**
+ * Returns the underlying {@link MutableConfiguration} being modified by this builder.
+ *
+ * @return The underlying {@link MutableConfiguration}.
+ * @hide
+ */
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ MutableConfiguration getMutableConfiguration();
+
+ /**
+ * The solution for the unchecked cast warning.
+ *
+ * @hide
+ */
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ T builder();
+
+ /**
+ * Inserts a Option/Value pair into the configuration.
+ *
+ * <p>If the option already exists in this configuration, it will be replaced.
+ *
+ * @param opt The option to be added or modified
+ * @param value The value to insert for this option.
+ * @param <ValueT> The type of the value being inserted.
+ * @hide
+ */
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ default <ValueT> T insertOption(Option<ValueT> opt, ValueT value) {
+ getMutableConfiguration().insertOption(opt, value);
+ return builder();
+ }
+
+ /**
+ * Removes an option from the configuration if it exists.
+ *
+ * @param opt The option to remove from the configuration.
+ * @param <ValueT> The type of the value being removed.
+ * @return The value that previously existed for <code>opt</code>, or <code>null</code> if
+ * the option did not exist in this configuration.
+ * @hide
+ */
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ @Nullable
+ default <ValueT> T removeOption(Option<ValueT> opt) {
+ getMutableConfiguration().removeOption(opt);
+ return builder();
+ }
+
+ /**
+ * Creates an immutable {@link Configuration} object from the current state of this builder.
+ *
+ * @return The {@link Configuration} generated from the current state.
+ */
+ C build();
+ }
+
+ /**
+ * An {@link Option} is used to set and retrieve values for settings defined in a {@link
+ * Configuration}.
+ *
+ * <p>{@link Option}s can be thought of as the key in a key/value pair that makes up a setting.
+ * As the name suggests, {@link Option}s are optional, and may or may not exist inside a {@link
+ * Configuration}.
+ *
+ * @param <T> The type of the value for this option.
+ * @hide
+ */
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ @AutoValue
+ abstract class Option<T> {
+
+ /** Prevent subclassing */
+ Option() {
+ }
+
+ /**
+ * Creates an {@link Option} from an id and value class.
+ *
+ * @param id A unique string identifier for this option. This generally follows
+ * the scheme
+ * <code><owner>.[optional.subCategories.]<optionId></code>.
+ * @param valueClass The class of the value stored by this option.
+ * @param <T> The type of the value stored by this option.
+ * @return An {@link Option} object which can be used to store/retrieve values from a {@link
+ * Configuration}.
+ */
+ public static <T> Option<T> create(String id, Class<T> valueClass) {
+ TypeReference<T> valueType = TypeReference.createSpecializedTypeReference(valueClass);
+ return create(id, valueType, /*token=*/ null);
+ }
+
+ /**
+ * Creates an {@link Option} from an id, value class and token.
+ *
+ * @param id A unique string identifier for this option. This generally follows
+ * the scheme
+ * <code><owner>.[optional.subCategories.]<optionId></code>.
+ * @param valueClass The class of the value stored by this option.
+ * @param <T> The type of the value stored by this option.
+ * @param token An optional, type-erased object for storing more context for this
+ * specific
+ * option. Generally this object should have static scope and be
+ * immutable.
+ * @return An {@link Option} object which can be used to store/retrieve values from a {@link
+ * Configuration}.
+ */
+ public static <T> Option<T> create(String id, Class<T> valueClass, @Nullable Object token) {
+ TypeReference<T> valueType = TypeReference.createSpecializedTypeReference(valueClass);
+ return create(id, valueType, token);
+ }
+
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ public static <T> Option<T> create(String name, TypeReference<T> valueType) {
+ return create(name, valueType, /*token=*/ null);
+ }
+
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ public static <T> Option<T> create(
+ String name, TypeReference<T> valueType, @Nullable Object token) {
+ return new AutoValue_Configuration_Option<>(name, valueType, token);
+ }
+
+ /**
+ * Returns the unique string identifier for this option.
+ *
+ * <p>This generally follows the scheme * <code>
+ * <owner>.[optional.subCategories.]<optionId>
+ * </code>.
+ *
+ * @return The identifier.
+ */
+ public abstract String getId();
+
+ abstract TypeReference<T> getTypeReference();
+
+ /**
+ * Returns the optional type-erased context object for this option.
+ *
+ * <p>Generally this object should have static scope and be immutable.
+ *
+ * @return The type-erased context object.
+ */
+ @Nullable
+ public abstract Object getToken();
+
+ /**
+ * Returns the class object associated with the value for this option.
+ *
+ * @return The class object for the value's type.
+ */
+ @Memoized
+ @SuppressWarnings("unchecked")
+ public Class<T> getValueClass() {
+ return (Class<T>) getTypeReference().getRawType();
+ }
+ }
+}
diff --git a/camera/core/src/main/java/androidx/camera/core/ConfigurationProvider.java b/camera/core/src/main/java/androidx/camera/core/ConfigurationProvider.java
new file mode 100644
index 0000000..6b0dbef
--- /dev/null
+++ b/camera/core/src/main/java/androidx/camera/core/ConfigurationProvider.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2019 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.core;
+
+import androidx.annotation.RestrictTo;
+import androidx.annotation.RestrictTo.Scope;
+
+/**
+ * A class which provides a {@link androidx.camera.core.Configuration} object.
+ *
+ * @hide
+ */
+@RestrictTo(Scope.LIBRARY_GROUP)
+public interface ConfigurationProvider<C extends Configuration> {
+
+ /** Retrieve the {@link androidx.camera.core.Configuration} object. */
+ C getConfiguration();
+}
diff --git a/camera/core/src/main/java/androidx/camera/core/DeferrableSurface.java b/camera/core/src/main/java/androidx/camera/core/DeferrableSurface.java
new file mode 100644
index 0000000..038bf61
--- /dev/null
+++ b/camera/core/src/main/java/androidx/camera/core/DeferrableSurface.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2019 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.core;
+
+import android.view.Surface;
+
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.annotation.RestrictTo.Scope;
+
+import com.google.common.util.concurrent.ListenableFuture;
+
+/**
+ * A reference to a {@link Surface} whose creation can be deferred to a later time.
+ *
+ * @hide
+ */
+@RestrictTo(Scope.LIBRARY_GROUP)
+public interface DeferrableSurface {
+ /** Returns a {@link Surface} that is wrapped in a {@link ListenableFuture}. */
+ @Nullable
+ ListenableFuture<Surface> getSurface();
+}
diff --git a/camera/core/src/main/java/androidx/camera/core/DeferrableSurfaces.java b/camera/core/src/main/java/androidx/camera/core/DeferrableSurfaces.java
new file mode 100644
index 0000000..ae0d86d
--- /dev/null
+++ b/camera/core/src/main/java/androidx/camera/core/DeferrableSurfaces.java
@@ -0,0 +1,97 @@
+/*
+ * Copyright (C) 2019 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.core;
+
+import android.view.Surface;
+
+import androidx.annotation.RestrictTo;
+import androidx.annotation.RestrictTo.Scope;
+
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.ExecutionException;
+
+/**
+ * Utility functions for manipulating {@link DeferrableSurface}.
+ *
+ * @hide
+ */
+@RestrictTo(Scope.LIBRARY_GROUP)
+public final class DeferrableSurfaces {
+ private static final String TAG = "DeferrableSurfaces";
+
+ private DeferrableSurfaces() {
+ }
+
+ /**
+ * Returns a {@link Surface} list from a {@link DeferrableSurface} collection.
+ *
+ * <p>Any {@link DeferrableSurface} that can not be obtained will be missing from the list. This
+ * means that the returned list will only be guaranteed to be less than or equal to in size to
+ * the original collection.
+ */
+ public static List<Surface> surfaceList(Collection<DeferrableSurface> deferrableSurfaces) {
+ List<ListenableFuture<Surface>> listenableFutureSurfaces = new ArrayList<>();
+
+ for (DeferrableSurface deferrableSurface : deferrableSurfaces) {
+ listenableFutureSurfaces.add(deferrableSurface.getSurface());
+ }
+
+ try {
+ // Need to create a new list since the list returned by successfulAsList() is
+ // unmodifiable so
+ // it will throw an Exception
+ List<Surface> surfaces =
+ new ArrayList<>(Futures.successfulAsList(listenableFutureSurfaces).get());
+ surfaces.removeAll(Collections.singleton(null));
+ return Collections.unmodifiableList(surfaces);
+ } catch (InterruptedException | ExecutionException e) {
+ return Collections.unmodifiableList(Collections.emptyList());
+ }
+ }
+
+ /**
+ * Returns a {@link Surface} set from a {@link DeferrableSurface} collection.
+ *
+ * <p>Any {@link DeferrableSurface} that can not be obtained will be missing from the set. This
+ * means that the returned set will only be guaranteed to be less than or equal to in size to
+ * the original collection.
+ */
+ public static Set<Surface> surfaceSet(Collection<DeferrableSurface> deferrableSurfaces) {
+ List<ListenableFuture<Surface>> listenableFutureSurfaces = new ArrayList<>();
+
+ for (DeferrableSurface deferrableSurface : deferrableSurfaces) {
+ listenableFutureSurfaces.add(deferrableSurface.getSurface());
+ }
+
+ try {
+ HashSet<Surface> surfaces =
+ new HashSet<>(Futures.successfulAsList(listenableFutureSurfaces).get());
+ surfaces.removeAll(Collections.singleton(null));
+ return Collections.unmodifiableSet(surfaces);
+ } catch (InterruptedException | ExecutionException e) {
+ return Collections.unmodifiableSet(Collections.emptySet());
+ }
+ }
+}
diff --git a/camera/core/src/main/java/androidx/camera/core/DeviceProperties.java b/camera/core/src/main/java/androidx/camera/core/DeviceProperties.java
new file mode 100644
index 0000000..d0383d7
--- /dev/null
+++ b/camera/core/src/main/java/androidx/camera/core/DeviceProperties.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2019 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.core;
+
+import android.os.Build;
+
+import com.google.auto.value.AutoValue;
+
+/** Container of the device properties. */
+@AutoValue
+abstract class DeviceProperties {
+ /** Creates an instance by querying the properties from {@link android.os.Build}. */
+ static DeviceProperties create() {
+ return create(Build.MANUFACTURER, Build.MODEL, Build.VERSION.SDK_INT);
+ }
+
+ /** Creates an instance from the given properties. */
+ static DeviceProperties create(String manufacturer, String model, int sdkVersion) {
+ return new AutoValue_DeviceProperties(manufacturer, model, sdkVersion);
+ }
+
+ /** Returns the manufacturer of the device. */
+ abstract String manufacturer();
+
+ /** Returns the model of the device. */
+ abstract String model();
+
+ /** Returns the SDK version of the OS running on the device. */
+ abstract int sdkVersion();
+}
diff --git a/camera/core/src/main/java/androidx/camera/core/ErrorHandler.java b/camera/core/src/main/java/androidx/camera/core/ErrorHandler.java
new file mode 100644
index 0000000..7685d03
--- /dev/null
+++ b/camera/core/src/main/java/androidx/camera/core/ErrorHandler.java
@@ -0,0 +1,87 @@
+/*
+ * Copyright (C) 2019 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.core;
+
+import android.os.Handler;
+import android.os.Looper;
+import android.util.Log;
+
+import androidx.annotation.GuardedBy;
+import androidx.annotation.RestrictTo;
+import androidx.annotation.RestrictTo.Scope;
+import androidx.camera.core.CameraX.ErrorCode;
+import androidx.camera.core.CameraX.ErrorListener;
+
+/**
+ * Handler for sending and receiving error messages.
+ *
+ * @hide Only internal classes should post error messages
+ */
+@RestrictTo(Scope.LIBRARY_GROUP)
+public final class ErrorHandler {
+ private static final String TAG = "ErrorHandler";
+
+ private final Object errorLock = new Object();
+
+ @GuardedBy("errorLock")
+ private ErrorListener listener = new PrintingErrorListener();
+
+ @GuardedBy("errorLock")
+ private Handler handler = new Handler(Looper.getMainLooper());
+
+ /**
+ * Posts an error message.
+ *
+ * @param error the type of error that occurred
+ * @param message detailed message of the error condition
+ */
+ void postError(ErrorCode error, String message) {
+ synchronized (errorLock) {
+ ErrorListener listenerReference = listener;
+ handler.post(() -> listenerReference.onError(error, message));
+ }
+ }
+
+ /**
+ * Sets the listener for the error.
+ *
+ * @param listener the listener which should handle the error condition
+ * @param handler the handler on which to run the listener
+ */
+ void setErrorListener(ErrorListener listener, Handler handler) {
+ synchronized (errorLock) {
+ if (handler == null) {
+ this.handler = new Handler(Looper.getMainLooper());
+ } else {
+ this.handler = handler;
+ }
+ if (listener == null) {
+ this.listener = new PrintingErrorListener();
+ } else {
+ this.listener = listener;
+ }
+ }
+ }
+
+ /** An error listener which logs the error message and returns. */
+ static final class PrintingErrorListener implements ErrorListener {
+ @Override
+ public void onError(ErrorCode error, String message) {
+ Log.e(TAG, "ErrorHandler occurred: " + error + " with message: " + message);
+ }
+ }
+}
diff --git a/camera/core/src/main/java/androidx/camera/core/Exif.java b/camera/core/src/main/java/androidx/camera/core/Exif.java
new file mode 100644
index 0000000..a74df30
--- /dev/null
+++ b/camera/core/src/main/java/androidx/camera/core/Exif.java
@@ -0,0 +1,649 @@
+/*
+ * Copyright (C) 2019 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.core;
+
+import android.location.Location;
+import android.util.Log;
+
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.annotation.RestrictTo.Scope;
+import androidx.exifinterface.media.ExifInterface;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.Locale;
+
+/**
+ * Utility class for modifying metadata on JPEG files.
+ *
+ * <p>Call {@link #save()} to persist changes to disc.
+ *
+ * @hide
+ */
+@RestrictTo(Scope.LIBRARY_GROUP)
+public final class Exif {
+
+ /** Timestamp value indicating a timestamp value that is either not set or not valid */
+ public static final long INVALID_TIMESTAMP = -1;
+
+ private static final String TAG = Exif.class.getSimpleName();
+
+ private static final ThreadLocal<SimpleDateFormat> DATE_FORMAT =
+ new ThreadLocal<SimpleDateFormat>() {
+ @Override
+ public SimpleDateFormat initialValue() {
+ return new SimpleDateFormat("yyyy:MM:dd", Locale.US);
+ }
+ };
+ private static final ThreadLocal<SimpleDateFormat> TIME_FORMAT =
+ new ThreadLocal<SimpleDateFormat>() {
+ @Override
+ public SimpleDateFormat initialValue() {
+ return new SimpleDateFormat("HH:mm:ss", Locale.US);
+ }
+ };
+ private static final ThreadLocal<SimpleDateFormat> DATETIME_FORMAT =
+ new ThreadLocal<SimpleDateFormat>() {
+ @Override
+ public SimpleDateFormat initialValue() {
+ return new SimpleDateFormat("yyyy:MM:dd HH:mm:ss", Locale.US);
+ }
+ };
+
+ private static final String KILOMETERS_PER_HOUR = "K";
+ private static final String MILES_PER_HOUR = "M";
+ private static final String KNOTS = "N";
+
+ private final ExifInterface exifInterface;
+
+ // When true, avoid saving any time. This is a privacy issue.
+ private boolean removeTimestamp = false;
+
+ private Exif(ExifInterface exifInterface) {
+ this.exifInterface = exifInterface;
+ }
+
+ public static Exif createFromFile(File file) throws IOException {
+ return createFromFileString(file.toString());
+ }
+
+ public static Exif createFromFileString(String filePath) throws IOException {
+ return new Exif(new ExifInterface(filePath));
+ }
+
+ public static Exif createFromInputStream(InputStream is) throws IOException {
+ return new Exif(new ExifInterface(is));
+ }
+
+ private static String convertToExifDateTime(long timestamp) {
+ return DATETIME_FORMAT.get().format(new Date(timestamp));
+ }
+
+ private static Date convertFromExifDateTime(String dateTime) throws ParseException {
+ return DATETIME_FORMAT.get().parse(dateTime);
+ }
+
+ private static Date convertFromExifDate(String date) throws ParseException {
+ return DATE_FORMAT.get().parse(date);
+ }
+
+ private static Date convertFromExifTime(String time) throws ParseException {
+ return TIME_FORMAT.get().parse(time);
+ }
+
+ /** Persists changes to disc. */
+ public void save() throws IOException {
+ if (!removeTimestamp) {
+ attachLastModifiedTimestamp();
+ }
+ exifInterface.saveAttributes();
+ }
+
+ @Override
+ public String toString() {
+ return String.format(
+ Locale.ENGLISH,
+ "Exif{width=%s, height=%s, rotation=%d, "
+ + "isFlippedVertically=%s, isFlippedHorizontally=%s, location=%s, "
+ + "timestamp=%s, description=%s}",
+ getWidth(),
+ getHeight(),
+ getRotation(),
+ isFlippedVertically(),
+ isFlippedHorizontally(),
+ getLocation(),
+ getTimestamp(),
+ getDescription());
+ }
+
+ private int getOrientation() {
+ return exifInterface.getAttributeInt(
+ ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_UNDEFINED);
+ }
+
+ /** Returns the width of the photo in pixels. */
+ public int getWidth() {
+ return exifInterface.getAttributeInt(ExifInterface.TAG_IMAGE_WIDTH, 0);
+ }
+
+ /** Returns the height of the photo in pixels. */
+ public int getHeight() {
+ return exifInterface.getAttributeInt(ExifInterface.TAG_IMAGE_LENGTH, 0);
+ }
+
+ @Nullable
+ public String getDescription() {
+ return exifInterface.getAttribute(ExifInterface.TAG_IMAGE_DESCRIPTION);
+ }
+
+ public void setDescription(@Nullable String description) {
+ exifInterface.setAttribute(ExifInterface.TAG_IMAGE_DESCRIPTION, description);
+ }
+
+ /** @return The degree of rotation (eg. 0, 90, 180, 270). */
+ public int getRotation() {
+ switch (getOrientation()) {
+ case ExifInterface.ORIENTATION_FLIP_HORIZONTAL:
+ return 0;
+ case ExifInterface.ORIENTATION_ROTATE_180:
+ return 180;
+ case ExifInterface.ORIENTATION_FLIP_VERTICAL:
+ return 180;
+ case ExifInterface.ORIENTATION_TRANSPOSE:
+ return 270;
+ case ExifInterface.ORIENTATION_ROTATE_90:
+ return 90;
+ case ExifInterface.ORIENTATION_TRANSVERSE:
+ return 90;
+ case ExifInterface.ORIENTATION_ROTATE_270:
+ return 270;
+ case ExifInterface.ORIENTATION_NORMAL:
+ // Fall-through
+ case ExifInterface.ORIENTATION_UNDEFINED:
+ // Fall-through
+ default:
+ return 0;
+ }
+ }
+
+ /** @return True if the image is flipped vertically after rotation. */
+ public boolean isFlippedVertically() {
+ switch (getOrientation()) {
+ case ExifInterface.ORIENTATION_FLIP_HORIZONTAL:
+ return false;
+ case ExifInterface.ORIENTATION_ROTATE_180:
+ return false;
+ case ExifInterface.ORIENTATION_FLIP_VERTICAL:
+ return true;
+ case ExifInterface.ORIENTATION_TRANSPOSE:
+ return true;
+ case ExifInterface.ORIENTATION_ROTATE_90:
+ return false;
+ case ExifInterface.ORIENTATION_TRANSVERSE:
+ return true;
+ case ExifInterface.ORIENTATION_ROTATE_270:
+ return false;
+ case ExifInterface.ORIENTATION_NORMAL:
+ // Fall-through
+ case ExifInterface.ORIENTATION_UNDEFINED:
+ // Fall-through
+ default:
+ return false;
+ }
+ }
+
+ /** @return True if the image is flipped horizontally after rotation. */
+ public boolean isFlippedHorizontally() {
+ switch (getOrientation()) {
+ case ExifInterface.ORIENTATION_FLIP_HORIZONTAL:
+ return true;
+ case ExifInterface.ORIENTATION_ROTATE_180:
+ return false;
+ case ExifInterface.ORIENTATION_FLIP_VERTICAL:
+ return false;
+ case ExifInterface.ORIENTATION_TRANSPOSE:
+ return false;
+ case ExifInterface.ORIENTATION_ROTATE_90:
+ return false;
+ case ExifInterface.ORIENTATION_TRANSVERSE:
+ return false;
+ case ExifInterface.ORIENTATION_ROTATE_270:
+ return false;
+ case ExifInterface.ORIENTATION_NORMAL:
+ // Fall-through
+ case ExifInterface.ORIENTATION_UNDEFINED:
+ // Fall-through
+ default:
+ return false;
+ }
+ }
+
+ private void attachLastModifiedTimestamp() {
+ long now = System.currentTimeMillis();
+ String datetime = convertToExifDateTime(now);
+
+ exifInterface.setAttribute(ExifInterface.TAG_DATETIME, datetime);
+
+ try {
+ String subsec = Long.toString(now - convertFromExifDateTime(datetime).getTime());
+ exifInterface.setAttribute(ExifInterface.TAG_SUBSEC_TIME, subsec);
+ } catch (ParseException e) {
+ }
+ }
+
+ /**
+ * @return The timestamp (in millis) that this picture was modified, or {@link
+ * #INVALID_TIMESTAMP} if no time is available.
+ */
+ public long getLastModifiedTimestamp() {
+ long timestamp = parseTimestamp(exifInterface.getAttribute(ExifInterface.TAG_DATETIME));
+ if (timestamp == INVALID_TIMESTAMP) {
+ return INVALID_TIMESTAMP;
+ }
+
+ String subSecs = exifInterface.getAttribute(ExifInterface.TAG_SUBSEC_TIME);
+ if (subSecs != null) {
+ try {
+ long sub = Long.parseLong(subSecs);
+ while (sub > 1000) {
+ sub /= 10;
+ }
+ timestamp += sub;
+ } catch (NumberFormatException e) {
+ // Ignored
+ }
+ }
+
+ return timestamp;
+ }
+
+ /**
+ * @return The timestamp (in millis) that this picture was taken, or {@link #INVALID_TIMESTAMP}
+ * if no time is available.
+ */
+ public long getTimestamp() {
+ long timestamp =
+ parseTimestamp(exifInterface.getAttribute(ExifInterface.TAG_DATETIME_ORIGINAL));
+ if (timestamp == INVALID_TIMESTAMP) {
+ return INVALID_TIMESTAMP;
+ }
+
+ String subSecs = exifInterface.getAttribute(ExifInterface.TAG_SUBSEC_TIME_ORIGINAL);
+ if (subSecs != null) {
+ try {
+ long sub = Long.parseLong(subSecs);
+ while (sub > 1000) {
+ sub /= 10;
+ }
+ timestamp += sub;
+ } catch (NumberFormatException e) {
+ // Ignored
+ }
+ }
+
+ return timestamp;
+ }
+
+ /** @return The location this picture was taken, or null if no location is available. */
+ @Nullable
+ public Location getLocation() {
+ String provider = exifInterface.getAttribute(ExifInterface.TAG_GPS_PROCESSING_METHOD);
+ double[] latlng = exifInterface.getLatLong();
+ double altitude = exifInterface.getAltitude(0);
+ double speed = exifInterface.getAttributeDouble(ExifInterface.TAG_GPS_SPEED, 0);
+ String speedRef = exifInterface.getAttribute(ExifInterface.TAG_GPS_SPEED_REF);
+ speedRef = speedRef == null ? KILOMETERS_PER_HOUR : speedRef; // Ensure speedRef is not null
+ long timestamp =
+ parseTimestamp(
+ exifInterface.getAttribute(ExifInterface.TAG_GPS_DATESTAMP),
+ exifInterface.getAttribute(ExifInterface.TAG_GPS_TIMESTAMP));
+ if (latlng == null) {
+ return null;
+ }
+ if (provider == null) {
+ provider = TAG;
+ }
+
+ Location location = new Location(provider);
+ location.setLatitude(latlng[0]);
+ location.setLongitude(latlng[1]);
+ if (altitude != 0) {
+ location.setAltitude(altitude);
+ }
+ if (speed != 0) {
+ switch (speedRef) {
+ case MILES_PER_HOUR:
+ speed = Speed.fromMilesPerHour(speed).toMetersPerSecond();
+ break;
+ case KNOTS:
+ speed = Speed.fromKnots(speed).toMetersPerSecond();
+ break;
+ case KILOMETERS_PER_HOUR:
+ // fall through
+ default:
+ speed = Speed.fromKilometersPerHour(speed).toMetersPerSecond();
+ break;
+ }
+
+ location.setSpeed((float) speed);
+ }
+ if (timestamp != INVALID_TIMESTAMP) {
+ location.setTime(timestamp);
+ }
+ return location;
+ }
+
+ /**
+ * Rotates the image by the given degrees. Can only rotate by right angles (eg. 90, 180, -90).
+ * Other increments will set the orientation to undefined.
+ */
+ public void rotate(int degrees) {
+ if (degrees % 90 != 0) {
+ Log.w(
+ TAG,
+ String.format(
+ "Can only rotate in right angles (eg. 0, 90, 180, 270). %d is "
+ + "unsupported.",
+ degrees));
+ exifInterface.setAttribute(
+ ExifInterface.TAG_ORIENTATION,
+ String.valueOf(ExifInterface.ORIENTATION_UNDEFINED));
+ return;
+ }
+
+ degrees %= 360;
+
+ int orientation = getOrientation();
+ while (degrees < 0) {
+ degrees += 90;
+
+ switch (orientation) {
+ case ExifInterface.ORIENTATION_FLIP_HORIZONTAL:
+ orientation = ExifInterface.ORIENTATION_TRANSPOSE;
+ break;
+ case ExifInterface.ORIENTATION_ROTATE_180:
+ orientation = ExifInterface.ORIENTATION_ROTATE_90;
+ break;
+ case ExifInterface.ORIENTATION_FLIP_VERTICAL:
+ orientation = ExifInterface.ORIENTATION_TRANSVERSE;
+ break;
+ case ExifInterface.ORIENTATION_TRANSPOSE:
+ orientation = ExifInterface.ORIENTATION_FLIP_VERTICAL;
+ break;
+ case ExifInterface.ORIENTATION_ROTATE_90:
+ orientation = ExifInterface.ORIENTATION_NORMAL;
+ break;
+ case ExifInterface.ORIENTATION_TRANSVERSE:
+ orientation = ExifInterface.ORIENTATION_FLIP_HORIZONTAL;
+ break;
+ case ExifInterface.ORIENTATION_ROTATE_270:
+ orientation = ExifInterface.ORIENTATION_ROTATE_90;
+ break;
+ case ExifInterface.ORIENTATION_NORMAL:
+ // Fall-through
+ case ExifInterface.ORIENTATION_UNDEFINED:
+ // Fall-through
+ default:
+ orientation = ExifInterface.ORIENTATION_ROTATE_270;
+ break;
+ }
+ }
+ while (degrees > 0) {
+ degrees -= 90;
+
+ switch (orientation) {
+ case ExifInterface.ORIENTATION_FLIP_HORIZONTAL:
+ orientation = ExifInterface.ORIENTATION_TRANSVERSE;
+ break;
+ case ExifInterface.ORIENTATION_ROTATE_180:
+ orientation = ExifInterface.ORIENTATION_ROTATE_270;
+ break;
+ case ExifInterface.ORIENTATION_FLIP_VERTICAL:
+ orientation = ExifInterface.ORIENTATION_TRANSPOSE;
+ break;
+ case ExifInterface.ORIENTATION_TRANSPOSE:
+ orientation = ExifInterface.ORIENTATION_FLIP_HORIZONTAL;
+ break;
+ case ExifInterface.ORIENTATION_ROTATE_90:
+ orientation = ExifInterface.ORIENTATION_ROTATE_180;
+ break;
+ case ExifInterface.ORIENTATION_TRANSVERSE:
+ orientation = ExifInterface.ORIENTATION_FLIP_VERTICAL;
+ break;
+ case ExifInterface.ORIENTATION_ROTATE_270:
+ orientation = ExifInterface.ORIENTATION_NORMAL;
+ break;
+ case ExifInterface.ORIENTATION_NORMAL:
+ // Fall-through
+ case ExifInterface.ORIENTATION_UNDEFINED:
+ // Fall-through
+ default:
+ orientation = ExifInterface.ORIENTATION_ROTATE_90;
+ break;
+ }
+ }
+ exifInterface.setAttribute(ExifInterface.TAG_ORIENTATION, String.valueOf(orientation));
+ }
+
+ /**
+ * Sets attributes to represent a flip of the image over the horizon so that the top and bottom
+ * are reversed.
+ */
+ public void flipVertically() {
+ int orientation;
+ switch (getOrientation()) {
+ case ExifInterface.ORIENTATION_FLIP_HORIZONTAL:
+ orientation = ExifInterface.ORIENTATION_ROTATE_180;
+ break;
+ case ExifInterface.ORIENTATION_ROTATE_180:
+ orientation = ExifInterface.ORIENTATION_FLIP_HORIZONTAL;
+ break;
+ case ExifInterface.ORIENTATION_FLIP_VERTICAL:
+ orientation = ExifInterface.ORIENTATION_NORMAL;
+ break;
+ case ExifInterface.ORIENTATION_TRANSPOSE:
+ orientation = ExifInterface.ORIENTATION_ROTATE_270;
+ break;
+ case ExifInterface.ORIENTATION_ROTATE_90:
+ orientation = ExifInterface.ORIENTATION_TRANSVERSE;
+ break;
+ case ExifInterface.ORIENTATION_TRANSVERSE:
+ orientation = ExifInterface.ORIENTATION_ROTATE_90;
+ break;
+ case ExifInterface.ORIENTATION_ROTATE_270:
+ orientation = ExifInterface.ORIENTATION_TRANSPOSE;
+ break;
+ case ExifInterface.ORIENTATION_NORMAL:
+ // Fall-through
+ case ExifInterface.ORIENTATION_UNDEFINED:
+ // Fall-through
+ default:
+ orientation = ExifInterface.ORIENTATION_FLIP_VERTICAL;
+ break;
+ }
+ exifInterface.setAttribute(ExifInterface.TAG_ORIENTATION, String.valueOf(orientation));
+ }
+
+ /**
+ * Sets attributes to represent a flip of the image over the vertical so that the left and right
+ * are reversed.
+ */
+ public void flipHorizontally() {
+ int orientation;
+ switch (getOrientation()) {
+ case ExifInterface.ORIENTATION_FLIP_HORIZONTAL:
+ orientation = ExifInterface.ORIENTATION_NORMAL;
+ break;
+ case ExifInterface.ORIENTATION_ROTATE_180:
+ orientation = ExifInterface.ORIENTATION_FLIP_VERTICAL;
+ break;
+ case ExifInterface.ORIENTATION_FLIP_VERTICAL:
+ orientation = ExifInterface.ORIENTATION_ROTATE_180;
+ break;
+ case ExifInterface.ORIENTATION_TRANSPOSE:
+ orientation = ExifInterface.ORIENTATION_ROTATE_90;
+ break;
+ case ExifInterface.ORIENTATION_ROTATE_90:
+ orientation = ExifInterface.ORIENTATION_TRANSPOSE;
+ break;
+ case ExifInterface.ORIENTATION_TRANSVERSE:
+ orientation = ExifInterface.ORIENTATION_ROTATE_270;
+ break;
+ case ExifInterface.ORIENTATION_ROTATE_270:
+ orientation = ExifInterface.ORIENTATION_TRANSVERSE;
+ break;
+ case ExifInterface.ORIENTATION_NORMAL:
+ // Fall-through
+ case ExifInterface.ORIENTATION_UNDEFINED:
+ // Fall-through
+ default:
+ orientation = ExifInterface.ORIENTATION_FLIP_HORIZONTAL;
+ break;
+ }
+ exifInterface.setAttribute(ExifInterface.TAG_ORIENTATION, String.valueOf(orientation));
+ }
+
+ /** Attaches the current timestamp to the file. */
+ public void attachTimestamp() {
+ long now = System.currentTimeMillis();
+ String datetime = convertToExifDateTime(now);
+
+ exifInterface.setAttribute(ExifInterface.TAG_DATETIME_ORIGINAL, datetime);
+ exifInterface.setAttribute(ExifInterface.TAG_DATETIME_DIGITIZED, datetime);
+
+ try {
+ String subsec = Long.toString(now - convertFromExifDateTime(datetime).getTime());
+ exifInterface.setAttribute(ExifInterface.TAG_SUBSEC_TIME_ORIGINAL, subsec);
+ exifInterface.setAttribute(ExifInterface.TAG_SUBSEC_TIME_DIGITIZED, subsec);
+ } catch (ParseException e) {
+ }
+
+ removeTimestamp = false;
+ }
+
+ /** Removes the timestamp from the file. */
+ public void removeTimestamp() {
+ exifInterface.setAttribute(ExifInterface.TAG_DATETIME, null);
+ exifInterface.setAttribute(ExifInterface.TAG_DATETIME_ORIGINAL, null);
+ exifInterface.setAttribute(ExifInterface.TAG_DATETIME_DIGITIZED, null);
+ exifInterface.setAttribute(ExifInterface.TAG_SUBSEC_TIME, null);
+ exifInterface.setAttribute(ExifInterface.TAG_SUBSEC_TIME_ORIGINAL, null);
+ exifInterface.setAttribute(ExifInterface.TAG_SUBSEC_TIME_DIGITIZED, null);
+ removeTimestamp = true;
+ }
+
+ /** Attaches the given location to the file. */
+ public void attachLocation(Location location) {
+ exifInterface.setGpsInfo(location);
+ }
+
+ /** Removes the location from the file. */
+ public void removeLocation() {
+ exifInterface.setAttribute(ExifInterface.TAG_GPS_PROCESSING_METHOD, null);
+ exifInterface.setAttribute(ExifInterface.TAG_GPS_LATITUDE, null);
+ exifInterface.setAttribute(ExifInterface.TAG_GPS_LATITUDE_REF, null);
+ exifInterface.setAttribute(ExifInterface.TAG_GPS_LONGITUDE, null);
+ exifInterface.setAttribute(ExifInterface.TAG_GPS_LONGITUDE_REF, null);
+ exifInterface.setAttribute(ExifInterface.TAG_GPS_ALTITUDE, null);
+ exifInterface.setAttribute(ExifInterface.TAG_GPS_ALTITUDE_REF, null);
+ exifInterface.setAttribute(ExifInterface.TAG_GPS_SPEED, null);
+ exifInterface.setAttribute(ExifInterface.TAG_GPS_SPEED_REF, null);
+ exifInterface.setAttribute(ExifInterface.TAG_GPS_DATESTAMP, null);
+ exifInterface.setAttribute(ExifInterface.TAG_GPS_TIMESTAMP, null);
+ }
+
+ /** @return The timestamp (in millis), or {@link #INVALID_TIMESTAMP} if no time is available. */
+ private long parseTimestamp(@Nullable String date, @Nullable String time) {
+ if (date == null && time == null) {
+ return INVALID_TIMESTAMP;
+ }
+ if (time == null) {
+ try {
+ return convertFromExifDate(date).getTime();
+ } catch (ParseException e) {
+ return INVALID_TIMESTAMP;
+ }
+ }
+ if (date == null) {
+ try {
+ return convertFromExifTime(time).getTime();
+ } catch (ParseException e) {
+ return INVALID_TIMESTAMP;
+ }
+ }
+ return parseTimestamp(date + " " + time);
+ }
+
+ /** @return The timestamp (in millis), or {@link #INVALID_TIMESTAMP} if no time is available. */
+ private long parseTimestamp(@Nullable String datetime) {
+ if (datetime == null) {
+ return INVALID_TIMESTAMP;
+ }
+ try {
+ return convertFromExifDateTime(datetime).getTime();
+ } catch (ParseException e) {
+ return INVALID_TIMESTAMP;
+ }
+ }
+
+ private static final class Speed {
+ static Converter fromKilometersPerHour(double kph) {
+ return new Converter(kph * 0.621371);
+ }
+
+ static Converter fromMetersPerSecond(double mps) {
+ return new Converter(mps * 2.23694);
+ }
+
+ static Converter fromMilesPerHour(double mph) {
+ return new Converter(mph);
+ }
+
+ static Converter fromKnots(double knots) {
+ return new Converter(knots * 1.15078);
+ }
+
+ static final class Converter {
+ final double mph;
+
+ Converter(double mph) {
+ this.mph = mph;
+ }
+
+ double toKilometersPerHour() {
+ return mph / 0.621371;
+ }
+
+ double toMilesPerHour() {
+ return mph;
+ }
+
+ double toKnots() {
+ return mph / 1.15078;
+ }
+
+ double toMetersPerSecond() {
+ return mph / 2.23694;
+ }
+ }
+ }
+}
diff --git a/camera/core/src/main/java/androidx/camera/core/ExtendableUseCaseConfigFactory.java b/camera/core/src/main/java/androidx/camera/core/ExtendableUseCaseConfigFactory.java
new file mode 100644
index 0000000..f81e718
--- /dev/null
+++ b/camera/core/src/main/java/androidx/camera/core/ExtendableUseCaseConfigFactory.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2019 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.core;
+
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.annotation.RestrictTo.Scope;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * A {@link androidx.camera.core.UseCaseConfigurationFactory} that uses {@link
+ * ConfigurationProvider}s to provide configurations.
+ *
+ * @hide
+ */
+@RestrictTo(Scope.LIBRARY_GROUP)
+public final class ExtendableUseCaseConfigFactory implements UseCaseConfigurationFactory {
+ private final Map<Class<?>, ConfigurationProvider<?>> defaultProviders = new HashMap<>();
+
+ /**
+ * Inserts or overrides the {@link androidx.camera.core.ConfigurationProvider} for the given
+ * config type.
+ */
+ public <C extends Configuration> void installDefaultProvider(
+ Class<C> configType, ConfigurationProvider<C> defaultProvider) {
+ defaultProviders.put(configType, defaultProvider);
+ }
+
+ @Nullable
+ @Override
+ public <C extends UseCaseConfiguration<?>> C getConfiguration(Class<C> configType) {
+ @SuppressWarnings("unchecked") // Providers only could have been inserted with
+ // installDefaultProvider(), so the class should return the correct type.
+ ConfigurationProvider<C> provider =
+ (ConfigurationProvider<C>) defaultProviders.get(configType);
+ if (provider != null) {
+ return provider.getConfiguration();
+ }
+ return null;
+ }
+}
diff --git a/camera/core/src/main/java/androidx/camera/core/FixedSizeSurfaceTexture.java b/camera/core/src/main/java/androidx/camera/core/FixedSizeSurfaceTexture.java
new file mode 100644
index 0000000..e1a8227
--- /dev/null
+++ b/camera/core/src/main/java/androidx/camera/core/FixedSizeSurfaceTexture.java
@@ -0,0 +1,108 @@
+/*
+ * Copyright (C) 2019 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.core;
+
+import android.graphics.SurfaceTexture;
+import android.os.Build.VERSION_CODES;
+import android.util.Size;
+import android.view.Surface;
+
+import androidx.annotation.RequiresApi;
+
+/**
+ * An implementation of {@link SurfaceTexture} with a fixed default buffer size.
+ *
+ * <p>The fixed default buffer size used at construction time cannot be changed through the {@link
+ * #setDefaultBufferSize(int, int)} method.
+ */
+final class FixedSizeSurfaceTexture extends SurfaceTexture {
+
+ /**
+ * Construct a new SurfaceTexture to stream images to a given OpenGL texture.
+ *
+ * @param texName the OpenGL texture object name (e.g. generated via glGenTextures)
+ * @param fixedSize the fixed default buffer size
+ * @throws Surface.OutOfResourcesException If the SurfaceTexture cannot be created.
+ */
+ FixedSizeSurfaceTexture(int texName, Size fixedSize) {
+ super(texName);
+ super.setDefaultBufferSize(fixedSize.getWidth(), fixedSize.getHeight());
+ }
+
+ /**
+ * Construct a new SurfaceTexture to stream images to a given OpenGL texture.
+ *
+ * <p>In single buffered mode the application is responsible for serializing access to the image
+ * content buffer. Each time the image content is to be updated, the {@link #releaseTexImage()}
+ * method must be called before the image content producer takes ownership of the buffer. For
+ * example, when producing image content with the NDK ANativeWindow_lock and
+ * ANativeWindow_unlockAndPost functions, {@link #releaseTexImage()} must be called before each
+ * ANativeWindow_lock, or that call will fail. When producing image content with OpenGL ES,
+ * {@link #releaseTexImage()} must be called before the first OpenGL ES function call each
+ * frame.
+ *
+ * @param texName the OpenGL texture object name (e.g. generated via glGenTextures)
+ * @param singleBufferMode whether the SurfaceTexture will be in single buffered mode.
+ * @param fixedSize the fixed default buffer size
+ * @throws Surface.OutOfResourcesException If the SurfaceTexture cannot be created.
+ */
+ FixedSizeSurfaceTexture(int texName, boolean singleBufferMode, Size fixedSize) {
+ super(texName, singleBufferMode);
+ super.setDefaultBufferSize(fixedSize.getWidth(), fixedSize.getHeight());
+ }
+
+ /**
+ * Construct a new SurfaceTexture to stream images to a given OpenGL texture.
+ *
+ * <p>In single buffered mode the application is responsible for serializing access to the image
+ * content buffer. Each time the image content is to be updated, the {@link #releaseTexImage()}
+ * method must be called before the image content producer takes ownership of the buffer. For
+ * example, when producing image content with the NDK ANativeWindow_lock and
+ * ANativeWindow_unlockAndPost functions, {@link #releaseTexImage()} must be called before each
+ * ANativeWindow_lock, or that call will fail. When producing image content with OpenGL ES,
+ * {@link #releaseTexImage()} must be called before the first OpenGL ES function call each
+ * frame.
+ *
+ * <p>Unlike {@link SurfaceTexture(int, boolean)}, which takes an OpenGL texture object name,
+ * this constructor creates the SurfaceTexture in detached mode. A texture name must be passed
+ * in using {@link #attachToGLContext} before calling {@link #releaseTexImage()} and producing
+ * image content using OpenGL ES.
+ *
+ * @param singleBufferMode whether the SurfaceTexture will be in single buffered mode.
+ * @param fixedSize the fixed default buffer size
+ * @throws Surface.OutOfResourcesException If the SurfaceTexture cannot be created.
+ */
+ @RequiresApi(api = VERSION_CODES.O)
+ FixedSizeSurfaceTexture(boolean singleBufferMode, Size fixedSize) {
+ super(singleBufferMode);
+ super.setDefaultBufferSize(fixedSize.getWidth(), fixedSize.getHeight());
+ }
+
+ /**
+ * This method has no effect.
+ *
+ * <p>Unlike {@link SurfaceTexture}, this method does not affect the default buffer size. The
+ * default buffer size will remain what it was set to during construction.
+ *
+ * @param width ignored width
+ * @param height ignored height
+ */
+ @Override
+ public void setDefaultBufferSize(int width, int height) {
+ // No-op
+ }
+}
diff --git a/camera/core/src/main/java/androidx/camera/core/FlashMode.java b/camera/core/src/main/java/androidx/camera/core/FlashMode.java
new file mode 100644
index 0000000..3b1f277
--- /dev/null
+++ b/camera/core/src/main/java/androidx/camera/core/FlashMode.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2019 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.core;
+
+/** The flash mode options when taking a picture using ImageCaptureUseCase. */
+public enum FlashMode {
+ /**
+ * Auto flash. The flash will be used according to the camera system's determination when taking
+ * a picture.
+ */
+ AUTO,
+ /** Always flash. The flash will always be used when taking a picture. */
+ ON,
+ /** No flash. The flash will never be used when taking a picture. */
+ OFF
+}
diff --git a/camera/core/src/main/java/androidx/camera/core/ForwardingImageProxy.java b/camera/core/src/main/java/androidx/camera/core/ForwardingImageProxy.java
new file mode 100644
index 0000000..f4bb754
--- /dev/null
+++ b/camera/core/src/main/java/androidx/camera/core/ForwardingImageProxy.java
@@ -0,0 +1,123 @@
+/*
+ * Copyright (C) 2019 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.core;
+
+import android.graphics.Rect;
+
+import androidx.annotation.GuardedBy;
+
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * An {@link ImageProxy} which forwards all calls to another {@link ImageProxy}.
+ *
+ * <p>This class enables subclasses to override a few methods to achieve a custom behavior, while
+ * still delegating calls on the remaining methods to a wrapped {@link ImageProxy} instance.
+ *
+ * <p>Listeners for the image close call can be added. When the image is closed, the listeners will
+ * be notified.
+ */
+abstract class ForwardingImageProxy implements ImageProxy {
+ @GuardedBy("this")
+ protected final ImageProxy image;
+
+ @GuardedBy("this")
+ private final Set<OnImageCloseListener> onImageCloseListeners = new HashSet<>();
+
+ /**
+ * Creates a new instance which wraps the given image.
+ *
+ * @param image to wrap
+ * @return new {@link AndroidImageProxy} instance
+ */
+ protected ForwardingImageProxy(ImageProxy image) {
+ this.image = image;
+ }
+
+ @Override
+ public synchronized void close() {
+ image.close();
+ notifyOnImageCloseListeners();
+ }
+
+ @Override
+ public synchronized Rect getCropRect() {
+ return image.getCropRect();
+ }
+
+ @Override
+ public synchronized void setCropRect(Rect rect) {
+ image.setCropRect(rect);
+ }
+
+ @Override
+ public synchronized int getFormat() {
+ return image.getFormat();
+ }
+
+ @Override
+ public synchronized int getHeight() {
+ return image.getHeight();
+ }
+
+ @Override
+ public synchronized int getWidth() {
+ return image.getWidth();
+ }
+
+ @Override
+ public synchronized long getTimestamp() {
+ return image.getTimestamp();
+ }
+
+ @Override
+ public synchronized void setTimestamp(long timestamp) {
+ image.setTimestamp(timestamp);
+ }
+
+ @Override
+ public synchronized ImageProxy.PlaneProxy[] getPlanes() {
+ return image.getPlanes();
+ }
+
+ /**
+ * Adds a listener for close calls on this image.
+ *
+ * @param listener to add
+ */
+ synchronized void addOnImageCloseListener(OnImageCloseListener listener) {
+ onImageCloseListeners.add(listener);
+ }
+
+ /** Notifies the listeners that this image has been closed. */
+ protected synchronized void notifyOnImageCloseListeners() {
+ for (OnImageCloseListener listener : onImageCloseListeners) {
+ listener.onImageClose(this);
+ }
+ }
+
+ /** Listener for the image close event. */
+ interface OnImageCloseListener {
+ /**
+ * Callback for image close.
+ *
+ * @param image which is closed
+ */
+ void onImageClose(ImageProxy image);
+ }
+}
diff --git a/camera/core/src/main/java/androidx/camera/core/ForwardingImageReaderListener.java b/camera/core/src/main/java/androidx/camera/core/ForwardingImageReaderListener.java
new file mode 100644
index 0000000..084561d
--- /dev/null
+++ b/camera/core/src/main/java/androidx/camera/core/ForwardingImageReaderListener.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright (C) 2019 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.core;
+
+import android.media.Image;
+import android.media.ImageReader;
+
+import androidx.annotation.GuardedBy;
+
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * An {@link ImageReader.OnImageAvailableListener} which forks and forwards newly available images
+ * to multiple {@link ImageReaderProxy} instances.
+ */
+final class ForwardingImageReaderListener implements ImageReader.OnImageAvailableListener {
+ @GuardedBy("this")
+ private final List<QueuedImageReaderProxy> imageReaders;
+
+ /**
+ * Creates a new forwarding listener.
+ *
+ * @param imageReaders list of image readers which will receive a copy of every new image
+ * @return new {@link ForwardingImageReaderListener} instance
+ */
+ ForwardingImageReaderListener(List<QueuedImageReaderProxy> imageReaders) {
+ this.imageReaders = Collections.unmodifiableList(imageReaders);
+ }
+
+ @Override
+ public synchronized void onImageAvailable(ImageReader imageReader) {
+ Image image = imageReader.acquireNextImage();
+ ImageProxy imageProxy = new AndroidImageProxy(image);
+ ReferenceCountedImageProxy referenceCountedImageProxy =
+ new ReferenceCountedImageProxy(imageProxy);
+ for (QueuedImageReaderProxy imageReaderProxy : imageReaders) {
+ synchronized (imageReaderProxy) {
+ if (!imageReaderProxy.isClosed()) {
+ ImageProxy forkedImage = referenceCountedImageProxy.fork();
+ ForwardingImageProxy imageToEnqueue =
+ ImageProxyDownsampler.downsample(
+ forkedImage,
+ imageReaderProxy.getWidth(),
+ imageReaderProxy.getHeight(),
+ ImageProxyDownsampler.DownsamplingMethod.AVERAGING);
+ imageReaderProxy.enqueueImage(imageToEnqueue);
+ }
+ }
+ }
+ referenceCountedImageProxy.close();
+ }
+}
diff --git a/camera/core/src/main/java/androidx/camera/core/ImageAnalysisUseCase.java b/camera/core/src/main/java/androidx/camera/core/ImageAnalysisUseCase.java
new file mode 100644
index 0000000..5a5f92a
--- /dev/null
+++ b/camera/core/src/main/java/androidx/camera/core/ImageAnalysisUseCase.java
@@ -0,0 +1,362 @@
+/*
+ * Copyright (C) 2019 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.core;
+
+import android.media.ImageReader;
+import android.os.Handler;
+import android.os.Looper;
+import android.util.Log;
+import android.util.Rational;
+import android.util.Size;
+import android.view.Display;
+import android.view.Surface;
+
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.annotation.RestrictTo.Scope;
+import androidx.annotation.UiThread;
+import androidx.camera.core.CameraX.LensFacing;
+import androidx.camera.core.ImageOutputConfiguration.RotationValue;
+
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicReference;
+
+/**
+ * A use case providing CPU accessible images for an app to perform image analysis on.
+ *
+ * <p>Newly available images are acquired from the camera using an {@link ImageReader}. Each image
+ * is analyzed with an {@link ImageAnalysisUseCase.Analyzer} to produce a result. Then, the image is
+ * closed.
+ *
+ * <p>The result type, as well as distribution of the result, are left up to the implementation of
+ * the {@link Analyzer}.
+ */
+public final class ImageAnalysisUseCase extends BaseUseCase {
+ /**
+ * Provides a static configuration with implementation-agnostic options.
+ *
+ * @hide
+ */
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ public static final Defaults DEFAULT_CONFIG = new Defaults();
+ private static final String TAG = "ImageAnalysisUseCase";
+ private final AtomicReference<Analyzer> subscribedAnalyzer;
+ private final AtomicInteger relativeRotation = new AtomicInteger();
+ private final Handler handler;
+ private final ImageAnalysisUseCaseConfiguration.Builder useCaseConfigBuilder;
+ @Nullable
+ private ImageReaderProxy imageReader;
+ /**
+ * Creates a new image analysis use case from the given configuration.
+ *
+ * @param configuration for this use case instance
+ */
+ public ImageAnalysisUseCase(ImageAnalysisUseCaseConfiguration configuration) {
+ super(configuration);
+ useCaseConfigBuilder = ImageAnalysisUseCaseConfiguration.Builder.fromConfig(configuration);
+
+ // Get the combined configuration with defaults
+ ImageAnalysisUseCaseConfiguration combinedConfig =
+ (ImageAnalysisUseCaseConfiguration) getUseCaseConfiguration();
+ subscribedAnalyzer = new AtomicReference<>();
+ handler = combinedConfig.getCallbackHandler(null);
+ if (handler == null) {
+ throw new IllegalStateException("No default handler specified.");
+ }
+ setImageFormat(ImageReaderFormatRecommender.chooseCombo().imageAnalysisFormat());
+ }
+
+ /**
+ * Removes a previously set analyzer.
+ *
+ * <p>This is equivalent to calling {@code setAnalyzer(null)}. Removing the analyzer will stop
+ * the stream of data from the camera.
+ */
+ @UiThread
+ public void removeAnalyzer() {
+ setAnalyzer(null);
+ }
+
+ /**
+ * Sets the rotation of the analysis pipeline.
+ *
+ * <p>This informs the use case of what the analyzer's reference rotation will be so it can
+ * adjust the rotation value sent to {@link Analyzer#analyze(ImageProxy, int)}.
+ *
+ * <p>In most cases this should be set to the current rotation returned by {@link
+ * Display#getRotation()}.
+ *
+ * @param rotation Desired rotation of the output image.
+ */
+ public void setTargetRotation(@RotationValue int rotation) {
+ ImageAnalysisUseCaseConfiguration oldconfig =
+ (ImageAnalysisUseCaseConfiguration) getUseCaseConfiguration();
+ int oldRotation = oldconfig.getTargetRotation(ImageOutputConfiguration.INVALID_ROTATION);
+ if (oldRotation == ImageOutputConfiguration.INVALID_ROTATION || oldRotation != rotation) {
+ useCaseConfigBuilder.setTargetRotation(rotation);
+ updateUseCaseConfiguration(useCaseConfigBuilder.build());
+
+ // TODO(b/122846516): Update session configuration and possibly reconfigure session.
+ // For now we'll just update the relative rotation value.
+ // Attempt to get the camera ID and update the relative rotation. If we can't, we
+ // probably
+ // don't yet have permission, so we will try again in onSuggestedResolutionUpdated().
+ // Old
+ // configuration lens facing should match new configuration.
+ try {
+ String cameraId = CameraX.getCameraWithLensFacing(oldconfig.getLensFacing());
+ tryUpdateRelativeRotation(cameraId);
+ } catch (CameraInfoUnavailableException e) {
+ // Likely don't yet have permissions. This is expected if this method is called
+ // before
+ // this use case becomes active. That's OK though since we've updated the use case
+ // configuration. We'll try to update relative rotation again in
+ // onSuggestedResolutionUpdated().
+ }
+ }
+ }
+
+ /**
+ * Retrieves a previously set analyzer.
+ *
+ * @return The last set analyzer or {@code null} if no analyzer is set.
+ */
+ @UiThread
+ @Nullable
+ public Analyzer getAnalyzer() {
+ return subscribedAnalyzer.get();
+ }
+
+ /**
+ * Sets an analyzer to receive and analyze images.
+ *
+ * <p>Setting an analyzer will signal to the camera that it should begin sending data. The
+ * stream of data can be stopped by setting the analyzer to {@code null} or by calling {@link
+ * #removeAnalyzer()}.
+ *
+ * <p>Distribution of the result is left up to the implementation of the {@link Analyzer}.
+ *
+ * @param analyzer of the images or {@code null} to stop the stream of data.
+ */
+ @UiThread
+ public void setAnalyzer(@Nullable Analyzer analyzer) {
+ Analyzer previousAnalyzer = subscribedAnalyzer.getAndSet(analyzer);
+ if (previousAnalyzer == null && analyzer != null) {
+ notifyActive();
+ } else if (previousAnalyzer != null && analyzer == null) {
+ notifyInactive();
+ }
+ }
+
+ @Override
+ public String toString() {
+ return TAG + ":" + getName();
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * @hide
+ */
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ @Override
+ public void clear() {
+ if (imageReader != null) {
+ imageReader.close();
+ imageReader = null;
+ }
+ super.clear();
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * @hide
+ */
+ @Override
+ @Nullable
+ protected UseCaseConfiguration.Builder<?, ?, ?> getDefaultBuilder() {
+ ImageAnalysisUseCaseConfiguration defaults =
+ CameraX.getDefaultUseCaseConfiguration(ImageAnalysisUseCaseConfiguration.class);
+ if (defaults != null) {
+ return ImageAnalysisUseCaseConfiguration.Builder.fromConfig(defaults);
+ }
+
+ return null;
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * @hide
+ */
+ @Override
+ protected Map<String, Size> onSuggestedResolutionUpdated(
+ Map<String, Size> suggestedResolutionMap) {
+ ImageAnalysisUseCaseConfiguration configuration =
+ (ImageAnalysisUseCaseConfiguration) getUseCaseConfiguration();
+
+ String cameraId;
+ LensFacing lensFacing = configuration.getLensFacing();
+ try {
+ cameraId = CameraX.getCameraWithLensFacing(lensFacing);
+ } catch (CameraInfoUnavailableException e) {
+ throw new IllegalArgumentException(
+ "Unable to find camera with LensFacing " + lensFacing, e);
+ }
+
+ Size resolution = suggestedResolutionMap.get(cameraId);
+ if (resolution == null) {
+ throw new IllegalArgumentException(
+ "Suggested resolution map missing resolution for camera " + cameraId);
+ }
+
+ if (imageReader != null) {
+ imageReader.close();
+ }
+
+ imageReader =
+ ImageReaderProxys.createCompatibleReader(
+ cameraId,
+ resolution.getWidth(),
+ resolution.getHeight(),
+ getImageFormat(),
+ configuration.getImageQueueDepth(),
+ handler);
+
+ tryUpdateRelativeRotation(cameraId);
+ imageReader.setOnImageAvailableListener(
+ imageReader -> {
+ Analyzer analyzer = subscribedAnalyzer.get();
+ try (ImageProxy image =
+ configuration
+ .getImageReaderMode(configuration.getImageReaderMode())
+ .equals(ImageReaderMode.ACQUIRE_NEXT_IMAGE)
+ ? imageReader.acquireNextImage()
+ : imageReader.acquireLatestImage()) {
+ // Do not analyze if unable to acquire an ImageProxy
+ if (image == null) {
+ return;
+ }
+
+ if (analyzer != null) {
+ analyzer.analyze(image, relativeRotation.get());
+ }
+ }
+ },
+ handler);
+
+ SessionConfiguration.Builder sessionConfigBuilder =
+ SessionConfiguration.Builder.createFrom(configuration);
+ sessionConfigBuilder.addSurface(new ImmediateSurface(imageReader.getSurface()));
+
+ attachToCamera(cameraId, sessionConfigBuilder.build());
+
+ return suggestedResolutionMap;
+ }
+
+ private void tryUpdateRelativeRotation(String cameraId) {
+ ImageOutputConfiguration configuration =
+ (ImageOutputConfiguration) getUseCaseConfiguration();
+ // Get the relative rotation or default to 0 if the camera info is unavailable
+ try {
+ CameraInfo cameraInfo = CameraX.getCameraInfo(cameraId);
+ relativeRotation.set(
+ cameraInfo.getSensorRotationDegrees(
+ configuration.getTargetRotation(Surface.ROTATION_0)));
+ } catch (CameraInfoUnavailableException e) {
+ Log.e(TAG, "Unable to retrieve camera sensor orientation.", e);
+ }
+ }
+
+ /**
+ * The different ways that the image sent to the analyzer is acquired from the underlying {@link
+ * ImageReader}. This corresponds to acquireLatestImage or acquireNextImage in {@link
+ * ImageReader}.
+ *
+ * @see android.media.ImageReader
+ */
+ public enum ImageReaderMode {
+ /** Acquires the latest image in the queue, discarding any images older than the latest. */
+ ACQUIRE_LATEST_IMAGE,
+ /** Acquires the next image in the queue. */
+ ACQUIRE_NEXT_IMAGE,
+ }
+
+ /** An analyzer of images. */
+ public interface Analyzer {
+ /**
+ * Analyzes an image to produce a result.
+ *
+ * <p>The caller is responsible for ensuring this analysis method can be executed quickly
+ * enough to prevent stalls in the image acquisition pipeline. Otherwise, newly available
+ * images will not be acquired and analyzed.
+ *
+ * <p>The image passed to this method becomes invalid after this method returns. The caller
+ * should not store external references to this image, as these references will become
+ * invalid.
+ *
+ * @param image to analyze
+ * @param rotationDegrees The rotation required to match the rotation given by
+ * ImageOutputConfiguration#getTargetRotation(int).
+ */
+ void analyze(ImageProxy image, int rotationDegrees);
+ }
+
+ /**
+ * Provides a base static default configuration for the ImageAnalysisUseCase
+ *
+ * <p>These values may be overridden by the implementation. They only provide a minimum set of
+ * defaults that are implementation independent.
+ *
+ * @hide
+ */
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ public static final class Defaults
+ implements ConfigurationProvider<ImageAnalysisUseCaseConfiguration> {
+ private static final ImageReaderMode DEFAULT_IMAGE_READER_MODE =
+ ImageReaderMode.ACQUIRE_NEXT_IMAGE;
+ private static final Handler DEFAULT_HANDLER = new Handler(Looper.getMainLooper());
+ private static final Rational DEFAULT_ASPECT_RATIO = new Rational(4, 3);
+ private static final int DEFAULT_IMAGE_QUEUE_DEPTH = 6;
+ private static final Size DEFAULT_TARGET_RESOLUTION = new Size(640, 480);
+ private static final Size DEFAULT_MAX_RESOLUTION = new Size(1920, 1080);
+ private static final int DEFAULT_SURFACE_OCCUPANCY_PRIORITY = 1;
+
+ private static final ImageAnalysisUseCaseConfiguration DEFAULT_CONFIG;
+
+ static {
+ ImageAnalysisUseCaseConfiguration.Builder builder =
+ new ImageAnalysisUseCaseConfiguration.Builder()
+ .setImageReaderMode(DEFAULT_IMAGE_READER_MODE)
+ .setCallbackHandler(DEFAULT_HANDLER)
+ .setTargetAspectRatio(DEFAULT_ASPECT_RATIO)
+ .setImageQueueDepth(DEFAULT_IMAGE_QUEUE_DEPTH)
+ .setTargetResolution(DEFAULT_TARGET_RESOLUTION)
+ .setMaxResolution(DEFAULT_MAX_RESOLUTION)
+ .setSurfaceOccupancyPriority(DEFAULT_SURFACE_OCCUPANCY_PRIORITY);
+
+ DEFAULT_CONFIG = builder.build();
+ }
+
+ @Override
+ public ImageAnalysisUseCaseConfiguration getConfiguration() {
+ return DEFAULT_CONFIG;
+ }
+ }
+}
diff --git a/camera/core/src/main/java/androidx/camera/core/ImageAnalysisUseCaseConfiguration.java b/camera/core/src/main/java/androidx/camera/core/ImageAnalysisUseCaseConfiguration.java
new file mode 100644
index 0000000..9826fd4
--- /dev/null
+++ b/camera/core/src/main/java/androidx/camera/core/ImageAnalysisUseCaseConfiguration.java
@@ -0,0 +1,266 @@
+/*
+ * Copyright (C) 2019 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.core;
+
+import android.media.ImageReader;
+import android.util.Size;
+
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.annotation.RestrictTo.Scope;
+import androidx.camera.core.ImageAnalysisUseCase.ImageReaderMode;
+
+/** Configuration for an image analysis use case. */
+public final class ImageAnalysisUseCaseConfiguration
+ implements UseCaseConfiguration<ImageAnalysisUseCase>,
+ ImageOutputConfiguration,
+ CameraDeviceConfiguration,
+ ThreadConfiguration {
+
+ // Option Declarations:
+ // ***********************************************************************************************
+ static final Option<ImageReaderMode> OPTION_IMAGE_READER_MODE =
+ Option.create("camerax.core.imageAnalysis.imageReaderMode", ImageReaderMode.class);
+ static final Option<Integer> OPTION_IMAGE_QUEUE_DEPTH =
+ Option.create("camerax.core.imageAnalysis.imageQueueDepth", int.class);
+ private final OptionsBundle config;
+
+ ImageAnalysisUseCaseConfiguration(OptionsBundle config) {
+ this.config = config;
+ }
+
+ /**
+ * Returns the mode that the image is acquired from {@link ImageReader}.
+ *
+ * <p>The available values are {@link ImageReaderMode#ACQUIRE_NEXT_IMAGE} and {@link
+ * ImageReaderMode#ACQUIRE_LATEST_IMAGE}.
+ *
+ * @param valueIfMissing The value to return if this configuration option has not been set.
+ * @return The stored value or <code>valueIfMissing</code> if the value does not exist in this
+ * configuration.
+ */
+ @Nullable
+ public ImageReaderMode getImageReaderMode(@Nullable ImageReaderMode valueIfMissing) {
+ return getConfiguration().retrieveOption(OPTION_IMAGE_READER_MODE, valueIfMissing);
+ }
+
+ /**
+ * Returns the mode that the image is acquired from {@link ImageReader}.
+ *
+ * <p>The available values are {@link ImageReaderMode#ACQUIRE_NEXT_IMAGE} and {@link
+ * ImageReaderMode#ACQUIRE_LATEST_IMAGE}.
+ *
+ * @return The stored value, if it exists in this configuration.
+ * @throws IllegalArgumentException if the option does not exist in this configuration.
+ */
+ public ImageReaderMode getImageReaderMode() {
+ return getConfiguration().retrieveOption(OPTION_IMAGE_READER_MODE);
+ }
+
+ /**
+ * Returns the number of images available to the camera pipeline.
+ *
+ * <p>The image queue depth is the total number of images, including the image being analyzed,
+ * available to the camera pipeline. If analysis takes long enough, the image queue may become
+ * full and stall the camera pipeline.
+ *
+ * @param valueIfMissing The value to return if this configuration option has not been set.
+ * @return The stored value or <code>valueIfMissing</code> if the value does not exist in this
+ * configuration.
+ */
+ public int getImageQueueDepth(int valueIfMissing) {
+ return getConfiguration().retrieveOption(OPTION_IMAGE_QUEUE_DEPTH, valueIfMissing);
+ }
+
+ /**
+ * Returns the number of images available to the camera pipeline.
+ *
+ * <p>The image queue depth is the total number of images, including the image being analyzed,
+ * available to the camera pipeline. If analysis takes long enough, the image queue may become
+ * full and stall the camera pipeline.
+ *
+ * @return The stored value, if it exists in this configuration.
+ * @throws IllegalArgumentException if the option does not exist in this configuration.
+ */
+ public int getImageQueueDepth() {
+ return getConfiguration().retrieveOption(OPTION_IMAGE_QUEUE_DEPTH);
+ }
+
+ /**
+ * Retrieves the resolution of the target intending to use from this configuration.
+ *
+ * @param valueIfMissing The value to return if this configuration option has not been set.
+ * @return The stored value or <code>valueIfMissing</code> if the value does not exist in this
+ * configuration.
+ */
+ @Override
+ public Size getTargetResolution(Size valueIfMissing) {
+ return getConfiguration()
+ .retrieveOption(ImageOutputConfiguration.OPTION_TARGET_RESOLUTION, valueIfMissing);
+ }
+
+ /**
+ * Retrieves the resolution of the target intending to use from this configuration.
+ *
+ * @return The stored value, if it exists in this configuration.
+ * @throws IllegalArgumentException if the option does not exist in this configuration.
+ */
+ @Override
+ public Size getTargetResolution() {
+ return getConfiguration().retrieveOption(ImageOutputConfiguration.OPTION_TARGET_RESOLUTION);
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * @hide
+ */
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ @Override
+ public Configuration getConfiguration() {
+ return config;
+ }
+
+ /** Builder for a {@link ImageAnalysisUseCaseConfiguration}. */
+ public static final class Builder
+ implements CameraDeviceConfiguration.Builder<
+ ImageAnalysisUseCaseConfiguration, Builder>,
+ ImageOutputConfiguration.Builder<ImageAnalysisUseCaseConfiguration, Builder>,
+ ThreadConfiguration.Builder<ImageAnalysisUseCaseConfiguration, Builder>,
+ UseCaseConfiguration.Builder<
+ ImageAnalysisUseCase, ImageAnalysisUseCaseConfiguration, Builder> {
+ private final MutableOptionsBundle mutableConfig;
+
+ /** Creates a new Builder object. */
+ public Builder() {
+ this(MutableOptionsBundle.create());
+ }
+
+ private Builder(MutableOptionsBundle mutableConfig) {
+ this.mutableConfig = mutableConfig;
+
+ Class<?> oldConfigClass =
+ mutableConfig.retrieveOption(TargetConfiguration.OPTION_TARGET_CLASS, null);
+ if (oldConfigClass != null && !oldConfigClass.equals(ImageAnalysisUseCase.class)) {
+ throw new IllegalArgumentException(
+ "Invalid target class configuration for "
+ + Builder.this
+ + ": "
+ + oldConfigClass);
+ }
+
+ setTargetClass(ImageAnalysisUseCase.class);
+ }
+
+ /**
+ * Generates a Builder from another Configuration object.
+ *
+ * @param configuration An immutable configuration to pre-populate this builder.
+ * @return The new Builder.
+ */
+ public static Builder fromConfig(ImageAnalysisUseCaseConfiguration configuration) {
+ return new Builder(MutableOptionsBundle.from(configuration));
+ }
+
+ /**
+ * Sets the mode that the image is acquired from {@link ImageReader}.
+ *
+ * <p>The available values are {@link ImageReaderMode#ACQUIRE_NEXT_IMAGE} and {@link
+ * ImageReaderMode#ACQUIRE_LATEST_IMAGE}.
+ *
+ * @param mode The mode to set.
+ * @return The current Builder.
+ */
+ public Builder setImageReaderMode(ImageReaderMode mode) {
+ getMutableConfiguration().insertOption(OPTION_IMAGE_READER_MODE, mode);
+ return builder();
+ }
+
+ /**
+ * Sets the number of images available to the camera pipeline.
+ *
+ * <p>The image queue depth is the number of images available to the camera to fill with
+ * data. This includes the image currently being analyzed by {@link
+ * ImageAnalysisUseCase.Analyzer#analyze(ImageProxy, int)}. Increasing the image queue depth
+ * may make camera operation smoother, depending on the {@link ImageReaderMode}, at the cost
+ * of increased memory usage.
+ *
+ * <p>When the {@link ImageReaderMode} is set to {@link
+ * ImageReaderMode#ACQUIRE_LATEST_IMAGE}, increasing the image queue depth will increase the
+ * amount of time available to analyze an image before stalling the capture pipeline.
+ *
+ * <p>When the {@link ImageReaderMode} is set to {@link ImageReaderMode#ACQUIRE_NEXT_IMAGE},
+ * increasing the image queue depth may make the camera pipeline run smoother on systems
+ * under high load. However, the time spent analyzing an image should still be kept under a
+ * single frame period for the current frame rate, on average, to avoid stalling the camera
+ * pipeline.
+ *
+ * @param depth The total number of images available to the camera.
+ * @return The current Builder.
+ */
+ public Builder setImageQueueDepth(int depth) {
+ getMutableConfiguration().insertOption(OPTION_IMAGE_QUEUE_DEPTH, depth);
+ return builder();
+ }
+
+ /**
+ * Sets the resolution of the intended target from this configuration.
+ *
+ * <p>The target resolution attempts to establish a minimum bound for the image resolution.
+ * The actual image resolution will be the closest available resolution in size that is not
+ * smaller than the target resolution, as determined by the Camera implementation. However,
+ * if no resolution exists that is equal to or larger than the target resolution, the
+ * nearest available resolution smaller than the target resolution will be chosen.
+ *
+ * @param resolution The target resolution to choose from supported output sizes list.
+ * @return The current Builder.
+ */
+ @Override
+ public Builder setTargetResolution(Size resolution) {
+ getMutableConfiguration()
+ .insertOption(ImageOutputConfiguration.OPTION_TARGET_RESOLUTION, resolution);
+ return builder();
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * @hide
+ */
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ @Override
+ public MutableConfiguration getMutableConfiguration() {
+ return mutableConfig;
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * @hide
+ */
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ @Override
+ public Builder builder() {
+ return this;
+ }
+
+ @Override
+ public ImageAnalysisUseCaseConfiguration build() {
+ return new ImageAnalysisUseCaseConfiguration(OptionsBundle.from(mutableConfig));
+ }
+ }
+}
diff --git a/camera/core/src/main/java/androidx/camera/core/ImageCaptureUseCase.java b/camera/core/src/main/java/androidx/camera/core/ImageCaptureUseCase.java
new file mode 100644
index 0000000..595c4ae
--- /dev/null
+++ b/camera/core/src/main/java/androidx/camera/core/ImageCaptureUseCase.java
@@ -0,0 +1,1035 @@
+/*
+ * Copyright (C) 2019 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.core;
+
+import android.hardware.camera2.CameraDevice;
+import android.hardware.camera2.CaptureRequest;
+import android.location.Location;
+import android.media.Image;
+import android.media.ImageReader;
+import android.os.Build;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.SystemClock;
+import android.util.Log;
+import android.util.Rational;
+import android.util.Size;
+import android.view.Display;
+import android.view.Surface;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.annotation.RestrictTo.Scope;
+import androidx.annotation.UiThread;
+import androidx.camera.core.CameraCaptureMetaData.AeState;
+import androidx.camera.core.CameraCaptureMetaData.AfMode;
+import androidx.camera.core.CameraCaptureMetaData.AfState;
+import androidx.camera.core.CameraCaptureMetaData.AwbState;
+import androidx.camera.core.CameraCaptureResult.EmptyCameraCaptureResult;
+import androidx.camera.core.CameraX.LensFacing;
+import androidx.camera.core.ImageOutputConfiguration.RotationValue;
+
+import com.google.common.util.concurrent.FluentFuture;
+import com.google.common.util.concurrent.FutureCallback;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.common.util.concurrent.SettableFuture;
+
+import java.io.File;
+import java.util.ArrayDeque;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ThreadFactory;
+import java.util.concurrent.atomic.AtomicInteger;
+
+/**
+ * A use case for taking a picture.
+ *
+ * <p>This class is designed for basic picture taking. It provides simple controls on how a picture
+ * will be taken. The caller is responsible for deciding how to use the captured picture, such as
+ * saving the picture to a file.
+ *
+ * <p>The captured image is made available through an {@link ImageReader} which is passed to an
+ * {@link ImageCaptureUseCase.OnImageCapturedListener}.
+ */
+public class ImageCaptureUseCase extends BaseUseCase {
+ /**
+ * Provides a static configuration with implementation-agnostic options.
+ *
+ * @hide
+ */
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ public static final Defaults DEFAULT_CONFIG = new Defaults();
+ private static final String TAG = "ImageCaptureUseCase";
+ private static final long CHECK_3A_TIMEOUT_IN_MS = 1000L;
+ private static final int MAX_IMAGES = 2;
+ // Empty metadata object used as a placeholder for no user-supplied metadata.
+ // Should be initialized to all default values.
+ private static final Metadata EMPTY_METADATA = new Metadata();
+ final Handler handler;
+ final Handler mainHandler = new Handler(Looper.getMainLooper());
+ private final SessionConfiguration.Builder sessionConfigBuilder;
+ private final ArrayDeque<ImageCaptureRequest> imageCaptureRequests = new ArrayDeque<>();
+ private final ExecutorService executor =
+ Executors.newFixedThreadPool(
+ 1,
+ new ThreadFactory() {
+ private final AtomicInteger id = new AtomicInteger(0);
+
+ @Override
+ public Thread newThread(Runnable r) {
+ return new Thread(
+ r,
+ CameraXThreads.TAG + "image_capture_" + id.getAndIncrement());
+ }
+ });
+ private final CaptureCallbackChecker sessionCallbackChecker = new CaptureCallbackChecker();
+ private final CaptureMode captureMode;
+ private final ImageCaptureUseCaseConfiguration.Builder useCaseConfigBuilder;
+ private ImageCaptureUseCaseConfiguration configuration;
+ private ImageReaderProxy imageReader;
+ /**
+ * A flag to check 3A converged or not.
+ *
+ * <p>In order to speed up the taking picture process, trigger AF / AE should be skipped when
+ * the flag is disabled. Set it to be enabled in the maximum quality mode and disabled in the
+ * minimum latency mode.
+ */
+ private boolean enableCheck3AConverged;
+ /** Current flash mode. */
+ private FlashMode flashMode;
+ /**
+ * Creates a new image capture use case from the given configuration.
+ *
+ * @param userConfiguration for this use case instance
+ */
+ public ImageCaptureUseCase(ImageCaptureUseCaseConfiguration userConfiguration) {
+ super(userConfiguration);
+ useCaseConfigBuilder =
+ ImageCaptureUseCaseConfiguration.Builder.fromConfig(userConfiguration);
+ setImageFormat(ImageReaderFormatRecommender.chooseCombo().imageCaptureFormat());
+ // Ensure we're using the combined configuration (user config + defaults)
+ configuration = (ImageCaptureUseCaseConfiguration) getUseCaseConfiguration();
+ captureMode = configuration.getCaptureMode();
+ flashMode = configuration.getFlashMode();
+
+ if (captureMode == CaptureMode.MAX_QUALITY) {
+ enableCheck3AConverged = true; // check 3A convergence in MAX_QUALITY mode
+ } else if (captureMode == CaptureMode.MIN_LATENCY) {
+ enableCheck3AConverged = false; // skip 3A convergence in MIN_LATENCY mode
+ }
+
+ handler = configuration.getCallbackHandler(null);
+ if (handler == null) {
+ throw new IllegalStateException("No default handler specified.");
+ }
+
+ sessionConfigBuilder = SessionConfiguration.Builder.createFrom(configuration);
+ sessionConfigBuilder.setCameraCaptureCallback(sessionCallbackChecker);
+ }
+
+ private static final String getCameraIdUnchecked(LensFacing lensFacing) {
+ try {
+ return CameraX.getCameraWithLensFacing(lensFacing);
+ } catch (Exception e) {
+ throw new IllegalArgumentException(
+ "Unable to get camera id for camera lens facing " + lensFacing, e);
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * @hide
+ */
+ @Override
+ @Nullable
+ protected UseCaseConfiguration.Builder<?, ?, ?> getDefaultBuilder() {
+ ImageCaptureUseCaseConfiguration defaults =
+ CameraX.getDefaultUseCaseConfiguration(ImageCaptureUseCaseConfiguration.class);
+ if (defaults != null) {
+ return ImageCaptureUseCaseConfiguration.Builder.fromConfig(defaults);
+ }
+
+ return null;
+ }
+
+ private CameraControl getCurrentCameraControl() {
+ String cameraId = getCameraIdUnchecked(configuration.getLensFacing());
+ return getCameraControl(cameraId);
+ }
+
+ /** Configures flash mode to CameraControl once it is ready. */
+ @Override
+ protected void onCameraControlReady(String cameraId) {
+ getCameraControl(cameraId).setFlashMode(flashMode);
+ }
+
+ /**
+ * Get the flash mode.
+ *
+ * @return the {@link FlashMode}.
+ */
+ public FlashMode getFlashMode() {
+ return flashMode;
+ }
+
+ /**
+ * Set the flash mode.
+ *
+ * @param flashMode the {@link FlashMode}.
+ */
+ public void setFlashMode(FlashMode flashMode) {
+ this.flashMode = flashMode;
+ getCurrentCameraControl().setFlashMode(flashMode);
+ }
+
+ /**
+ * Sets target aspect ratio.
+ *
+ * @param aspectRatio New target aspect ratio.
+ */
+ public void setTargetAspectRatio(Rational aspectRatio) {
+ ImageOutputConfiguration oldconfig = (ImageOutputConfiguration) getUseCaseConfiguration();
+ Rational oldRatio = oldconfig.getTargetAspectRatio(null);
+ if (!aspectRatio.equals(oldRatio)) {
+ useCaseConfigBuilder.setTargetAspectRatio(aspectRatio);
+ updateUseCaseConfiguration(useCaseConfigBuilder.build());
+ configuration = (ImageCaptureUseCaseConfiguration) getUseCaseConfiguration();
+
+ // TODO(b/122846516): Reconfigure capture session if the ratio is changed drastically.
+ }
+ }
+
+ /**
+ * Sets the desired rotation of the output image.
+ *
+ * <p>This will affect the rotation of the saved image or the rotation value returned by the
+ * {@link OnImageCapturedListener}.
+ *
+ * <p>In most cases this should be set to the current rotation returned by {@link
+ * Display#getRotation()}.
+ *
+ * @param rotation Desired rotation of the output image.
+ */
+ public void setTargetRotation(@RotationValue int rotation) {
+ ImageOutputConfiguration oldconfig = (ImageOutputConfiguration) getUseCaseConfiguration();
+ int oldRotation = oldconfig.getTargetRotation(ImageOutputConfiguration.INVALID_ROTATION);
+ if (oldRotation == ImageOutputConfiguration.INVALID_ROTATION || oldRotation != rotation) {
+ useCaseConfigBuilder.setTargetRotation(rotation);
+ updateUseCaseConfiguration(useCaseConfigBuilder.build());
+ configuration = (ImageCaptureUseCaseConfiguration) getUseCaseConfiguration();
+
+ // TODO(b/122846516): Update session configuration and possibly reconfigure session.
+ }
+ }
+
+ /**
+ * Captures a new still image.
+ *
+ * <p>The listener's callback will be called only once for every invocation of this method. The
+ * listener is responsible for calling {@link Image#close()} on the returned image.
+ *
+ * @param listener for the newly captured image
+ * @hide
+ */
+ public void takePicture(OnImageCapturedListener listener) {
+ if (Looper.getMainLooper() != Looper.myLooper()) {
+ mainHandler.post(
+ () -> {
+ takePicture(listener);
+ });
+ return;
+ }
+
+ sendImageCaptureRequest(listener, handler);
+ }
+
+ /**
+ * Captures a new still image and saves to disk.
+ *
+ * <p>The listener's callback will be called only once for every invocation of this method.
+ *
+ * @param saveLocation Location to store the newly captured image.
+ * @param imageSavedListener Listener to be called for the newly captured image.
+ */
+ public void takePicture(File saveLocation, OnImageSavedListener imageSavedListener) {
+ takePicture(saveLocation, imageSavedListener, EMPTY_METADATA);
+ }
+
+ /**
+ * Captures a new still image and saves to disk.
+ *
+ * <p>The listener's callback will be called only once for every invocation of this method.
+ *
+ * @param saveLocation Location to store the newly captured image.
+ * @param imageSavedListener Listener to be called for the newly captured image.
+ * @param metadata Metadata to be stored with the saved image. For JPEG this will
+ * be included in
+ * EXIF.
+ */
+ public void takePicture(
+ File saveLocation, OnImageSavedListener imageSavedListener, Metadata metadata) {
+ if (Looper.getMainLooper() != Looper.myLooper()) {
+ mainHandler.post(
+ () -> {
+ takePicture(saveLocation, imageSavedListener, metadata);
+ });
+ return;
+ }
+
+ /*
+ * We need to chain the following callbacks to save the image to disk:
+ *
+ * +-----------------------+
+ * | |
+ * |ImageCaptureUseCase. |
+ * |OnImageCapturedListener|
+ * | |
+ * +-----------+-----------+
+ * |
+ * |
+ * +-----------v-----------+ +----------------------+
+ * | | | |
+ * | ImageSaver. | | ImageCaptureUseCase. |
+ * | OnImageSavedListener +------> OnImageSavedListener |
+ * | | | |
+ * +-----------------------+ +----------------------+
+ */
+
+ // Convert the ImageSaver.OnImageSavedListener to ImageCaptureUseCase.OnImageSavedListener
+ ImageSaver.OnImageSavedListener imageSavedListenerWrapper =
+ new ImageSaver.OnImageSavedListener() {
+ @Override
+ public void onImageSaved(File file) {
+ imageSavedListener.onImageSaved(file);
+ }
+
+ @Override
+ public void onError(
+ ImageSaver.SaveError error, String message, @Nullable Throwable cause) {
+ UseCaseError useCaseError = UseCaseError.UNKNOWN_ERROR;
+ switch (error) {
+ case FILE_IO_FAILED:
+ useCaseError = UseCaseError.FILE_IO_ERROR;
+ break;
+ default:
+ // Keep the useCaseError as UNKNOWN_ERROR
+ break;
+ }
+
+ imageSavedListener.onError(useCaseError, message, cause);
+ }
+ };
+
+ Rational targetRatio = configuration.getTargetAspectRatio();
+
+ // Wrap the ImageCaptureUseCase.OnImageSavedListener with an OnImageCapturedListener so it
+ // can
+ // be put into the capture request queue
+ OnImageCapturedListener imageCaptureCallbackWrapper =
+ new OnImageCapturedListener() {
+ @Override
+ public void onCaptureSuccess(ImageProxy image, int rotationDegrees) {
+ Handler completionHandler = (handler != null) ? handler : mainHandler;
+ IoExecutor.getInstance()
+ .execute(
+ new ImageSaver(
+ image,
+ saveLocation,
+ rotationDegrees,
+ metadata.isReversedHorizontal,
+ metadata.isReversedVertical,
+ metadata.location,
+ targetRatio,
+ imageSavedListenerWrapper,
+ completionHandler));
+ }
+
+ @Override
+ public void onError(
+ UseCaseError error, String message, @Nullable Throwable cause) {
+ imageSavedListener.onError(error, message, cause);
+ }
+ };
+
+ // Always use the mainHandler for the initial callback so we don't need to double post to
+ // another thread
+ sendImageCaptureRequest(imageCaptureCallbackWrapper, mainHandler);
+ }
+
+ @UiThread
+ private void sendImageCaptureRequest(
+ OnImageCapturedListener listener, @Nullable Handler listenerHandler) {
+
+ String cameraId = getCameraIdUnchecked(configuration.getLensFacing());
+
+ // Get the relative rotation or default to 0 if the camera info is unavailable
+ int relativeRotation = 0;
+ try {
+ CameraInfo cameraInfo = CameraX.getCameraInfo(cameraId);
+ relativeRotation =
+ cameraInfo.getSensorRotationDegrees(
+ configuration.getTargetRotation(Surface.ROTATION_0));
+ } catch (CameraInfoUnavailableException e) {
+ Log.e(TAG, "Unable to retrieve camera sensor orientation.", e);
+ }
+
+ imageCaptureRequests.offer(
+ new ImageCaptureRequest(listener, listenerHandler, relativeRotation));
+ if (imageCaptureRequests.size() == 1) {
+ issueImageCaptureRequests();
+ }
+ }
+
+ /** Issues saved ImageCaptureRequest. */
+ @UiThread
+ private void issueImageCaptureRequests() {
+ if (imageCaptureRequests.isEmpty()) {
+ return;
+ }
+ takePictureInternal();
+ }
+
+ /**
+ * The take picture flow.
+ *
+ * <p>There are three steps to take a picture.
+ *
+ * <p>(1) Pre-take picture, which will trigger af/ae scan or open torch if necessary. Then check
+ * 3A converged if necessary.
+ *
+ * <p>(2) Issue take picture single request.
+ *
+ * <p>(3) Post-take picture, which will cancel af/ae scan or close torch if necessary.
+ */
+ private void takePictureInternal() {
+ TakePictureState state = new TakePictureState();
+
+ FluentFuture.from(preTakePicture(state))
+ .transformAsync(v -> issueTakePicture(), executor)
+ .transformAsync(v -> postTakePicture(state), executor)
+ .addCallback(
+ new FutureCallback<Void>() {
+ @Override
+ public void onSuccess(Void result) {
+ }
+
+ @Override
+ public void onFailure(Throwable t) {
+ Log.e(TAG, "takePictureInternal onFailure", t);
+ }
+ },
+ executor);
+ }
+
+ @Override
+ public String toString() {
+ return TAG + ":" + getName();
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * @hide
+ */
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ @Override
+ public void clear() {
+ if (imageReader != null) {
+ imageReader.close();
+ imageReader = null;
+ }
+ executor.shutdown();
+ super.clear();
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * @hide
+ */
+ @Override
+ protected Map<String, Size> onSuggestedResolutionUpdated(
+ Map<String, Size> suggestedResolutionMap) {
+ String cameraId = getCameraIdUnchecked(configuration.getLensFacing());
+ Size resolution = suggestedResolutionMap.get(cameraId);
+ if (resolution == null) {
+ throw new IllegalArgumentException(
+ "Suggested resolution map missing resolution for camera " + cameraId);
+ }
+
+ if (imageReader != null) {
+ if (imageReader.getHeight() == resolution.getHeight()
+ && imageReader.getWidth() == resolution.getWidth()) {
+ // Resolution does not need to be updated. Return early.
+ return suggestedResolutionMap;
+ }
+ imageReader.close();
+ }
+
+ imageReader =
+ ImageReaderProxys.createCompatibleReader(
+ cameraId,
+ resolution.getWidth(),
+ resolution.getHeight(),
+ getImageFormat(),
+ MAX_IMAGES,
+ handler);
+
+ imageReader.setOnImageAvailableListener(
+ imageReader -> {
+ // Call the listener so that the captured image can be processed.
+ ImageCaptureRequest imageCaptureRequest = imageCaptureRequests.peek();
+ if (imageCaptureRequest != null) {
+ ImageProxy image = null;
+ try {
+ image = imageReader.acquireLatestImage();
+ } catch (IllegalStateException e) {
+ Log.e(TAG, "Failed to acquire latest image.", e);
+ } finally {
+ if (image != null) {
+ // Remove the first listener from the queue
+ imageCaptureRequests.poll();
+
+ // Inform the listener
+ imageCaptureRequest.dispatchImage(image);
+
+ issueImageCaptureRequests();
+ }
+ }
+ } else {
+ // Flush the queue if we have no requests
+ ImageProxy image = null;
+ try {
+ image = imageReader.acquireLatestImage();
+ } catch (IllegalStateException e) {
+ Log.e(TAG, "Failed to acquire latest image.", e);
+ } finally {
+ if (image != null) {
+ image.close();
+ }
+ }
+ }
+ },
+ mainHandler);
+
+ sessionConfigBuilder.clearSurfaces();
+ sessionConfigBuilder.addNonRepeatingSurface(new ImmediateSurface(imageReader.getSurface()));
+
+ attachToCamera(cameraId, sessionConfigBuilder.build());
+
+ // In order to speed up the take picture process, notifyActive at an early stage to attach
+ // the
+ // session capture callback to repeating and get capture result all the time.
+ notifyActive();
+
+ return suggestedResolutionMap;
+ }
+
+ /**
+ * Routine before taking picture.
+ *
+ * <p>For example, trigger 3A scan, open torch and check 3A converged if necessary.
+ */
+ private ListenableFuture<Void> preTakePicture(TakePictureState state) {
+ return FluentFuture.from(getPreCaptureStateIfNeeded())
+ .transformAsync(
+ captureResult -> {
+ state.preCaptureState = captureResult;
+ triggerAfIfNeeded(state);
+
+ if (isFlashRequired(state)) {
+ state.isFlashTriggered = true;
+ triggerAePrecapture(state);
+ }
+ return check3AConverged(state);
+ },
+ executor)
+ // Ignore the 3A convergence result.
+ .transform(is3AConverged -> null, executor);
+ }
+
+ /**
+ * Routine after picture was taken.
+ *
+ * <p>For example, cancel 3A scan, close torch if necessary.
+ */
+ private ListenableFuture<Void> postTakePicture(TakePictureState state) {
+ return Futures.submitAsync(
+ () -> {
+ cancelAfAeTrigger(state);
+ return Futures.immediateFuture(null);
+ },
+ executor);
+ }
+
+ /**
+ * Gets a capture result or not according to current configuration.
+ *
+ * <p>Conditions to get a capture result.
+ *
+ * <p>(1) The enableCheck3AConverged is enabled because it needs to know current AF mode and
+ * state.
+ *
+ * <p>(2) The flashMode is AUTO because it needs to know the current AE state.
+ */
+ // Currently this method is used to prevent there is no repeating surface to get capture result.
+ // If app is in min-latency mode and flash ALWAYS/OFF mode, it can still take picture without
+ // checking the capture result. Remove this check once no repeating surface issue is fixed.
+ private ListenableFuture<CameraCaptureResult> getPreCaptureStateIfNeeded() {
+ if (enableCheck3AConverged || getFlashMode() == FlashMode.AUTO) {
+ return sessionCallbackChecker.checkCaptureResult((captureResult) -> captureResult);
+ }
+ return Futures.immediateFuture(null);
+ }
+
+ private boolean isFlashRequired(TakePictureState state) {
+ switch (getFlashMode()) {
+ case ON:
+ return true;
+ case AUTO:
+ return state.preCaptureState.getAeState() == AeState.FLASH_REQUIRED;
+ case OFF:
+ return false;
+ }
+ throw new AssertionError(getFlashMode());
+ }
+
+ private ListenableFuture<Boolean> check3AConverged(TakePictureState state) {
+ // Besides enableCheck3AConverged == true (MAX_QUALITY), if flash is triggered we also need
+ // to
+ // wait for 3A convergence.
+ if (!enableCheck3AConverged && !state.isFlashTriggered) {
+ return Futures.immediateFuture(false);
+ }
+
+ return sessionCallbackChecker.checkCaptureResult(
+ (captureResult) -> {
+ // If afMode is CAF, don't check af locked to speed up.
+ if ((captureResult.getAfMode() == AfMode.ON_CONTINUOUS_AUTO
+ || (captureResult.getAfState() == AfState.FOCUSED
+ || captureResult.getAfState() == AfState.LOCKED_FOCUSED
+ || captureResult.getAfState()
+ == AfState.LOCKED_NOT_FOCUSED))
+ && captureResult.getAeState() == AeState.CONVERGED
+ && captureResult.getAwbState() == AwbState.CONVERGED) {
+ return true;
+ }
+ // Return null to continue check.
+ return null;
+ },
+ CHECK_3A_TIMEOUT_IN_MS,
+ false);
+ }
+
+ /**
+ * Issues the AF scan if needed.
+ *
+ * <p>If enableCheck3AConverged is disabled or it is in CAF mode, AF scan should not be
+ * triggered. Trigger AF scan only in {@link AfMode#ON_MANUAL_AUTO} and current AF state is
+ * {@link AfState#INACTIVE}. If the AF mode is {@link AfMode#ON_MANUAL_AUTO} and AF state is not
+ * inactive, it means that a manual or auto focus request may be in progress or completed.
+ */
+ private void triggerAfIfNeeded(TakePictureState state) {
+ if (enableCheck3AConverged
+ && state.preCaptureState.getAfMode() == AfMode.ON_MANUAL_AUTO
+ && state.preCaptureState.getAfState() == AfState.INACTIVE) {
+ triggerAf(state);
+ }
+ }
+
+ /**
+ * Issues a {@link CaptureRequest#CONTROL_AF_TRIGGER_START} request to start auto focus scan.
+ */
+ private void triggerAf(TakePictureState state) {
+ state.isAfTriggered = true;
+ getCurrentCameraControl().triggerAf();
+ }
+
+ /**
+ * Issues a {@link CaptureRequest#CONTROL_AE_PRECAPTURE_TRIGGER_START} request to start auto
+ * exposure scan.
+ */
+ private void triggerAePrecapture(TakePictureState state) {
+ state.isAePrecaptureTriggered = true;
+ getCurrentCameraControl().triggerAePrecapture();
+ }
+
+ /**
+ * Issues {@link CaptureRequest#CONTROL_AF_TRIGGER_CANCEL} or {@link
+ * CaptureRequest#CONTROL_AE_PRECAPTURE_TRIGGER_CANCEL} request to cancel auto focus or auto
+ * exposure scan.
+ */
+ private void cancelAfAeTrigger(TakePictureState state) {
+ if (!state.isAfTriggered && !state.isAePrecaptureTriggered) {
+ return;
+ }
+ getCurrentCameraControl()
+ .cancelAfAeTrigger(state.isAfTriggered, state.isAePrecaptureTriggered);
+ state.isAfTriggered = false;
+ state.isAePrecaptureTriggered = false;
+ }
+
+ // TODO(b/123897971): move the device specific code once we complete the device workaround
+ // module.
+ private void applyPixelHdrPlusChangeForCaptureMode(
+ CaptureMode captureMode, CaptureRequestConfiguration.Builder takePhotoRequestBuilder) {
+ if (Build.MANUFACTURER.equals("Google")
+ && (Build.MODEL.equals("Pixel 2") || Build.MODEL.equals("Pixel 3"))) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ switch (captureMode) {
+ case MAX_QUALITY:
+ // enable ZSL to make sure HDR+ is enabled
+ takePhotoRequestBuilder.addCharacteristic(
+ CaptureRequest.CONTROL_ENABLE_ZSL, true);
+ break;
+ case MIN_LATENCY:
+ // disable ZSL to turn off HDR+
+ takePhotoRequestBuilder.addCharacteristic(
+ CaptureRequest.CONTROL_ENABLE_ZSL, false);
+ break;
+ }
+ }
+ }
+ }
+
+ /** Issues a take picture request. */
+ private ListenableFuture<Void> issueTakePicture() {
+ CaptureRequestConfiguration.Builder builder = new CaptureRequestConfiguration.Builder();
+ builder.addSurface(new ImmediateSurface(imageReader.getSurface()));
+ builder.setTemplateType(CameraDevice.TEMPLATE_STILL_CAPTURE);
+
+ applyPixelHdrPlusChangeForCaptureMode(captureMode, builder);
+
+ SettableFuture<Void> future = SettableFuture.create();
+ builder.setCameraCaptureCallback(
+ new CameraCaptureCallback() {
+ @Override
+ public void onCaptureCompleted(@NonNull CameraCaptureResult result) {
+ future.set(null);
+ }
+
+ @Override
+ public void onCaptureFailed(@NonNull CameraCaptureFailure failure) {
+ Log.e(
+ TAG,
+ "capture picture get onCaptureFailed with reason "
+ + failure.getReason());
+ future.set(null);
+ }
+ });
+ notifySingleCapture(builder.build());
+ return future;
+ }
+
+ /**
+ * Describes the error that occurred during an image capture operation (such as {@link
+ * ImageCaptureUseCase.takePicture()}).
+ *
+ * <p>This is a parameter sent to the error callback functions set in listeners such as {@link
+ * ImageCaptureUseCase.OnImageSavedListener.onError}.
+ */
+ public enum UseCaseError {
+ /**
+ * An unknown error occurred.
+ *
+ * <p>See message parameter in onError callback or log for more details.
+ */
+ UNKNOWN_ERROR,
+ /**
+ * An error occurred while attempting to read or write a file, such as when saving an image
+ * to a File.
+ */
+ FILE_IO_ERROR
+ }
+
+ /**
+ * Capture mode options for ImageCaptureUseCase. A picture will always be taken regardless of
+ * mode, and the mode will be used on devices that support it.
+ */
+ public enum CaptureMode {
+ /**
+ * Optimizes capture pipeline to prioritize image quality over latency. When the capture
+ * mode is set to MAX_QUALITY, images may take longer to capture.
+ */
+ MAX_QUALITY,
+ /**
+ * Optimizes capture pipeline to prioritize latency over image quality. When the capture
+ * mode is set to MIN_LATENCY, images may capture faster but the image quality may be
+ * reduced.
+ */
+ MIN_LATENCY
+ }
+
+ /** Listener containing callbacks for image file I/O events. */
+ public interface OnImageSavedListener {
+ /** Called when an image has been successfully saved. */
+ void onImageSaved(@NonNull File file);
+
+ /** Called when an error occurs while attempting to save an image. */
+ void onError(
+ @NonNull UseCaseError useCaseError,
+ @NonNull String message,
+ @Nullable Throwable cause);
+ }
+
+ /**
+ * Listener called when an image capture has completed.
+ *
+ * @hide
+ */
+ public interface OnImageCapturedListener {
+ /**
+ * Callback for when the image has been captured.
+ *
+ * <p>The listener is responsible for closing the supplied {@link Image}.
+ */
+ default void onCaptureSuccess(ImageProxy image, int rotationDegrees) {
+ image.close();
+ }
+
+ /** Callback for when an error occurred during image capture. */
+ default void onError(
+ UseCaseError useCaseError, String message, @Nullable Throwable cause) {
+ }
+ }
+
+ /**
+ * Provides a base static default configuration for the ImageCaptureUseCase
+ *
+ * <p>These values may be overridden by the implementation. They only provide a minimum set of
+ * defaults that are implementation independent.
+ *
+ * @hide
+ */
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ public static final class Defaults
+ implements ConfigurationProvider<ImageCaptureUseCaseConfiguration> {
+ private static final CaptureMode DEFAULT_CAPTURE_MODE = CaptureMode.MIN_LATENCY;
+ private static final FlashMode DEFAULT_FLASH_MODE = FlashMode.OFF;
+ private static final Handler DEFAULT_HANDLER = new Handler(Looper.getMainLooper());
+ private static final Rational DEFAULT_ASPECT_RATIO = new Rational(4, 3);
+ private static final int DEFAULT_SURFACE_OCCUPANCY_PRIORITY = 4;
+
+ private static final ImageCaptureUseCaseConfiguration DEFAULT_CONFIG;
+
+ static {
+ ImageCaptureUseCaseConfiguration.Builder builder =
+ new ImageCaptureUseCaseConfiguration.Builder()
+ .setCaptureMode(DEFAULT_CAPTURE_MODE)
+ .setFlashMode(DEFAULT_FLASH_MODE)
+ .setCallbackHandler(DEFAULT_HANDLER)
+ .setTargetAspectRatio(DEFAULT_ASPECT_RATIO)
+ .setSurfaceOccupancyPriority(DEFAULT_SURFACE_OCCUPANCY_PRIORITY);
+
+ DEFAULT_CONFIG = builder.build();
+ }
+
+ @Override
+ public ImageCaptureUseCaseConfiguration getConfiguration() {
+ return DEFAULT_CONFIG;
+ }
+ }
+
+ /** Holder class for metadata that will be saved with captured images. */
+ public static final class Metadata {
+ /**
+ * Indicates an upside down mirroring, equivalent to a horizontal mirroring (reflection)
+ * followed by a 180 degree rotation.
+ */
+ public boolean isReversedHorizontal;
+ /** Indicates a left-right mirroring (reflection). */
+ public boolean isReversedVertical;
+ /** Data representing a geographic location. */
+ public @Nullable
+ Location location;
+ }
+
+ /**
+ * An intermediate action recorder while taking picture. It is used to restore certain states.
+ * For example, cancel AF/AE scan, and close flash light.
+ */
+ static final class TakePictureState {
+ CameraCaptureResult preCaptureState = EmptyCameraCaptureResult.create();
+ boolean isAfTriggered = false;
+ boolean isAePrecaptureTriggered = false;
+ boolean isFlashTriggered = false;
+ }
+
+ /**
+ * A helper class to check camera capture result.
+ *
+ * <p>CaptureCallbackChecker is an implementation of {@link CameraCaptureCallback} that checks a
+ * specified list of condition and sets a ListenableFuture when the conditions have been met. It
+ * is mainly used to continuously capture callbacks to detect specific conditions. It also
+ * handles the timeout condition if the check condition does not satisfy the given timeout, and
+ * returns the given default value if the timeout is met.
+ */
+ static final class CaptureCallbackChecker extends CameraCaptureCallback {
+ private static final long NO_TIMEOUT = 0L;
+
+ /** Capture listeners. */
+ private final Set<CaptureResultListener> captureResultListeners = new HashSet<>();
+
+ @Override
+ public void onCaptureCompleted(@NonNull CameraCaptureResult cameraCaptureResult) {
+ deliverCaptureResultToListeners(cameraCaptureResult);
+ }
+
+ /**
+ * Check the capture results of current session capture callback by giving a {@link
+ * CaptureResultChecker}.
+ *
+ * @param checker a CaptureResult checker that returns an object with type T if the check is
+ * complete, returning null to continue the check process.
+ * @param <T> the type parameter for CaptureResult checker.
+ * @return a listenable future for capture result check process.
+ */
+ public <T> ListenableFuture<T> checkCaptureResult(CaptureResultChecker<T> checker) {
+ return checkCaptureResult(checker, NO_TIMEOUT, null);
+ }
+
+ /**
+ * Check the capture results of current session capture callback with timeout limit by
+ * giving a {@link CaptureResultChecker}.
+ *
+ * @param checker a CaptureResult checker that returns an object with type T if the
+ * check is
+ * complete, returning null to continue the check process.
+ * @param timeoutInMs used to force stop checking.
+ * @param defValue the default return value if timeout occur.
+ * @param <T> the type parameter for CaptureResult checker.
+ * @return a listenable future for capture result check process.
+ */
+ public <T> ListenableFuture<T> checkCaptureResult(
+ CaptureResultChecker<T> checker, long timeoutInMs, T defValue) {
+ if (timeoutInMs < NO_TIMEOUT) {
+ throw new IllegalArgumentException("Invalid timeout value: " + timeoutInMs);
+ }
+ long startTimeInMs = (timeoutInMs != NO_TIMEOUT) ? SystemClock.elapsedRealtime() : 0L;
+
+ SettableFuture<T> future = SettableFuture.create();
+ addListener(
+ (captureResult) -> {
+ T result = checker.check(captureResult);
+ if (result != null) {
+ future.set(result);
+ return true;
+ } else if (startTimeInMs > 0
+ && SystemClock.elapsedRealtime() - startTimeInMs > timeoutInMs) {
+ future.set(defValue);
+ return true;
+ }
+ // Return false to continue check.
+ return false;
+ });
+ return future;
+ }
+
+ /**
+ * Delivers camera capture result to {@link CaptureCallbackChecker#captureResultListeners}.
+ */
+ private void deliverCaptureResultToListeners(@NonNull CameraCaptureResult captureResult) {
+ synchronized (captureResultListeners) {
+ Set<CaptureResultListener> removeSet = null;
+ for (CaptureResultListener listener : new HashSet<>(captureResultListeners)) {
+ // Remove listener if the callback return true
+ if (listener.onCaptureResult(captureResult)) {
+ if (removeSet == null) {
+ removeSet = new HashSet<>();
+ }
+ removeSet.add(listener);
+ }
+ }
+ if (removeSet != null) {
+ captureResultListeners.removeAll(removeSet);
+ }
+ }
+ }
+
+ /** Add capture result listener. */
+ private void addListener(CaptureResultListener listener) {
+ synchronized (captureResultListeners) {
+ captureResultListeners.add(listener);
+ }
+ }
+
+ /** An interface to check camera capture result. */
+ public interface CaptureResultChecker<T> {
+
+ /**
+ * The callback to check camera capture result.
+ *
+ * @param captureResult the camera capture result.
+ * @return the check result, return null to continue checking.
+ */
+ T check(@NonNull CameraCaptureResult captureResult);
+ }
+
+ /** An interface to listen to camera capture results. */
+ private interface CaptureResultListener {
+
+ /**
+ * Callback to handle camera capture results.
+ *
+ * @param captureResult camera capture result.
+ * @return true to finish listening, false to continue listening.
+ */
+ boolean onCaptureResult(@NonNull CameraCaptureResult captureResult);
+ }
+ }
+
+ private final class ImageCaptureRequest {
+ OnImageCapturedListener listener;
+ @Nullable
+ Handler handler;
+ @RotationValue
+ int rotationDegrees;
+
+ ImageCaptureRequest(
+ OnImageCapturedListener listener,
+ @Nullable Handler handler,
+ @RotationValue int rotationDegrees) {
+ this.listener = listener;
+ this.handler = handler;
+ this.rotationDegrees = rotationDegrees;
+ }
+
+ void dispatchImage(ImageProxy image) {
+ if (handler != null && Looper.myLooper() != handler.getLooper()) {
+ boolean posted =
+ handler.post(
+ () -> {
+ dispatchImage(image);
+ });
+ // Unable to post to the supplied handler, close the image.
+ if (!posted) {
+ Log.e(TAG, "Unable to post to the supplied handler.");
+ image.close();
+ }
+ return;
+ }
+
+ listener.onCaptureSuccess(image, rotationDegrees);
+ }
+ }
+}
diff --git a/camera/core/src/main/java/androidx/camera/core/ImageCaptureUseCaseConfiguration.java b/camera/core/src/main/java/androidx/camera/core/ImageCaptureUseCaseConfiguration.java
new file mode 100644
index 0000000..4adf610
--- /dev/null
+++ b/camera/core/src/main/java/androidx/camera/core/ImageCaptureUseCaseConfiguration.java
@@ -0,0 +1,195 @@
+/*
+ * Copyright (C) 2019 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.core;
+
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.annotation.RestrictTo.Scope;
+import androidx.camera.core.ImageCaptureUseCase.CaptureMode;
+
+/** Configuration for an image capture use case. */
+public final class ImageCaptureUseCaseConfiguration
+ implements UseCaseConfiguration<ImageCaptureUseCase>,
+ ImageOutputConfiguration,
+ CameraDeviceConfiguration,
+ ThreadConfiguration {
+
+ // Option Declarations:
+ // ***********************************************************************************************
+ static final Option<ImageCaptureUseCase.CaptureMode> OPTION_IMAGE_CAPTURE_MODE =
+ Option.create(
+ "camerax.core.imageCapture.captureMode", ImageCaptureUseCase.CaptureMode.class);
+ static final Option<FlashMode> OPTION_FLASH_MODE =
+ Option.create("camerax.core.imageCapture.flashMode", FlashMode.class);
+ private final OptionsBundle config;
+
+ /** Creates a new configuration instance. */
+ ImageCaptureUseCaseConfiguration(OptionsBundle config) {
+ this.config = config;
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * @hide
+ */
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ @Override
+ public Configuration getConfiguration() {
+ return config;
+ }
+
+ /**
+ * Returns the {@link ImageCaptureUseCase.CaptureMode}.
+ *
+ * @param valueIfMissing The value to return if this configuration option has not been set.
+ * @return The stored value or <code>valueIfMissing</code> if the value does not exist in this
+ * configuration.
+ */
+ @Nullable
+ public ImageCaptureUseCase.CaptureMode getCaptureMode(
+ @Nullable ImageCaptureUseCase.CaptureMode valueIfMissing) {
+ return getConfiguration().retrieveOption(OPTION_IMAGE_CAPTURE_MODE, valueIfMissing);
+ }
+
+ /**
+ * Returns the {@link ImageCaptureUseCase.CaptureMode}.
+ *
+ * @return The stored value, if it exists in this configuration.
+ * @throws IllegalArgumentException if the option does not exist in this configuration.
+ */
+ public ImageCaptureUseCase.CaptureMode getCaptureMode() {
+ return getConfiguration().retrieveOption(OPTION_IMAGE_CAPTURE_MODE);
+ }
+
+ /**
+ * Returns the {@link FlashMode}.
+ *
+ * @param valueIfMissing The value to return if this configuration option has not been set.
+ * @return The stored value or <code>valueIfMissing</code> if the value does not exist in this
+ * configuration.
+ */
+ @Nullable
+ public FlashMode getFlashMode(@Nullable FlashMode valueIfMissing) {
+ return getConfiguration().retrieveOption(OPTION_FLASH_MODE, valueIfMissing);
+ }
+
+ /**
+ * Returns the {@link FlashMode}.
+ *
+ * @return The stored value, if it exists in this configuration.
+ * @throws IllegalArgumentException if the option does not exist in this configuration.
+ */
+ public FlashMode getFlashMode() {
+ return getConfiguration().retrieveOption(OPTION_FLASH_MODE);
+ }
+
+ /** Builder for a {@link ImageCaptureUseCaseConfiguration}. */
+ public static final class Builder
+ implements UseCaseConfiguration.Builder<
+ ImageCaptureUseCase, ImageCaptureUseCaseConfiguration, Builder>,
+ ImageOutputConfiguration.Builder<ImageCaptureUseCaseConfiguration, Builder>,
+ CameraDeviceConfiguration.Builder<ImageCaptureUseCaseConfiguration, Builder>,
+ ThreadConfiguration.Builder<ImageCaptureUseCaseConfiguration, Builder> {
+
+ private final MutableOptionsBundle mutableConfig;
+
+ /** Creates a new Builder object. */
+ public Builder() {
+ this(MutableOptionsBundle.create());
+ }
+
+ private Builder(MutableOptionsBundle mutableConfig) {
+ this.mutableConfig = mutableConfig;
+
+ Class<?> oldConfigClass =
+ mutableConfig.retrieveOption(TargetConfiguration.OPTION_TARGET_CLASS, null);
+ if (oldConfigClass != null && !oldConfigClass.equals(ImageCaptureUseCase.class)) {
+ throw new IllegalArgumentException(
+ "Invalid target class configuration for "
+ + Builder.this
+ + ": "
+ + oldConfigClass);
+ }
+
+ setTargetClass(ImageCaptureUseCase.class);
+ }
+
+ /**
+ * Generates a Builder from another Configuration object
+ *
+ * @param configuration An immutable configuration to pre-populate this builder.
+ * @return The new Builder.
+ */
+ public static Builder fromConfig(ImageCaptureUseCaseConfiguration configuration) {
+ return new Builder(MutableOptionsBundle.from(configuration));
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * @hide
+ */
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ @Override
+ public MutableConfiguration getMutableConfiguration() {
+ return mutableConfig;
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * @hide
+ */
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ @Override
+ public Builder builder() {
+ return this;
+ }
+
+ @Override
+ public ImageCaptureUseCaseConfiguration build() {
+ return new ImageCaptureUseCaseConfiguration(OptionsBundle.from(mutableConfig));
+ }
+
+ /**
+ * Sets the image capture mode.
+ *
+ * <p>Valid capture modes are {@link CaptureMode#MIN_LATENCY}, which prioritizes latency
+ * over image quality, or {@link CaptureMode#MAX_QUALITY}, which prioritizes image quality
+ * over latency.
+ *
+ * @param captureMode The requested image capture mode.
+ * @return The current Builder.
+ */
+ public Builder setCaptureMode(ImageCaptureUseCase.CaptureMode captureMode) {
+ getMutableConfiguration().insertOption(OPTION_IMAGE_CAPTURE_MODE, captureMode);
+ return builder();
+ }
+
+ /**
+ * Sets the {@link FlashMode}.
+ *
+ * @param flashMode The requested flash mode.
+ * @return The current Builder.
+ */
+ public Builder setFlashMode(FlashMode flashMode) {
+ getMutableConfiguration().insertOption(OPTION_FLASH_MODE, flashMode);
+ return builder();
+ }
+ }
+}
diff --git a/camera/core/src/main/java/androidx/camera/core/ImageFormatConstants.java b/camera/core/src/main/java/androidx/camera/core/ImageFormatConstants.java
new file mode 100644
index 0000000..d846ffb
--- /dev/null
+++ b/camera/core/src/main/java/androidx/camera/core/ImageFormatConstants.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2019 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.core;
+
+import androidx.annotation.RestrictTo;
+import androidx.annotation.RestrictTo.Scope;
+
+/**
+ * This class used to constant values corresponding to the internal defined image format value used
+ * in StreamConfigurationMap.java.
+ *
+ * @hide
+ */
+@RestrictTo(Scope.LIBRARY_GROUP)
+public final class ImageFormatConstants {
+ // Internal format in StreamConfigurationMap.java that will be mapped to public ImageFormat.JPEG
+ public static final int INTERNAL_DEFINED_IMAGE_FORMAT_JPEG = 0x21;
+
+ // Internal format HAL_PIXEL_FORMAT_IMPLEMENTATION_DEFINED (0x22) in StreamConfigurationMap.java
+ // that will be mapped to public ImageFormat.PRIVATE after android level 23.
+ public static final int INTERNAL_DEFINED_IMAGE_FORMAT_PRIVATE = 0x22;
+
+ private ImageFormatConstants() {
+ }
+
+ ;
+}
diff --git a/camera/core/src/main/java/androidx/camera/core/ImageOutputConfiguration.java b/camera/core/src/main/java/androidx/camera/core/ImageOutputConfiguration.java
new file mode 100644
index 0000000..8db7eb3
--- /dev/null
+++ b/camera/core/src/main/java/androidx/camera/core/ImageOutputConfiguration.java
@@ -0,0 +1,266 @@
+/*
+ * Copyright (C) 2019 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.core;
+
+import android.util.Rational;
+import android.util.Size;
+import android.view.Surface;
+
+import androidx.annotation.IntDef;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.annotation.RestrictTo.Scope;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/** Configuration containing options for configuring the output image data of a pipeline. */
+public interface ImageOutputConfiguration extends Configuration.Reader {
+ /**
+ * Invalid integer rotation.
+ *
+ * @hide
+ */
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ int INVALID_ROTATION = -1;
+ /**
+ * Option: camerax.core.imageOutput.targetAspectRatio
+ *
+ * @hide
+ */
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ Option<Rational> OPTION_TARGET_ASPECT_RATIO =
+ Option.create("camerax.core.imageOutput.targetAspectRatio", Rational.class);
+ /**
+ * Option: camerax.core.imageOutput.targetRotation
+ *
+ * @hide
+ */
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ Option<Integer> OPTION_TARGET_ROTATION =
+ Option.create("camerax.core.imageOutput.targetRotation", int.class);
+ /**
+ * Option: camerax.core.imageOutput.targetResolution
+ *
+ * @hide
+ */
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ Option<Size> OPTION_TARGET_RESOLUTION =
+ Option.create("camerax.core.imageOutput.targetResolution", Size.class);
+ /**
+ * Option: camerax.core.imageOutput.maxResolution
+ *
+ * @hide
+ */
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ Option<Size> OPTION_MAX_RESOLUTION =
+ Option.create("camerax.core.imageOutput.maxResolution", Size.class);
+
+ /**
+ * Retrieves the aspect ratio of the target intending to use images from this configuration.
+ *
+ * <p>This is the ratio of the target's width to the image's height, where the numerator of the
+ * provided {@link Rational} corresponds to the width, and the denominator corresponds to the
+ * height.
+ *
+ * @param valueIfMissing The value to return if this configuration option has not been set.
+ * @return The stored value or <code>valueIfMissing</code> if the value does not exist in this
+ * configuration.
+ */
+ @Nullable
+ default Rational getTargetAspectRatio(@Nullable Rational valueIfMissing) {
+ return retrieveOption(OPTION_TARGET_ASPECT_RATIO, valueIfMissing);
+ }
+
+ /**
+ * Retrieves the aspect ratio of the target intending to use images from this configuration.
+ *
+ * <p>This is the ratio of the target's width to the image's height, where the numerator of the
+ * provided {@link Rational} corresponds to the width, and the denominator corresponds to the
+ * height.
+ *
+ * @return The stored value, if it exists in this configuration.
+ * @throws IllegalArgumentException if the option does not exist in this configuration.
+ */
+ default Rational getTargetAspectRatio() {
+ return retrieveOption(OPTION_TARGET_ASPECT_RATIO);
+ }
+
+ /**
+ * Retrieves the rotation of the target intending to use images from this configuration.
+ *
+ * <p>This is one of four valid values: {@link Surface#ROTATION_0}, {@link Surface#ROTATION_90},
+ * {@link Surface#ROTATION_180}, {@link Surface#ROTATION_270}. Rotation values are relative to
+ * the device's "natural" rotation, {@link Surface#ROTATION_0}.
+ *
+ * @param valueIfMissing The value to return if this configuration option has not been set.
+ * @return The stored value or <code>valueIfMissing</code> if the value does not exist in this
+ * configuration.
+ */
+ @RotationValue
+ default int getTargetRotation(int valueIfMissing) {
+ return retrieveOption(OPTION_TARGET_ROTATION, valueIfMissing);
+ }
+
+ /**
+ * Retrieves the rotation of the target intending to use images from this configuration.
+ *
+ * <p>This is one of four valid values: {@link Surface#ROTATION_0}, {@link Surface#ROTATION_90},
+ * {@link Surface#ROTATION_180}, {@link Surface#ROTATION_270}. Rotation values are relative to
+ * the device's "natural" rotation, {@link Surface#ROTATION_0}.
+ *
+ * @return The stored value, if it exists in this configuration.
+ * @throws IllegalArgumentException if the option does not exist in this configuration.
+ */
+ @RotationValue
+ default int getTargetRotation() {
+ return retrieveOption(OPTION_TARGET_ROTATION);
+ }
+
+ /**
+ * Retrieves the resolution of the target intending to use from this configuration.
+ *
+ * @param valueIfMissing The value to return if this configuration option has not been set.
+ * @return The stored value or <code>valueIfMissing</code> if the value does not exist in this
+ * configuration.
+ * @hide
+ */
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ default Size getTargetResolution(Size valueIfMissing) {
+ return retrieveOption(OPTION_TARGET_RESOLUTION, valueIfMissing);
+ }
+
+ /**
+ * Retrieves the resolution of the target intending to use from this configuration.
+ *
+ * @return The stored value, if it exists in this configuration.
+ * @throws IllegalArgumentException if the option does not exist in this configuration.
+ * @hide
+ */
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ default Size getTargetResolution() {
+ return retrieveOption(OPTION_TARGET_RESOLUTION);
+ }
+
+ // Option Declarations:
+ // ***********************************************************************************************
+
+ /**
+ * Retrieves the max resolution limitation of the target intending to use from this
+ * configuration.
+ *
+ * @param valueIfMissing The value to return if this configuration option has not been set.
+ * @return The stored value or <code>valueIfMissing</code> if the value does not exist in this
+ * configuration.
+ * @hide
+ */
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ default Size getMaxResolution(Size valueIfMissing) {
+ return retrieveOption(OPTION_MAX_RESOLUTION, valueIfMissing);
+ }
+
+ /**
+ * Retrieves the max resolution limitation of the target intending to use from this
+ * configuration.
+ *
+ * @return The stored value, if it exists in this configuration.
+ * @throws IllegalArgumentException if the option does not exist in this configuration.
+ * @hide
+ */
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ default Size getMaxResolution() {
+ return retrieveOption(OPTION_MAX_RESOLUTION);
+ }
+
+ /**
+ * Builder for a {@link ImageOutputConfiguration}.
+ *
+ * @param <C> The top level configuration which will be generated by {@link #build()}.
+ * @param <T> The top level builder type for which this builder is composed with.
+ */
+ interface Builder<C extends Configuration, T extends Builder<C, T>>
+ extends Configuration.Builder<C, T> {
+
+ /**
+ * Sets the aspect ratio of the intended target for images from this configuration.
+ *
+ * <p>This is the ratio of the target's width to the image's height, where the numerator of
+ * the provided {@link Rational} corresponds to the width, and the denominator corresponds
+ * to the height.
+ *
+ * @param aspectRatio A {@link Rational} representing the ratio of the target's width and
+ * height.
+ * @return The current Builder.
+ */
+ default T setTargetAspectRatio(Rational aspectRatio) {
+ getMutableConfiguration().insertOption(OPTION_TARGET_ASPECT_RATIO, aspectRatio);
+ return builder();
+ }
+
+ /**
+ * Sets the rotation of the intended target for images from this configuration.
+ *
+ * <p>This is one of four valid values: {@link Surface#ROTATION_0}, {@link
+ * Surface#ROTATION_90}, {@link Surface#ROTATION_180}, {@link Surface#ROTATION_270}.
+ * Rotation values are relative to the "natural" rotation, {@link Surface#ROTATION_0}.
+ *
+ * @param rotation The rotation of the intended target.
+ * @return The current Builder.
+ */
+ default T setTargetRotation(@RotationValue int rotation) {
+ getMutableConfiguration().insertOption(OPTION_TARGET_ROTATION, rotation);
+ return builder();
+ }
+
+ /**
+ * Sets the resolution of the intended target from this configuration.
+ *
+ * @param resolution The target resolution to choose from supported output sizes list.
+ * @return The current Builder.
+ * @hide
+ */
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ default T setTargetResolution(Size resolution) {
+ getMutableConfiguration().insertOption(OPTION_TARGET_RESOLUTION, resolution);
+ return builder();
+ }
+
+ /**
+ * Sets the max resolution limitation of the intended target from this configuration.
+ *
+ * @param resolution The max resolution limitation to choose from supported output sizes
+ * list.
+ * @return The current Builder.
+ * @hide
+ */
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ default T setMaxResolution(Size resolution) {
+ getMutableConfiguration().insertOption(OPTION_MAX_RESOLUTION, resolution);
+ return builder();
+ }
+ }
+
+ /**
+ * Valid integer rotation values.
+ *
+ * @hide
+ */
+ @IntDef({Surface.ROTATION_0, Surface.ROTATION_90, Surface.ROTATION_180, Surface.ROTATION_270})
+ @Retention(RetentionPolicy.SOURCE)
+ @interface RotationValue {
+ }
+}
diff --git a/camera/core/src/main/java/androidx/camera/core/ImageProxy.java b/camera/core/src/main/java/androidx/camera/core/ImageProxy.java
new file mode 100644
index 0000000..72d0beb
--- /dev/null
+++ b/camera/core/src/main/java/androidx/camera/core/ImageProxy.java
@@ -0,0 +1,114 @@
+/*
+ * Copyright (C) 2019 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.core;
+
+import android.graphics.Rect;
+
+import java.nio.ByteBuffer;
+
+/** An image proxy which has an analogous interface as {@link android.media.Image}. */
+public interface ImageProxy extends AutoCloseable {
+ /**
+ * Closes the image.
+ *
+ * <p>@see {@link android.media.Image#close()}.
+ */
+ void close();
+
+ /**
+ * Returns the crop rectangle.
+ *
+ * <p>@see {@link android.media.Image#getCropRect()}.
+ */
+ Rect getCropRect();
+
+ /**
+ * Sets the crop rectangle.
+ *
+ * <p>@see {@link android.media.Image#setCropRect(Rect)}.
+ */
+ void setCropRect(Rect rect);
+
+ /**
+ * Returns the image format.
+ *
+ * <p>@see {@link android.media.Image#getFormat()}.
+ */
+ int getFormat();
+
+ /**
+ * Returns the image height.
+ *
+ * <p>@see {@link android.media.Image#getHeight()}.
+ */
+ int getHeight();
+
+ /**
+ * Returns the image width.
+ *
+ * <p>@see {@link android.media.Image#getWidth()}.
+ */
+ int getWidth();
+
+ /**
+ * Returns the timestamp.
+ *
+ * <p>@see {@link android.media.Image#getTimestamp()}.
+ */
+ long getTimestamp();
+
+ /**
+ * Sets the timestamp.
+ *
+ * <p>@see {@link android.media.Image#setTimestamp(long)}.
+ */
+ void setTimestamp(long timestamp);
+
+ /**
+ * Returns the array of planes.
+ *
+ * <p>@see {@link android.media.Image#getPlanes()}.
+ */
+ PlaneProxy[] getPlanes();
+
+ /** A plane proxy which has an analogous interface as {@link android.media.Image.Plane}. */
+ interface PlaneProxy {
+ /**
+ * Returns the row stride.
+ *
+ * <p>@see {@link android.media.Image.Plane#getRowStride()}.
+ */
+ int getRowStride();
+
+ /**
+ * Returns the pixel stride.
+ *
+ * <p>@see {@link android.media.Image.Plane#getPixelStride()}.
+ */
+ int getPixelStride();
+
+ /**
+ * Returns the pixels buffer.
+ *
+ * <p>@see {@link android.media.Image.Plane#getBuffer()}.
+ */
+ ByteBuffer getBuffer();
+ }
+
+ // TODO(b/123902197): HardwareBuffer access is provided on higher API levels. Wrap
+ // getHardwareBuffer() once we figure out how to provide compatibility with lower API levels.
+}
diff --git a/camera/core/src/main/java/androidx/camera/core/ImageProxyDownsampler.java b/camera/core/src/main/java/androidx/camera/core/ImageProxyDownsampler.java
new file mode 100644
index 0000000..209b9cf
--- /dev/null
+++ b/camera/core/src/main/java/androidx/camera/core/ImageProxyDownsampler.java
@@ -0,0 +1,249 @@
+/*
+ * Copyright (C) 2019 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.core;
+
+import android.graphics.ImageFormat;
+import android.util.Size;
+
+import java.nio.ByteBuffer;
+
+/** Utility functions for downsampling an {@link ImageProxy}. */
+final class ImageProxyDownsampler {
+
+ private ImageProxyDownsampler() {
+ }
+
+ /**
+ * Downsamples an {@link ImageProxy}.
+ *
+ * @param image to downsample
+ * @param downsampledWidth width of the downsampled image
+ * @param downsampledHeight height of the dowsampled image
+ * @param downsamplingMethod the downsampling method
+ * @return the downsampled image
+ */
+ static ForwardingImageProxy downsample(
+ ImageProxy image,
+ int downsampledWidth,
+ int downsampledHeight,
+ DownsamplingMethod downsamplingMethod) {
+ if (image.getFormat() != ImageFormat.YUV_420_888) {
+ throw new UnsupportedOperationException(
+ "Only YUV_420_888 format is currently supported.");
+ }
+ if (image.getWidth() < downsampledWidth || image.getHeight() < downsampledHeight) {
+ throw new IllegalArgumentException(
+ "Downsampled dimension "
+ + new Size(downsampledWidth, downsampledHeight)
+ + " is not <= original dimension "
+ + new Size(image.getWidth(), image.getHeight())
+ + ".");
+ }
+
+ if (image.getWidth() == downsampledWidth && image.getHeight() == downsampledHeight) {
+ return new ForwardingImageProxyImpl(
+ image, image.getPlanes(), downsampledWidth, downsampledHeight);
+ }
+
+ int[] inputWidths = {image.getWidth(), image.getWidth() / 2, image.getWidth() / 2};
+ int[] inputHeights = {image.getHeight(), image.getHeight() / 2, image.getHeight() / 2};
+ int[] outputWidths = {downsampledWidth, downsampledWidth / 2, downsampledWidth / 2};
+ int[] outputHeights = {downsampledHeight, downsampledHeight / 2, downsampledHeight / 2};
+
+ ImageProxy.PlaneProxy[] outputPlanes = new ImageProxy.PlaneProxy[3];
+ for (int i = 0; i < 3; ++i) {
+ ImageProxy.PlaneProxy inputPlane = image.getPlanes()[i];
+ ByteBuffer inputBuffer = inputPlane.getBuffer();
+ byte[] output = new byte[outputWidths[i] * outputHeights[i]];
+ switch (downsamplingMethod) {
+ case NEAREST_NEIGHBOR:
+ resizeNearestNeighbor(
+ inputBuffer,
+ inputWidths[i],
+ inputPlane.getPixelStride(),
+ inputPlane.getRowStride(),
+ inputHeights[i],
+ output,
+ outputWidths[i],
+ outputHeights[i]);
+ break;
+ case AVERAGING:
+ resizeAveraging(
+ inputBuffer,
+ inputWidths[i],
+ inputPlane.getPixelStride(),
+ inputPlane.getRowStride(),
+ inputHeights[i],
+ output,
+ outputWidths[i],
+ outputHeights[i]);
+ break;
+ }
+ outputPlanes[i] = createPlaneProxy(outputWidths[i], 1, output);
+ }
+ return new ForwardingImageProxyImpl(
+ image, outputPlanes, downsampledWidth, downsampledHeight);
+ }
+
+ private static void resizeNearestNeighbor(
+ ByteBuffer input,
+ int inputWidth,
+ int inputPixelStride,
+ int inputRowStride,
+ int inputHeight,
+ byte[] output,
+ int outputWidth,
+ int outputHeight) {
+ float scaleX = (float) inputWidth / outputWidth;
+ float scaleY = (float) inputHeight / outputHeight;
+ int outputRowStride = outputWidth;
+
+ byte[] row = new byte[inputRowStride];
+ int[] sourceIndices = new int[outputWidth];
+ for (int ix = 0; ix < outputWidth; ++ix) {
+ float sourceX = ix * scaleX;
+ int floorSourceX = (int) sourceX;
+ sourceIndices[ix] = floorSourceX * inputPixelStride;
+ }
+
+ synchronized (input) {
+ input.rewind();
+ for (int iy = 0; iy < outputHeight; ++iy) {
+ float sourceY = iy * scaleY;
+ int floorSourceY = (int) sourceY;
+ int rowOffsetSource = Math.min(floorSourceY, inputHeight - 1) * inputRowStride;
+ int rowOffsetTarget = iy * outputRowStride;
+
+ input.position(rowOffsetSource);
+ input.get(row, 0, Math.min(inputRowStride, input.remaining()));
+
+ for (int ix = 0; ix < outputWidth; ++ix) {
+ output[rowOffsetTarget + ix] = row[sourceIndices[ix]];
+ }
+ }
+ }
+ }
+
+ private static void resizeAveraging(
+ ByteBuffer input,
+ int inputWidth,
+ int inputPixelStride,
+ int inputRowStride,
+ int inputHeight,
+ byte[] output,
+ int outputWidth,
+ int outputHeight) {
+ float scaleX = (float) inputWidth / outputWidth;
+ float scaleY = (float) inputHeight / outputHeight;
+ int outputRowStride = outputWidth;
+
+ byte[] row0 = new byte[inputRowStride];
+ byte[] row1 = new byte[inputRowStride];
+ int[] sourceIndices = new int[outputWidth];
+ for (int ix = 0; ix < outputWidth; ++ix) {
+ float sourceX = ix * scaleX;
+ int floorSourceX = (int) sourceX;
+ sourceIndices[ix] = floorSourceX * inputPixelStride;
+ }
+
+ synchronized (input) {
+ input.rewind();
+ for (int iy = 0; iy < outputHeight; ++iy) {
+ float sourceY = iy * scaleY;
+ int floorSourceY = (int) sourceY;
+ int rowOffsetSource0 = Math.min(floorSourceY, inputHeight - 1) * inputRowStride;
+ int rowOffsetSource1 = Math.min(floorSourceY + 1, inputHeight - 1) * inputRowStride;
+ int rowOffsetTarget = iy * outputRowStride;
+
+ input.position(rowOffsetSource0);
+ input.get(row0, 0, Math.min(inputRowStride, input.remaining()));
+ input.position(rowOffsetSource1);
+ input.get(row1, 0, Math.min(inputRowStride, input.remaining()));
+
+ for (int ix = 0; ix < outputWidth; ++ix) {
+ int sampleA = row0[sourceIndices[ix]] & 0xFF;
+ int sampleB = row0[sourceIndices[ix] + inputPixelStride] & 0xFF;
+ int sampleC = row1[sourceIndices[ix]] & 0xFF;
+ int sampleD = row1[sourceIndices[ix] + inputPixelStride] & 0xFF;
+ int mixed = (sampleA + sampleB + sampleC + sampleD) / 4;
+ output[rowOffsetTarget + ix] = (byte) (mixed & 0xFF);
+ }
+ }
+ }
+ }
+
+ private static ImageProxy.PlaneProxy createPlaneProxy(
+ int rowStride, int pixelStride, byte[] data) {
+ return new ImageProxy.PlaneProxy() {
+ final ByteBuffer buffer = ByteBuffer.wrap(data);
+
+ @Override
+ public int getRowStride() {
+ return rowStride;
+ }
+
+ @Override
+ public int getPixelStride() {
+ return pixelStride;
+ }
+
+ @Override
+ public ByteBuffer getBuffer() {
+ return buffer;
+ }
+ };
+ }
+
+ static enum DownsamplingMethod {
+ // Uses nearest sample.
+ NEAREST_NEIGHBOR,
+ // Uses average of 4 nearest samples.
+ AVERAGING,
+ }
+
+ private static final class ForwardingImageProxyImpl extends ForwardingImageProxy {
+ private final PlaneProxy[] downsampledPlanes;
+ private final int downsampledWidth;
+ private final int downsampledHeight;
+
+ ForwardingImageProxyImpl(
+ ImageProxy originalImage,
+ PlaneProxy[] downsampledPlanes,
+ int downsampledWidth,
+ int downsampledHeight) {
+ super(originalImage);
+ this.downsampledPlanes = downsampledPlanes;
+ this.downsampledWidth = downsampledWidth;
+ this.downsampledHeight = downsampledHeight;
+ }
+
+ @Override
+ public synchronized int getWidth() {
+ return downsampledWidth;
+ }
+
+ @Override
+ public synchronized int getHeight() {
+ return downsampledHeight;
+ }
+
+ @Override
+ public synchronized PlaneProxy[] getPlanes() {
+ return downsampledPlanes;
+ }
+ }
+}
diff --git a/camera/core/src/main/java/androidx/camera/core/ImageReaderFormatRecommender.java b/camera/core/src/main/java/androidx/camera/core/ImageReaderFormatRecommender.java
new file mode 100644
index 0000000..e39d825
--- /dev/null
+++ b/camera/core/src/main/java/androidx/camera/core/ImageReaderFormatRecommender.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2019 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.core;
+
+import android.graphics.ImageFormat;
+import android.media.ImageReader;
+
+import com.google.auto.value.AutoValue;
+
+/** Recommends formats for a combination of {@link ImageReader} instances. */
+final class ImageReaderFormatRecommender {
+
+ private ImageReaderFormatRecommender() {
+ }
+
+ /** Chooses a combination which is compatible for the current device. */
+ static FormatCombo chooseCombo() {
+ if (ImageReaderProxys.inSharedReaderWhitelist(DeviceProperties.create())) {
+ return FormatCombo.create(ImageFormat.YUV_420_888, ImageFormat.YUV_420_888);
+ } else {
+ return FormatCombo.create(ImageFormat.JPEG, ImageFormat.YUV_420_888);
+ }
+ }
+
+ /** Container for a combination of {@link ImageReader} formats. */
+ @AutoValue
+ abstract static class FormatCombo {
+ static FormatCombo create(int imageCaptureFormat, int imageAnalysisFormat) {
+ return new AutoValue_ImageReaderFormatRecommender_FormatCombo(
+ imageCaptureFormat, imageAnalysisFormat);
+ }
+
+ // Returns the format for image capture.
+ abstract int imageCaptureFormat();
+
+ // Returns the format for image analysis.
+ abstract int imageAnalysisFormat();
+ }
+}
diff --git a/camera/core/src/main/java/androidx/camera/core/ImageReaderProxy.java b/camera/core/src/main/java/androidx/camera/core/ImageReaderProxy.java
new file mode 100644
index 0000000..b98161e
--- /dev/null
+++ b/camera/core/src/main/java/androidx/camera/core/ImageReaderProxy.java
@@ -0,0 +1,118 @@
+/*
+ * Copyright (C) 2019 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.core;
+
+import android.media.ImageReader;
+import android.os.Handler;
+import android.view.Surface;
+
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.annotation.RestrictTo.Scope;
+
+/**
+ * An image reader proxy which has an analogous interface as {@link ImageReader}.
+ *
+ * <p>Whereas an {@link ImageReader} provides {@link android.media.Image} instances, an {@link
+ * ImageReaderProxy} provides {@link ImageProxy} instances.
+ *
+ * @hide
+ */
+@RestrictTo(Scope.LIBRARY_GROUP)
+public interface ImageReaderProxy {
+ /**
+ * Acquires the latest image in the queue.
+ *
+ * <p>@see {@link ImageReader#acquireLatestImage()}.
+ */
+ @Nullable
+ ImageProxy acquireLatestImage();
+
+ /**
+ * Acquires the next image in the queue.
+ *
+ * <p>@see {@link ImageReader#acquireNextImage()}.
+ */
+ @Nullable
+ ImageProxy acquireNextImage();
+
+ /**
+ * Closes the reader.
+ *
+ * <p>@see {@link ImageReader#close()}.
+ */
+ void close();
+
+ /**
+ * Returns the image height.
+ *
+ * <p>@see {@link ImageReader#getHeight()}.
+ */
+ int getHeight();
+
+ /**
+ * Returns the image width.
+ *
+ * <p>@see {@link ImageReader#getWidth()}.
+ */
+ int getWidth();
+
+ /**
+ * Returns the image format.
+ *
+ * <p>@see {@link ImageReader#getImageFormat()}.
+ */
+ int getImageFormat();
+
+ /**
+ * Returns the max number of images in the queue.
+ *
+ * <p>@see {@link ImageReader#getMaxImages()}.
+ */
+ int getMaxImages();
+
+ /**
+ * Returns the underlying surface.
+ *
+ * <p>@see {@link ImageReader#getSurface()}.
+ */
+ Surface getSurface();
+
+ /**
+ * Sets the on-image-available listener.
+ *
+ * <p>@see {@link ImageReader#setOnImageAvailableListener}.
+ */
+ void setOnImageAvailableListener(
+ @Nullable ImageReaderProxy.OnImageAvailableListener listener,
+ @Nullable Handler handler);
+
+ /**
+ * A listener for newly available images.
+ *
+ * @hide
+ */
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ interface OnImageAvailableListener {
+ /**
+ * Callback for a newly available image.
+ *
+ * <p>@see {@link ImageReader.OnImageAvailableListener#onImageAvailable(ImageReader)}.
+ */
+ void onImageAvailable(ImageReaderProxy imageReader);
+ }
+}
diff --git a/camera/core/src/main/java/androidx/camera/core/ImageReaderProxys.java b/camera/core/src/main/java/androidx/camera/core/ImageReaderProxys.java
new file mode 100644
index 0000000..22d15bf
--- /dev/null
+++ b/camera/core/src/main/java/androidx/camera/core/ImageReaderProxys.java
@@ -0,0 +1,158 @@
+/*
+ * Copyright (C) 2019 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.core;
+
+import android.graphics.ImageFormat;
+import android.media.ImageReader;
+import android.os.Handler;
+import android.util.Log;
+import android.util.Size;
+
+import androidx.annotation.RestrictTo;
+import androidx.annotation.RestrictTo.Scope;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * Different implementations of {@link ImageReaderProxy}.
+ *
+ * @hide
+ */
+@RestrictTo(Scope.LIBRARY_GROUP)
+public final class ImageReaderProxys {
+ private static final String TAG = ImageReaderProxys.class.getSimpleName();
+ private static final int SHARED_IMAGE_FORMAT = ImageFormat.YUV_420_888;
+ private static final int SHARED_MAX_IMAGES = 8;
+ private static final List<QueuedImageReaderProxy> sharedImageReaderProxys = new ArrayList<>();
+ private static Set<DeviceProperties> sharedReaderWhitelist;
+ private static ImageReader sharedImageReader;
+
+ private ImageReaderProxys() {
+ }
+
+ /**
+ * Creates an {@link ImageReaderProxy} which chooses a device-compatible implementation.
+ *
+ * @param cameraId of the target camera
+ * @param width of the reader
+ * @param height of the reader
+ * @param format of the reader
+ * @param maxImages of the reader
+ * @param handler for on-image-available callbacks
+ * @return new {@link ImageReaderProxy} instance
+ */
+ static ImageReaderProxy createCompatibleReader(
+ String cameraId, int width, int height, int format, int maxImages, Handler handler) {
+ if (inSharedReaderWhitelist(DeviceProperties.create())) {
+ return createSharedReader(cameraId, width, height, format, maxImages, handler);
+ } else {
+ return createIsolatedReader(width, height, format, maxImages, handler);
+ }
+ }
+
+ /**
+ * Creates an {@link ImageReaderProxy} which uses its own isolated {@link ImageReader}.
+ *
+ * @param width of the reader
+ * @param height of the reader
+ * @param format of the reader
+ * @param maxImages of the reader
+ * @param handler for on-image-available callbacks
+ * @return new {@link ImageReaderProxy} instance
+ */
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ public static ImageReaderProxy createIsolatedReader(
+ int width, int height, int format, int maxImages, Handler handler) {
+ ImageReader imageReader = ImageReader.newInstance(width, height, format, maxImages);
+ return new AndroidImageReaderProxy(imageReader);
+ }
+
+ /**
+ * Creates an {@link ImageReaderProxy} which shares an underlying {@link ImageReader}.
+ *
+ * @param cameraId of the target camera
+ * @param width of the reader
+ * @param height of the reader
+ * @param format of the reader
+ * @param maxImages of the reader
+ * @param handler for on-image-available callbacks
+ * @return new {@link ImageReaderProxy} instance
+ */
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ public static ImageReaderProxy createSharedReader(
+ String cameraId, int width, int height, int format, int maxImages, Handler handler) {
+ if (sharedImageReader == null) {
+ Size resolution =
+ CameraX.getSurfaceManager().getMaxOutputSize(cameraId, SHARED_IMAGE_FORMAT);
+ Log.d(TAG, "Resolution of base ImageReader: " + resolution);
+ sharedImageReader =
+ ImageReader.newInstance(
+ resolution.getWidth(),
+ resolution.getHeight(),
+ SHARED_IMAGE_FORMAT,
+ SHARED_MAX_IMAGES);
+ }
+ Log.d(TAG, "Resolution of forked ImageReader: " + new Size(width, height));
+ QueuedImageReaderProxy imageReaderProxy =
+ new QueuedImageReaderProxy(
+ width, height, format, maxImages, sharedImageReader.getSurface());
+ sharedImageReaderProxys.add(imageReaderProxy);
+ sharedImageReader.setOnImageAvailableListener(
+ new ForwardingImageReaderListener(sharedImageReaderProxys), handler);
+ imageReaderProxy.addOnReaderCloseListener(
+ reader -> {
+ sharedImageReaderProxys.remove(reader);
+ if (sharedImageReaderProxys.isEmpty()) {
+ clearSharedReaders();
+ }
+ });
+ return imageReaderProxy;
+ }
+
+ /**
+ * Returns true if the device is in the shared reader whitelist.
+ *
+ * <p>Devices in the whitelist are known to work with shared readers. Devices outside the
+ * whitelist may also work with shared readers, but they have not been tested yet.
+ *
+ * @param device to check
+ * @return true if device is in whitelist
+ */
+ static boolean inSharedReaderWhitelist(DeviceProperties device) {
+ if (sharedReaderWhitelist == null) {
+ sharedReaderWhitelist = new HashSet<>();
+ for (int sdkVersion = 21; sdkVersion <= 27; ++sdkVersion) {
+ sharedReaderWhitelist.add(DeviceProperties.create("Google", "Pixel", sdkVersion));
+ sharedReaderWhitelist.add(
+ DeviceProperties.create("Google", "Pixel XL", sdkVersion));
+ sharedReaderWhitelist.add(
+ DeviceProperties.create("HMD Global", "Nokia 8.1", sdkVersion));
+ }
+ }
+ return sharedReaderWhitelist.contains(device);
+ }
+
+ private static void clearSharedReaders() {
+ sharedImageReaderProxys.clear();
+ sharedImageReader.setOnImageAvailableListener(null, null);
+ sharedImageReader.close();
+ sharedImageReader = null;
+ }
+}
diff --git a/camera/core/src/main/java/androidx/camera/core/ImageSaver.java b/camera/core/src/main/java/androidx/camera/core/ImageSaver.java
new file mode 100644
index 0000000..941c979
--- /dev/null
+++ b/camera/core/src/main/java/androidx/camera/core/ImageSaver.java
@@ -0,0 +1,182 @@
+/*
+ * Copyright (C) 2019 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.core;
+
+import android.graphics.ImageFormat;
+import android.location.Location;
+import android.os.Handler;
+import android.util.Log;
+import android.util.Rational;
+import android.util.Size;
+
+import androidx.annotation.Nullable;
+import androidx.camera.core.ImageUtil.EncodeFailedException;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+
+final class ImageSaver implements Runnable {
+ private static final String TAG = "ImageSaver";
+ private final @Nullable
+ Location location;
+ // The image that was captured
+ private final ImageProxy image;
+ // The orientation of the image
+ private final int orientation;
+ // If true, the picture taken is reversed horizontally and needs to be flipped.
+ // Typical with front facing cameras.
+ private final boolean isReversedHorizontal;
+ // If true, the picture taken is reversed vertically and needs to be flipped.
+ private final boolean isReversedVertical;
+ // The file to save the image to
+ private final File file;
+ // The callback to call on completion
+ private final OnImageSavedListener listener;
+ // The handler to call back on
+ private final Handler handler;
+ // The width/height ratio output should be cropped to
+ @Nullable
+ private final Rational cropAspectRatio;
+
+ ImageSaver(
+ ImageProxy image,
+ File file,
+ int orientation,
+ boolean reversedHorizontal,
+ boolean reversedVertical,
+ @Nullable Location location,
+ @Nullable Rational cropAspectRatio,
+ OnImageSavedListener listener,
+ Handler handler) {
+ this.image = image;
+ this.file = file;
+ this.orientation = orientation;
+ isReversedHorizontal = reversedHorizontal;
+ isReversedVertical = reversedVertical;
+ this.listener = listener;
+ this.handler = handler;
+ this.location = location;
+
+ // Fix cropRatio by orientation.
+ if (orientation == 90 || orientation == 270) {
+ this.cropAspectRatio = inverseRational(cropAspectRatio);
+ } else {
+ this.cropAspectRatio = cropAspectRatio;
+ }
+ }
+
+ @Override
+ public void run() {
+ // Finally, we save the file to disk
+ SaveError saveError = null;
+ String errorMessage = null;
+ Exception exception = null;
+ try (ImageProxy imageToClose = image;
+ FileOutputStream output = new FileOutputStream(file)) {
+ byte[] bytes = getBytes();
+ output.write(bytes);
+
+ Exif exif = Exif.createFromFile(file);
+ exif.attachTimestamp();
+ exif.rotate(orientation);
+ if (isReversedHorizontal) {
+ exif.flipHorizontally();
+ }
+ if (isReversedVertical) {
+ exif.flipVertically();
+ }
+ if (location != null) {
+ exif.attachLocation(location);
+ }
+ exif.save();
+ } catch (IOException e) {
+ saveError = SaveError.FILE_IO_FAILED;
+ errorMessage = "Failed to write or close the file";
+ exception = e;
+ } catch (EncodeFailedException e) {
+ saveError = SaveError.ENCODE_FAILED;
+ errorMessage = "Failed to encode image";
+ exception = e;
+ }
+
+ if (saveError != null) {
+ postError(saveError, errorMessage, exception);
+ } else {
+ postSuccess();
+ }
+ }
+
+ private void postSuccess() {
+ handler.post(() -> listener.onImageSaved(file));
+ }
+
+ private void postError(SaveError saveError, String message, @Nullable Throwable cause) {
+ handler.post(() -> listener.onError(saveError, message, cause));
+ }
+
+ private byte[] getBytes() throws EncodeFailedException {
+ byte[] data = null;
+ Size sourceSize = new Size(image.getWidth(), image.getHeight());
+
+ if (ImageUtil.isAspectRatioValid(sourceSize, cropAspectRatio)) {
+ if (image.getFormat() == ImageFormat.JPEG) {
+ data =
+ ImageUtil.cropByteArray(
+ ImageUtil.jpegImageToJpegByteArray(image),
+ ImageUtil.computeCropRectFromAspectRatio(
+ sourceSize, cropAspectRatio));
+ } else if (image.getFormat() == ImageFormat.YUV_420_888) {
+ data =
+ ImageUtil.yuvImageToJpegByteArray(
+ image,
+ ImageUtil.computeCropRectFromAspectRatio(
+ sourceSize, cropAspectRatio));
+ } else {
+ data = ImageUtil.imageToJpegByteArray(image);
+ Log.w(TAG, "Unrecognized image format: " + image.getFormat());
+ }
+ } else {
+ data = ImageUtil.imageToJpegByteArray(image);
+ }
+
+ return data;
+ }
+
+ private Rational inverseRational(Rational rational) {
+ if (rational == null) {
+ return rational;
+ }
+ return new Rational(
+ /*numerator=*/ rational.getDenominator(), /*denominator=*/ rational.getNumerator());
+ }
+
+ /** Type of error that occurred during save */
+ public enum SaveError {
+ /** Failed to write to or close the file */
+ FILE_IO_FAILED,
+ /** Failure when attempting to encode image */
+ ENCODE_FAILED
+ }
+
+ public interface OnImageSavedListener {
+
+ void onImageSaved(File file);
+
+ void onError(SaveError saveError, String message, @Nullable Throwable cause);
+ }
+}
diff --git a/camera/core/src/main/java/androidx/camera/core/ImageUtil.java b/camera/core/src/main/java/androidx/camera/core/ImageUtil.java
new file mode 100644
index 0000000..d18bbf6
--- /dev/null
+++ b/camera/core/src/main/java/androidx/camera/core/ImageUtil.java
@@ -0,0 +1,268 @@
+/*
+ * Copyright (C) 2019 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.core;
+
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.ImageFormat;
+import android.graphics.Matrix;
+import android.graphics.Rect;
+import android.graphics.YuvImage;
+import android.util.Log;
+import android.util.Rational;
+import android.util.Size;
+
+import androidx.annotation.Nullable;
+
+import java.io.ByteArrayOutputStream;
+import java.nio.ByteBuffer;
+
+/**
+ * Utility class for image related operations.
+ *
+ * @hide
+ */
+final class ImageUtil {
+ private static final String TAG = "ImageUtil";
+
+ private ImageUtil() {
+ }
+
+ /** {@link android.media.Image} to JPEG byte array. */
+ public static byte[] imageToJpegByteArray(ImageProxy image) throws EncodeFailedException {
+ byte[] data = null;
+ if (image.getFormat() == ImageFormat.JPEG) {
+ data = jpegImageToJpegByteArray(image);
+ } else if (image.getFormat() == ImageFormat.YUV_420_888) {
+ data = yuvImageToJpegByteArray(image, null);
+ } else {
+ Log.w(TAG, "Unrecognized image format: " + image.getFormat());
+ }
+ return data;
+ }
+
+ public static byte[] jpegImageToJpegByteArray(ImageProxy image) {
+ ImageProxy.PlaneProxy[] planes = image.getPlanes();
+ ByteBuffer buffer = planes[0].getBuffer();
+ byte[] data = new byte[buffer.capacity()];
+ buffer.get(data);
+ return data;
+ }
+
+ public static byte[] yuvImageToJpegByteArray(ImageProxy image, @Nullable Rect cropRect)
+ throws EncodeFailedException {
+ return ImageUtil.nv21ToJpeg(
+ ImageUtil.yuv_420_888toNv21(image), image.getWidth(), image.getHeight(), cropRect);
+ }
+
+ /** Crops byte array with given {@link android.graphics.Rect}. */
+ public static byte[] cropByteArray(byte[] data, Rect cropRect) throws EncodeFailedException {
+ if (cropRect == null) {
+ return data;
+ }
+
+ Bitmap imageBitmap = BitmapFactory.decodeByteArray(data, 0, data.length);
+ if (imageBitmap == null) {
+ Log.w(TAG, "Source image for cropping can't be decoded.");
+ return data;
+ }
+
+ Bitmap cropBitmap = cropBitmap(imageBitmap, cropRect);
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ boolean success = cropBitmap.compress(Bitmap.CompressFormat.JPEG, 100, out);
+ if (!success) {
+ throw new EncodeFailedException("cropImage failed to encode jpeg.");
+ }
+
+ imageBitmap.recycle();
+ cropBitmap.recycle();
+
+ return out.toByteArray();
+ }
+
+ /** Crops bitmap with given {@link android.graphics.Rect}. */
+ public static Bitmap cropBitmap(Bitmap bitmap, Rect cropRect) {
+ if (cropRect.width() > bitmap.getWidth() || cropRect.height() > bitmap.getHeight()) {
+ Log.w(TAG, "Crop rect size exceeds the source image.");
+ return bitmap;
+ }
+
+ return Bitmap.createBitmap(
+ bitmap, cropRect.left, cropRect.top, cropRect.width(), cropRect.height());
+ }
+
+ /** Flips bitmap. */
+ public static Bitmap flipBitmap(Bitmap bitmap, boolean flipHorizontal, boolean flipVertical) {
+ if (!flipHorizontal && !flipVertical) {
+ return bitmap;
+ }
+
+ Matrix matrix = new Matrix();
+ if (flipHorizontal) {
+ if (flipVertical) {
+ matrix.preScale(-1.0f, -1.0f);
+ } else {
+ matrix.preScale(-1.0f, 1.0f);
+ }
+ } else if (flipVertical) {
+ matrix.preScale(1.0f, -1.0f);
+ }
+
+ return Bitmap.createBitmap(
+ bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), matrix, true);
+ }
+
+ /** Rotates bitmap with specified degree. */
+ public static Bitmap rotateBitmap(Bitmap bitmap, int degree) {
+ if (degree == 0) {
+ return bitmap;
+ }
+
+ Matrix matrix = new Matrix();
+ matrix.preRotate(degree);
+
+ return Bitmap.createBitmap(
+ bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), matrix, true);
+ }
+
+ /** True if the given aspect ratio is meaningful. */
+ public static boolean isAspectRatioValid(Rational aspectRatio) {
+ return aspectRatio != null && aspectRatio.floatValue() > 0 && !aspectRatio.isNaN();
+ }
+
+ /** True if the given aspect ratio is meaningful and has effect on the given size. */
+ public static boolean isAspectRatioValid(Size sourceSize, Rational aspectRatio) {
+ return aspectRatio != null
+ && aspectRatio.floatValue() > 0
+ && isCropAspectRatioHasEffect(sourceSize, aspectRatio)
+ && !aspectRatio.isNaN();
+ }
+
+ /**
+ * Calculates crop rect with the specified aspect ratio on the given size. Assuming the rect is
+ * at the center of the source.
+ */
+ public static Rect computeCropRectFromAspectRatio(Size sourceSize, Rational aspectRatio) {
+ if (!isAspectRatioValid(aspectRatio)) {
+ Log.w(TAG, "Invalid view ratio.");
+ return null;
+ }
+
+ int sourceWidth = sourceSize.getWidth();
+ int sourceHeight = sourceSize.getHeight();
+ float srcRatio = sourceWidth / (float) sourceHeight;
+ int cropLeft = 0;
+ int cropTop = 0;
+ int outputWidth = sourceWidth;
+ int outputHeight = sourceHeight;
+ int numerator = aspectRatio.getNumerator();
+ int denominator = aspectRatio.getDenominator();
+
+ if (aspectRatio.floatValue() > srcRatio) {
+ outputHeight = Math.round((sourceWidth / (float) numerator) * denominator);
+ cropTop = (sourceHeight - outputHeight) / 2;
+ } else {
+ outputWidth = Math.round((sourceHeight / (float) denominator) * numerator);
+ cropLeft = (sourceWidth - outputWidth) / 2;
+ }
+
+ return new Rect(cropLeft, cropTop, cropLeft + outputWidth, cropTop + outputHeight);
+ }
+
+ private static byte[] nv21ToJpeg(byte[] nv21, int width, int height, @Nullable Rect cropRect)
+ throws EncodeFailedException {
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ YuvImage yuv = new YuvImage(nv21, ImageFormat.NV21, width, height, null);
+ boolean success =
+ yuv.compressToJpeg(
+ cropRect == null ? new Rect(0, 0, width, height) : cropRect, 100, out);
+ if (!success) {
+ throw new EncodeFailedException("YuvImage failed to encode jpeg.");
+ }
+ return out.toByteArray();
+ }
+
+ private static byte[] yuv_420_888toNv21(ImageProxy image) {
+ ImageProxy.PlaneProxy yPlane = image.getPlanes()[0];
+ ImageProxy.PlaneProxy uPlane = image.getPlanes()[1];
+ ImageProxy.PlaneProxy vPlane = image.getPlanes()[2];
+
+ ByteBuffer yBuffer = yPlane.getBuffer();
+ ByteBuffer uBuffer = uPlane.getBuffer();
+ ByteBuffer vBuffer = vPlane.getBuffer();
+ yBuffer.rewind();
+ uBuffer.rewind();
+ vBuffer.rewind();
+
+ int ySize = yBuffer.remaining();
+
+ int position = 0;
+ // TODO(b/115743986): Pull these bytes from a pool instead of allocating for every image.
+ byte[] nv21 = new byte[ySize + (image.getWidth() * image.getHeight() / 2)];
+
+ // Add the full y buffer to the array. If rowStride > 1, some padding may be skipped.
+ for (int row = 0; row < image.getHeight(); row++) {
+ yBuffer.get(nv21, position, image.getWidth());
+ position += image.getWidth();
+ yBuffer.position(
+ Math.min(ySize, yBuffer.position() - image.getWidth() + yPlane.getRowStride()));
+ }
+
+ int chromaHeight = image.getHeight() / 2;
+ int chromaWidth = image.getWidth() / 2;
+ int vRowStride = vPlane.getRowStride();
+ int uRowStride = uPlane.getRowStride();
+ int vPixelStride = vPlane.getPixelStride();
+ int uPixelStride = uPlane.getPixelStride();
+
+ // Interleave the u and v frames, filling up the rest of the buffer. Use two line buffers to
+ // perform faster bulk gets from the byte buffers.
+ byte[] vLineBuffer = new byte[vRowStride];
+ byte[] uLineBuffer = new byte[uRowStride];
+ for (int row = 0; row < chromaHeight; row++) {
+ vBuffer.get(vLineBuffer, 0, Math.min(vRowStride, vBuffer.remaining()));
+ uBuffer.get(uLineBuffer, 0, Math.min(uRowStride, uBuffer.remaining()));
+ int vLineBufferPosition = 0;
+ int uLineBufferPosition = 0;
+ for (int col = 0; col < chromaWidth; col++) {
+ nv21[position++] = vLineBuffer[vLineBufferPosition];
+ nv21[position++] = uLineBuffer[uLineBufferPosition];
+ vLineBufferPosition += vPixelStride;
+ uLineBufferPosition += uPixelStride;
+ }
+ }
+
+ return nv21;
+ }
+
+ private static boolean isCropAspectRatioHasEffect(Size sourceSize, Rational aspectRatio) {
+ int sourceWidth = sourceSize.getWidth();
+ int sourceHeight = sourceSize.getHeight();
+ int numerator = aspectRatio.getNumerator();
+ int denominator = aspectRatio.getDenominator();
+
+ return sourceHeight != Math.round((sourceWidth / (float) numerator) * denominator)
+ || sourceWidth != Math.round((sourceHeight / (float) denominator) * numerator);
+ }
+
+ /** Exception for error during encoding image. */
+ public static final class EncodeFailedException extends Exception {
+ EncodeFailedException(String message) {
+ super(message);
+ }
+ }
+}
diff --git a/camera/core/src/main/java/androidx/camera/core/ImmediateSurface.java b/camera/core/src/main/java/androidx/camera/core/ImmediateSurface.java
new file mode 100644
index 0000000..5813a5f
--- /dev/null
+++ b/camera/core/src/main/java/androidx/camera/core/ImmediateSurface.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2019 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.core;
+
+import android.view.Surface;
+
+import androidx.annotation.RestrictTo;
+import androidx.annotation.RestrictTo.Scope;
+
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+
+/**
+ * A {@link DeferrableSurface} which always returns immediately.
+ *
+ * @hide
+ */
+@RestrictTo(Scope.LIBRARY_GROUP)
+public final class ImmediateSurface implements DeferrableSurface {
+ private final Surface surface;
+
+ public ImmediateSurface(Surface surface) {
+ this.surface = surface;
+ }
+
+ @Override
+ public ListenableFuture<Surface> getSurface() {
+ return Futures.immediateFuture(surface);
+ }
+}
diff --git a/camera/core/src/main/java/androidx/camera/core/IoExecutor.java b/camera/core/src/main/java/androidx/camera/core/IoExecutor.java
new file mode 100644
index 0000000..75abc74
--- /dev/null
+++ b/camera/core/src/main/java/androidx/camera/core/IoExecutor.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright (C) 2019 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.core;
+
+import java.util.Locale;
+import java.util.concurrent.Executor;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ThreadFactory;
+import java.util.concurrent.atomic.AtomicInteger;
+
+/**
+ * A singleton executor which should be used for I/O tasks.
+ *
+ * <p>TODO(b/115779693): Make this executor configurable
+ */
+final class IoExecutor implements Executor {
+ private static volatile Executor instance;
+
+ private final ExecutorService ioService =
+ Executors.newFixedThreadPool(
+ 2,
+ new ThreadFactory() {
+ private static final String THREAD_NAME_STEM =
+ CameraXThreads.TAG + "camerax_io_%d";
+
+ private final AtomicInteger threadId = new AtomicInteger(0);
+
+ @Override
+ public Thread newThread(Runnable r) {
+ Thread t = new Thread(r);
+ t.setName(
+ String.format(
+ Locale.US,
+ THREAD_NAME_STEM,
+ threadId.getAndIncrement()));
+ return t;
+ }
+ });
+
+ static Executor getInstance() {
+ if (instance != null) {
+ return instance;
+ }
+ synchronized (IoExecutor.class) {
+ if (instance == null) {
+ instance = new IoExecutor();
+ }
+ }
+
+ return instance;
+ }
+
+ @Override
+ public void execute(Runnable command) {
+ ioService.execute(command);
+ }
+}
diff --git a/camera/core/src/main/java/androidx/camera/core/MutableConfiguration.java b/camera/core/src/main/java/androidx/camera/core/MutableConfiguration.java
new file mode 100644
index 0000000..68eff92
--- /dev/null
+++ b/camera/core/src/main/java/androidx/camera/core/MutableConfiguration.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2019 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.core;
+
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.annotation.RestrictTo.Scope;
+
+/**
+ * MutableConfiguration is a {@link Configuration} that can be modified.
+ *
+ * <p>MutableConfiguration is the interface used to create immutable Configuration objects.
+ */
+public interface MutableConfiguration extends Configuration {
+
+ /**
+ * Inserts a Option/Value pair into the configuration.
+ *
+ * <p>If the option already exists in this configuration, it will be replaced.
+ *
+ * @param opt The option to be added or modified
+ * @param value The value to insert for this option.
+ * @param <ValueT> The type of the value being inserted.
+ * @hide
+ */
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ <ValueT> void insertOption(Option<ValueT> opt, ValueT value);
+
+ /**
+ * Removes an option from the configuration if it exists.
+ *
+ * @param opt The option to remove from the configuration.
+ * @param <ValueT> The type of the value being removed.
+ * @return The value that previously existed for <code>opt</code>, or <code>null</code> if the
+ * option did not exist in this configuration.
+ * @hide
+ */
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ @Nullable
+ <ValueT> ValueT removeOption(Option<ValueT> opt);
+}
diff --git a/camera/core/src/main/java/androidx/camera/core/MutableOptionsBundle.java b/camera/core/src/main/java/androidx/camera/core/MutableOptionsBundle.java
new file mode 100644
index 0000000..ca7b6c6
--- /dev/null
+++ b/camera/core/src/main/java/androidx/camera/core/MutableOptionsBundle.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright (C) 2019 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.core;
+
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.annotation.RestrictTo.Scope;
+
+import java.util.Comparator;
+import java.util.TreeMap;
+
+/**
+ * A MutableOptionsBundle is an {@link OptionsBundle} which allows for insertion/removal.
+ *
+ * @hide
+ */
+@RestrictTo(Scope.LIBRARY_GROUP)
+public final class MutableOptionsBundle extends OptionsBundle implements MutableConfiguration {
+
+ private static final Comparator<Option<?>> ID_COMPARE =
+ (o1, o2) -> o1.getId().compareTo(o2.getId());
+
+ private MutableOptionsBundle(TreeMap<Option<?>, Object> persistentOptions) {
+ super(persistentOptions);
+ }
+
+ /**
+ * Creates an empty MutableOptionsBundle.
+ *
+ * @return an empty MutableOptionsBundle containing no options.
+ */
+ public static MutableOptionsBundle create() {
+ return new MutableOptionsBundle(new TreeMap<>(ID_COMPARE));
+ }
+
+ /**
+ * Creates a MutableOptionsBundle from an existing immutable Configuration.
+ *
+ * @param otherConfig configuration options to insert.
+ * @return a MutableOptionsBundle prepopulated with configuration options.
+ */
+ public static MutableOptionsBundle from(Configuration otherConfig) {
+ TreeMap<Option<?>, Object> persistentOptions = new TreeMap<>(ID_COMPARE);
+ for (Option<?> opt : otherConfig.listOptions()) {
+ persistentOptions.put(opt, otherConfig.retrieveOption(opt));
+ }
+
+ return new MutableOptionsBundle(persistentOptions);
+ }
+
+ @Nullable
+ @Override
+ public <ValueT> ValueT removeOption(Option<ValueT> opt) {
+ @SuppressWarnings("unchecked") // Options should have only been inserted via insertOption()
+ ValueT value = (ValueT) options.remove(opt);
+
+ return value;
+ }
+
+ @Override
+ public <ValueT> void insertOption(Option<ValueT> opt, ValueT value) {
+ options.put(opt, value);
+ }
+}
diff --git a/camera/core/src/main/java/androidx/camera/core/OnFocusCompletedListener.java b/camera/core/src/main/java/androidx/camera/core/OnFocusCompletedListener.java
new file mode 100644
index 0000000..d15b18b
--- /dev/null
+++ b/camera/core/src/main/java/androidx/camera/core/OnFocusCompletedListener.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2019 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.core;
+
+import android.graphics.Rect;
+
+/** Listener called when focus scan has completed. */
+public interface OnFocusCompletedListener {
+ /** Callback when focus has been locked. */
+ default void onFocusLocked(Rect afRect) {
+ }
+
+ /** Callback when unable to acquire focus. */
+ default void onFocusUnableToLock(Rect afRect) {
+ }
+
+ /** Callback when timeout is reached and af state haven't settled. */
+ default void onFocusTimedOut(Rect afRect) {
+ }
+}
diff --git a/camera/core/src/main/java/androidx/camera/core/OptionsBundle.java b/camera/core/src/main/java/androidx/camera/core/OptionsBundle.java
new file mode 100644
index 0000000..b1cd838
--- /dev/null
+++ b/camera/core/src/main/java/androidx/camera/core/OptionsBundle.java
@@ -0,0 +1,130 @@
+/*
+ * Copyright (C) 2019 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.core;
+
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.annotation.RestrictTo.Scope;
+
+import java.util.Collections;
+import java.util.Map.Entry;
+import java.util.Set;
+import java.util.TreeMap;
+
+/**
+ * An immutable implementation of {@link Configuration}.
+ *
+ * <p>OptionsBundle is a collection of {@link Configuration.Option}s and their values which can be
+ * queried based on exact {@link Configuration.Option} objects or based on Option ids.
+ *
+ * @hide
+ */
+@RestrictTo(Scope.LIBRARY_GROUP)
+public class OptionsBundle implements Configuration {
+
+ private static final OptionsBundle EMPTY_BUNDLE =
+ new OptionsBundle(new TreeMap<>((o1, o2) -> o1.getId().compareTo(o2.getId())));
+ // TODO: Make these options parcelable
+ protected final TreeMap<Option<?>, Object> options;
+
+ OptionsBundle(TreeMap<Option<?>, Object> options) {
+ this.options = options;
+ }
+
+ /**
+ * Create an OptionsBundle from another configuration.
+ *
+ * <p>This will copy the options/values from the provided configuration.
+ *
+ * @param otherConfig Configuration containing options/values to be copied.
+ * @return A new OptionsBundle pre-populated with options/values.
+ */
+ public static OptionsBundle from(Configuration otherConfig) {
+ // No need to create another instance since OptionsBundle is immutable
+ if (OptionsBundle.class.equals(otherConfig.getClass())) {
+ return (OptionsBundle) otherConfig;
+ }
+
+ TreeMap<Option<?>, Object> persistentOptions =
+ new TreeMap<>((o1, o2) -> o1.getId().compareTo(o2.getId()));
+ for (Option<?> opt : otherConfig.listOptions()) {
+ persistentOptions.put(opt, otherConfig.retrieveOption(opt));
+ }
+
+ return new OptionsBundle(persistentOptions);
+ }
+
+ /**
+ * Create an empty OptionsBundle.
+ *
+ * <p>This options bundle will have no option/value pairs.
+ *
+ * @return An OptionsBundle pre-populated with no options/values.
+ */
+ public static OptionsBundle emptyBundle() {
+ return EMPTY_BUNDLE;
+ }
+
+ @Override
+ public Set<Option<?>> listOptions() {
+ return Collections.unmodifiableSet(options.keySet());
+ }
+
+ @Override
+ public boolean containsOption(Option<?> id) {
+ return options.containsKey(id);
+ }
+
+ @Override
+ public <ValueT> ValueT retrieveOption(Option<ValueT> id) {
+ ValueT value = retrieveOption(id, /*valueIfMissing=*/ null);
+ if (value == null) {
+ throw new IllegalArgumentException("Option does not exist: " + id);
+ }
+
+ return value;
+ }
+
+ @Nullable
+ @Override
+ public <ValueT> ValueT retrieveOption(Option<ValueT> id, @Nullable ValueT valueIfMissing) {
+ @SuppressWarnings("unchecked") // Options should have only been inserted via insertOption()
+ ValueT value = (ValueT) options.get(id);
+ if (value == null) {
+ value = valueIfMissing;
+ }
+
+ return value;
+ }
+
+ @Override
+ public void findOptions(String idStem, OptionMatcher matcher) {
+ Option<Void> query = Option.create(idStem, Void.class);
+ for (Entry<Option<?>, Object> entry : options.tailMap(query).entrySet()) {
+ if (!entry.getKey().getId().startsWith(idStem)) {
+ // We've reached the end of the range that contains our search stem.
+ break;
+ }
+
+ Option<?> option = entry.getKey();
+ if (!matcher.onOptionMatched(option)) {
+ // Caller does not need further results
+ break;
+ }
+ }
+ }
+}
diff --git a/camera/core/src/main/java/androidx/camera/core/QueuedImageReaderProxy.java b/camera/core/src/main/java/androidx/camera/core/QueuedImageReaderProxy.java
new file mode 100644
index 0000000..3c5287a
--- /dev/null
+++ b/camera/core/src/main/java/androidx/camera/core/QueuedImageReaderProxy.java
@@ -0,0 +1,276 @@
+/*
+ * Copyright (C) 2019 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.core;
+
+import android.os.Handler;
+import android.view.Surface;
+
+import androidx.annotation.GuardedBy;
+import androidx.annotation.Nullable;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * An {@link ImageReaderProxy} which maintains a queue of recently available images.
+ *
+ * <p>Like a conventional {@link android.media.ImageReader}, when the queue becomes full and the
+ * user does not close older images quickly enough, newly available images will not be added to the
+ * queue and become lost. The user is responsible for setting a listener for newly available images
+ * and closing the acquired images quickly enough.
+ */
+final class QueuedImageReaderProxy
+ implements ImageReaderProxy, ForwardingImageProxy.OnImageCloseListener {
+ private final int width;
+ private final int height;
+ private final int format;
+ private final int maxImages;
+
+ @GuardedBy("this")
+ private final Surface surface;
+
+ // maxImages is not expected to be large, because images consume a lot of memory and there
+ // cannot
+ // co-exist too many images simultaneously. So, just use a List to simplify the implementation.
+ @GuardedBy("this")
+ private final List<ImageProxy> images;
+
+ @GuardedBy("this")
+ private final Set<ImageProxy> acquiredImages = new HashSet<>();
+ @GuardedBy("this")
+ private final Set<OnReaderCloseListener> onReaderCloseListeners = new HashSet<>();
+ // Current access position in the queue.
+ @GuardedBy("this")
+ private int currentPosition;
+ @GuardedBy("this")
+ @Nullable
+ private ImageReaderProxy.OnImageAvailableListener onImageAvailableListener;
+ @GuardedBy("this")
+ @Nullable
+ private Handler onImageAvailableHandler;
+ @GuardedBy("this")
+ private boolean closed;
+
+ /**
+ * Creates a new instance of a queued image reader proxy.
+ *
+ * @param width of the images
+ * @param height of the images
+ * @param format of the images
+ * @param maxImages capacity of the queue
+ * @param surface to which the reader is attached
+ * @return new {@link QueuedImageReaderProxy} instance
+ */
+ QueuedImageReaderProxy(int width, int height, int format, int maxImages, Surface surface) {
+ this.width = width;
+ this.height = height;
+ this.format = format;
+ this.maxImages = maxImages;
+ this.surface = surface;
+ images = new ArrayList<>(maxImages);
+ currentPosition = 0;
+ closed = false;
+ }
+
+ @Override
+ @Nullable
+ public synchronized ImageProxy acquireLatestImage() {
+ throwExceptionIfClosed();
+ if (images.isEmpty()) {
+ return null;
+ }
+ if (currentPosition >= images.size()) {
+ throw new IllegalStateException("Max images have already been acquired without close.");
+ }
+
+ // Close all images up to the tail of the list, except for already acquired images.
+ List<ImageProxy> imagesToClose = new ArrayList<>();
+ for (int i = 0; i < images.size() - 1; ++i) {
+ if (!acquiredImages.contains(images.get(i))) {
+ imagesToClose.add(images.get(i));
+ }
+ }
+ for (ImageProxy image : imagesToClose) {
+ // Calling image.close() will cause this.onImageClosed(image) to be called.
+ image.close();
+ }
+
+ // Move the current position to the tail of the list.
+ currentPosition = images.size() - 1;
+ ImageProxy acquiredImage = images.get(currentPosition++);
+ acquiredImages.add(acquiredImage);
+ return acquiredImage;
+ }
+
+ @Override
+ @Nullable
+ public synchronized ImageProxy acquireNextImage() {
+ throwExceptionIfClosed();
+ if (images.isEmpty()) {
+ return null;
+ }
+ if (currentPosition >= images.size()) {
+ throw new IllegalStateException("Max images have already been acquired without close.");
+ }
+ ImageProxy acquiredImage = images.get(currentPosition++);
+ acquiredImages.add(acquiredImage);
+ return acquiredImage;
+ }
+
+ /**
+ * Adds an image to the tail of the queue.
+ *
+ * <p>If the queue already contains the max number of images, the given image is not added to
+ * the queue and is closed. This is consistent with the documented behavior of an {@link
+ * android.media.ImageReader}, where new images may be lost if older images are not closed
+ * quickly enough.
+ *
+ * <p>If the image is added to the queue and an on-image-available listener has been previously
+ * set, the listener is notified that the new image is available.
+ *
+ * @param image to add
+ */
+ synchronized void enqueueImage(ForwardingImageProxy image) {
+ throwExceptionIfClosed();
+ if (images.size() < maxImages) {
+ images.add(image);
+ image.addOnImageCloseListener(this);
+ if (onImageAvailableListener != null && onImageAvailableHandler != null) {
+ final OnImageAvailableListener listener = onImageAvailableListener;
+ onImageAvailableHandler.post(
+ () -> {
+ if (!QueuedImageReaderProxy.this.isClosed()) {
+ listener.onImageAvailable(QueuedImageReaderProxy.this);
+ }
+ });
+ }
+ } else {
+ image.close();
+ }
+ }
+
+ @Override
+ public synchronized void close() {
+ if (!closed) {
+ setOnImageAvailableListener(null, null);
+ // We need to copy into a different list, because closing an image triggers the on-close
+ // listener which in turn modifies the original list.
+ List<ImageProxy> imagesToClose = new ArrayList<>(images);
+ for (ImageProxy image : imagesToClose) {
+ image.close();
+ }
+ images.clear();
+ closed = true;
+ notifyOnReaderCloseListeners();
+ }
+ }
+
+ @Override
+ public int getHeight() {
+ throwExceptionIfClosed();
+ return height;
+ }
+
+ @Override
+ public int getWidth() {
+ throwExceptionIfClosed();
+ return width;
+ }
+
+ @Override
+ public int getImageFormat() {
+ throwExceptionIfClosed();
+ return format;
+ }
+
+ @Override
+ public int getMaxImages() {
+ throwExceptionIfClosed();
+ return maxImages;
+ }
+
+ @Override
+ public synchronized Surface getSurface() {
+ throwExceptionIfClosed();
+ return surface;
+ }
+
+ @Override
+ public synchronized void setOnImageAvailableListener(
+ @Nullable OnImageAvailableListener onImageAvailableListener,
+ @Nullable Handler onImageAvailableHandler) {
+ throwExceptionIfClosed();
+ this.onImageAvailableListener = onImageAvailableListener;
+ this.onImageAvailableHandler = onImageAvailableHandler;
+ }
+
+ @Override
+ public synchronized void onImageClose(ImageProxy image) {
+ int index = images.indexOf(image);
+ if (index >= 0) {
+ images.remove(index);
+ if (index <= currentPosition) {
+ currentPosition--;
+ }
+ }
+ acquiredImages.remove(image);
+ }
+
+ /** Returns the current number of images in the queue. */
+ synchronized int getCurrentImages() {
+ throwExceptionIfClosed();
+ return images.size();
+ }
+
+ /** Returns true if the reader is already closed. */
+ synchronized boolean isClosed() {
+ return closed;
+ }
+
+ /**
+ * Adds a listener for close calls on this reader.
+ *
+ * @param listener to add
+ */
+ synchronized void addOnReaderCloseListener(OnReaderCloseListener listener) {
+ onReaderCloseListeners.add(listener);
+ }
+
+ private synchronized void throwExceptionIfClosed() {
+ if (closed) {
+ throw new IllegalStateException("This reader is already closed.");
+ }
+ }
+
+ private synchronized void notifyOnReaderCloseListeners() {
+ for (OnReaderCloseListener listener : onReaderCloseListeners) {
+ listener.onReaderClose(this);
+ }
+ }
+
+ /** Listener for the reader close event. */
+ interface OnReaderCloseListener {
+ /**
+ * Callback for reader close.
+ *
+ * @param imageReader which is closed
+ */
+ void onReaderClose(ImageReaderProxy imageReader);
+ }
+}
diff --git a/camera/core/src/main/java/androidx/camera/core/ReferenceCountedImageProxy.java b/camera/core/src/main/java/androidx/camera/core/ReferenceCountedImageProxy.java
new file mode 100644
index 0000000..6055768
--- /dev/null
+++ b/camera/core/src/main/java/androidx/camera/core/ReferenceCountedImageProxy.java
@@ -0,0 +1,82 @@
+/*
+ * Copyright (C) 2019 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.core;
+
+import android.media.Image;
+
+import androidx.annotation.GuardedBy;
+import androidx.annotation.Nullable;
+
+/**
+ * An {@link ImageProxy} which allows forking images with reference counting.
+ *
+ * <p>When a new instance is constructed, it starts with a reference count of 1. When {@link
+ * #fork()} is called, the reference count increments by 1. When {@link #close()} is called on a
+ * forked image reference, the reference count decrements by 1. When the reference count reaches 0
+ * after a call to {@link #close()}, the underlying {@link Image} is closed.
+ */
+final class ReferenceCountedImageProxy extends ForwardingImageProxy {
+ @GuardedBy("this")
+ private int referenceCount = 1;
+
+ /**
+ * Creates a new instance which wraps the given image and sets the reference count to 1.
+ *
+ * @param image to wrap
+ * @return a new {@link ReferenceCountedImageProxy} instance
+ */
+ ReferenceCountedImageProxy(ImageProxy image) {
+ super(image);
+ }
+
+ /**
+ * Forks a copy of the image.
+ *
+ * <p>If the reference count is 0, meaning the image has already been closed previously, null is
+ * returned. Otherwise, a forked copy is returned and the reference count is incremented.
+ */
+ @Nullable
+ synchronized ImageProxy fork() {
+ if (referenceCount <= 0) {
+ return null;
+ } else {
+ referenceCount++;
+ return new SingleCloseImageProxy(this);
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * <p>When the image is closed, the reference count is decremented. If the reference count
+ * becomes 0 after this close call, the underlying {@link Image} is also closed.
+ */
+ @Override
+ public synchronized void close() {
+ if (referenceCount > 0) {
+ referenceCount--;
+ if (referenceCount <= 0) {
+ super.close();
+ }
+ }
+ }
+
+ /** Returns the current reference count. */
+ synchronized int getReferenceCount() {
+ return referenceCount;
+ }
+}
diff --git a/camera/core/src/main/java/androidx/camera/core/SessionConfiguration.java b/camera/core/src/main/java/androidx/camera/core/SessionConfiguration.java
new file mode 100644
index 0000000..c39e32b
--- /dev/null
+++ b/camera/core/src/main/java/androidx/camera/core/SessionConfiguration.java
@@ -0,0 +1,359 @@
+/*
+ * Copyright (C) 2019 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.core;
+
+import android.hardware.camera2.CameraCaptureSession;
+import android.hardware.camera2.CameraDevice;
+import android.hardware.camera2.CameraDevice.StateCallback;
+import android.hardware.camera2.CaptureRequest;
+import android.hardware.camera2.CaptureRequest.Key;
+import android.util.Log;
+import android.view.Surface;
+
+import androidx.annotation.RestrictTo;
+import androidx.annotation.RestrictTo.Scope;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Configurations needed for a capture session.
+ *
+ * <p>The SessionConfiguration contains all the {@link android.hardware.camera2} parameters that are
+ * required to initialize a {@link android.hardware.camera2.CameraCaptureSession} and issue a {@link
+ * CaptureRequest}.
+ *
+ * @hide
+ */
+public final class SessionConfiguration {
+
+ /** The set of {@link Surface} that data from the camera will be put into. */
+ private final List<DeferrableSurface> surfaces;
+ /** The state callback for a {@link CameraDevice}. */
+ private final CameraDevice.StateCallback deviceStateCallback;
+ /** The state callback for a {@link CameraCaptureSession}. */
+ private final CameraCaptureSession.StateCallback sessionStateCallback;
+ /** The configuration for building the {@link CaptureRequest}. */
+ private final CaptureRequestConfiguration captureRequestConfiguration;
+
+ /**
+ * Private constructor for a SessionConfiguration.
+ *
+ * <p>In practice, the {@link SessionConfiguration.BaseBuilder} will be used to construct a
+ * SessionConfiguration.
+ *
+ * @param surfaces The set of {@link Surface} where data will be put into.
+ * @param deviceStateCallback The state callback for a {@link CameraDevice}.
+ * @param sessionStateCallback The state callback for a {@link CameraCaptureSession}.
+ * @param captureRequestConfiguration The configuration for building the {@link CaptureRequest}.
+ */
+ SessionConfiguration(
+ List<DeferrableSurface> surfaces,
+ StateCallback deviceStateCallback,
+ CameraCaptureSession.StateCallback sessionStateCallback,
+ CaptureRequestConfiguration captureRequestConfiguration) {
+ this.surfaces = surfaces;
+ this.deviceStateCallback = deviceStateCallback;
+ this.sessionStateCallback = sessionStateCallback;
+ this.captureRequestConfiguration = captureRequestConfiguration;
+ }
+
+ /** Returns an instance of a session configuration with minimal configurations. */
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ public static SessionConfiguration defaultEmptySessionConfiguration() {
+ return new SessionConfiguration(
+ new ArrayList<>(),
+ CameraDeviceStateCallbacks.createNoOpCallback(),
+ CameraCaptureSessionStateCallbacks.createNoOpCallback(),
+ new CaptureRequestConfiguration.Builder().build());
+ }
+
+ public List<DeferrableSurface> getSurfaces() {
+ return Collections.unmodifiableList(surfaces);
+ }
+
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ public Map<Key<?>, CaptureRequestParameter<?>> getCameraCharacteristics() {
+ return captureRequestConfiguration.getCameraCharacteristics();
+ }
+
+ public Configuration getImplementationOptions() {
+ return captureRequestConfiguration.getImplementationOptions();
+ }
+
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ public int getTemplateType() {
+ return captureRequestConfiguration.getTemplateType();
+ }
+
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ public CameraDevice.StateCallback getDeviceStateCallback() {
+ return deviceStateCallback;
+ }
+
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ public CameraCaptureSession.StateCallback getSessionStateCallback() {
+ return sessionStateCallback;
+ }
+
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ public CameraCaptureCallback getCameraCaptureCallback() {
+ return captureRequestConfiguration.getCameraCaptureCallback();
+ }
+
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ public CaptureRequestConfiguration getCaptureRequestConfiguration() {
+ return captureRequestConfiguration;
+ }
+
+ /**
+ * Interface for unpacking a configuration into a SessionConfiguration.Builder
+ *
+ * <p>TODO(b/120949879): This will likely be removed once SessionConfiguration is refactored to
+ * remove camera2 dependencies.
+ */
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ public interface OptionUnpacker {
+ void unpack(UseCaseConfiguration<?> config, SessionConfiguration.Builder builder);
+ }
+
+ /** Base builder for easy modification/rebuilding of a {@link SessionConfiguration}. */
+ static class BaseBuilder {
+ protected final Set<DeferrableSurface> surfaces = new HashSet<>();
+ protected final CaptureRequestConfiguration.Builder captureRequestConfigBuilder =
+ new CaptureRequestConfiguration.Builder();
+ protected CameraDevice.StateCallback deviceStateCallback =
+ CameraDeviceStateCallbacks.createNoOpCallback();
+ protected CameraCaptureSession.StateCallback sessionStateCallback =
+ CameraCaptureSessionStateCallbacks.createNoOpCallback();
+ }
+
+ /** Builder for easy modification/rebuilding of a {@link SessionConfiguration}. */
+ public static class Builder extends BaseBuilder {
+ /**
+ * Creates a {@link Builder} from a {@link UseCaseConfiguration}.
+ *
+ * <p>Populates the builder with all the properties defined in the base configuration.
+ */
+ public static Builder createFrom(UseCaseConfiguration<?> configuration) {
+ OptionUnpacker unpacker = configuration.getOptionUnpacker(null);
+ if (unpacker == null) {
+ throw new IllegalStateException(
+ "Implementation is missing option unpacker for "
+ + configuration.getTargetName(configuration.toString()));
+ }
+
+ Builder builder = new Builder();
+
+ // Unpack the configuration into this builder
+ unpacker.unpack(configuration, builder);
+ return builder;
+ }
+
+ /**
+ * Set the template characteristics of the SessionConfiguration.
+ *
+ * @param templateType Template constant that must match those defined by {@link
+ * CameraDevice}
+ * <p>TODO(b/120949879): This is camera2 implementation detail that
+ * should be moved
+ */
+ public void setTemplateType(int templateType) {
+ captureRequestConfigBuilder.setTemplateType(templateType);
+ }
+
+ // TODO(b/120949879): This is camera2 implementation detail that should be moved
+ public void setDeviceStateCallback(CameraDevice.StateCallback deviceStateCallback) {
+ this.deviceStateCallback = deviceStateCallback;
+ }
+
+ // TODO(b/120949879): This is camera2 implementation detail that should be moved
+ public void setSessionStateCallback(
+ CameraCaptureSession.StateCallback sessionStateCallback) {
+ this.sessionStateCallback = sessionStateCallback;
+ }
+
+ public void setCameraCaptureCallback(CameraCaptureCallback cameraCaptureCallback) {
+ captureRequestConfigBuilder.setCameraCaptureCallback(cameraCaptureCallback);
+ }
+
+ public void addSurface(DeferrableSurface surface) {
+ surfaces.add(surface);
+ captureRequestConfigBuilder.addSurface(surface);
+ }
+
+ public void addNonRepeatingSurface(DeferrableSurface surface) {
+ surfaces.add(surface);
+ }
+
+ public void removeSurface(DeferrableSurface surface) {
+ surfaces.remove(surface);
+ captureRequestConfigBuilder.removeSurface(surface);
+ }
+
+ public void clearSurfaces() {
+ surfaces.clear();
+ captureRequestConfigBuilder.clearSurfaces();
+ }
+
+ // TODO(b/120949879): This is camera2 implementation detail that should be moved
+ public <T> void addCharacteristic(Key<T> key, T value) {
+ captureRequestConfigBuilder.addCharacteristic(key, value);
+ }
+
+ // TODO(b/120949879): This is camera2 implementation detail that should be moved
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ public void addCharacteristics(Map<Key<?>, CaptureRequestParameter<?>> characteristics) {
+ captureRequestConfigBuilder.addCharacteristics(characteristics);
+ }
+
+ public void setImplementationOptions(Configuration config) {
+ captureRequestConfigBuilder.setImplementationOptions(config);
+ }
+
+ /**
+ * Builds an instance of a SessionConfiguration that has all the combined parameters of the
+ * SessionConfiguration that have been added to the Builder.
+ */
+ public SessionConfiguration build() {
+ return new SessionConfiguration(
+ new ArrayList<>(surfaces),
+ deviceStateCallback,
+ sessionStateCallback,
+ captureRequestConfigBuilder.build());
+ }
+ }
+
+ /**
+ * Builder for combining multiple instances of {@link SessionConfiguration}. This will check if
+ * all the parameters for the {@link SessionConfiguration} are compatible with each other
+ */
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ public static final class ValidatingBuilder extends BaseBuilder {
+ private static final String TAG = "ValidatingBuilder";
+ private final List<CameraDevice.StateCallback> deviceStateCallbacks = new ArrayList<>();
+ private final List<CameraCaptureSession.StateCallback> sessionStateCallbacks =
+ new ArrayList<>();
+ private final List<CameraCaptureCallback> cameraCaptureCallbacks = new ArrayList<>();
+ private boolean valid = true;
+ private boolean templateSet = false;
+
+ /**
+ * Add the SessionConfiguration to the set of SessionConfiguration that have been aggregated
+ * by the ValidatingBuilder
+ */
+ public void add(SessionConfiguration sessionConfiguration) {
+ CaptureRequestConfiguration captureRequestConfiguration =
+ sessionConfiguration.getCaptureRequestConfiguration();
+
+ // Check template
+ if (!templateSet) {
+ captureRequestConfigBuilder.setTemplateType(
+ captureRequestConfiguration.getTemplateType());
+ templateSet = true;
+ } else if (captureRequestConfigBuilder.getTemplateType()
+ != captureRequestConfiguration.getTemplateType()) {
+ String errorMessage =
+ "Invalid configuration due to template type: "
+ + captureRequestConfigBuilder.getTemplateType()
+ + " != "
+ + captureRequestConfiguration.getTemplateType();
+ Log.d(TAG, errorMessage);
+ valid = false;
+ }
+
+ // Check device state callback
+ deviceStateCallbacks.add(sessionConfiguration.getDeviceStateCallback());
+
+ // Check session state callback
+ sessionStateCallbacks.add(sessionConfiguration.getSessionStateCallback());
+
+ // Check camera capture callback
+ cameraCaptureCallbacks.add(captureRequestConfiguration.getCameraCaptureCallback());
+
+ // Check surfaces
+ surfaces.addAll(sessionConfiguration.getSurfaces());
+
+ // Check capture request surfaces
+ captureRequestConfigBuilder
+ .getSurfaces()
+ .addAll(captureRequestConfiguration.getSurfaces());
+
+ captureRequestConfigBuilder.addImplementationOptions(
+ captureRequestConfiguration.getImplementationOptions());
+
+ if (!surfaces.containsAll(captureRequestConfigBuilder.getSurfaces())) {
+ String errorMessage =
+ "Invalid configuration due to capture request surfaces are not a subset "
+ + "of surfaces";
+ Log.d(TAG, errorMessage);
+ valid = false;
+ }
+
+ // Check characteristics
+ for (Map.Entry<Key<?>, CaptureRequestParameter<?>> entry :
+ captureRequestConfiguration.getCameraCharacteristics().entrySet()) {
+ Key<?> addedKey = entry.getKey();
+ if (captureRequestConfigBuilder.getCharacteristic().containsKey(entry.getKey())) {
+ // value is equal
+ CaptureRequestParameter<?> addedValue = entry.getValue();
+ CaptureRequestParameter<?> oldValue =
+ captureRequestConfigBuilder.getCharacteristic().get(addedKey);
+ if (!addedValue.getValue().equals(oldValue.getValue())) {
+ String errorMessage =
+ "Invalid configuration due to conflicting CaptureRequest.Keys: "
+ + addedValue
+ + " != "
+ + oldValue;
+ Log.d(TAG, errorMessage);
+ valid = false;
+ }
+ } else {
+ captureRequestConfigBuilder
+ .getCharacteristic()
+ .put(entry.getKey(), entry.getValue());
+ }
+ }
+ }
+
+ /** Check if the set of SessionConfiguration that have been combined are valid */
+ public boolean isValid() {
+ return templateSet && valid;
+ }
+
+ /**
+ * Builds an instance of a SessionConfiguration that has all the combined parameters of the
+ * SessionConfiguration that have been added to the ValidatingBuilder.
+ */
+ public SessionConfiguration build() {
+ if (!valid) {
+ throw new IllegalArgumentException("Unsupported session configuration combination");
+ }
+ captureRequestConfigBuilder.setCameraCaptureCallback(
+ CameraCaptureCallbacks.createComboCallback(cameraCaptureCallbacks));
+ return new SessionConfiguration(
+ new ArrayList<>(surfaces),
+ CameraDeviceStateCallbacks.createComboCallback(deviceStateCallbacks),
+ CameraCaptureSessionStateCallbacks.createComboCallback(sessionStateCallbacks),
+ captureRequestConfigBuilder.build());
+ }
+ }
+}
diff --git a/camera/core/src/main/java/androidx/camera/core/SingleCloseImageProxy.java b/camera/core/src/main/java/androidx/camera/core/SingleCloseImageProxy.java
new file mode 100644
index 0000000..0fa842a
--- /dev/null
+++ b/camera/core/src/main/java/androidx/camera/core/SingleCloseImageProxy.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2019 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.core;
+
+import androidx.annotation.GuardedBy;
+
+/** A {@link ImageProxy} which filters out redundant calls to {@link #close()}. */
+final class SingleCloseImageProxy extends ForwardingImageProxy {
+ @GuardedBy("this")
+ private boolean closed = false;
+
+ /**
+ * Creates a new instances which wraps the given image.
+ *
+ * @param image to wrap
+ * @return new {@link SingleCloseImageProxy} instance
+ */
+ SingleCloseImageProxy(ImageProxy image) {
+ super(image);
+ }
+
+ @Override
+ public synchronized void close() {
+ if (!closed) {
+ closed = true;
+ super.close();
+ }
+ }
+}
diff --git a/camera/core/src/main/java/androidx/camera/core/SurfaceCombination.java b/camera/core/src/main/java/androidx/camera/core/SurfaceCombination.java
new file mode 100644
index 0000000..d8ae203
--- /dev/null
+++ b/camera/core/src/main/java/androidx/camera/core/SurfaceCombination.java
@@ -0,0 +1,146 @@
+/*
+ * Copyright (C) 2019 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.core;
+
+import androidx.annotation.RestrictTo;
+import androidx.annotation.RestrictTo.Scope;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Surface configuration combination
+ *
+ * <p>{@link android.hardware.camera2.CameraDevice#createCaptureSession} defines the default
+ * guaranteed stream combinations for different hardware level devices. It defines what combination
+ * of surface configuration type and size pairs can be supported for different hardware level camera
+ * devices. This structure is used to store a list of surface configuration as a combination.
+ *
+ * @hide
+ */
+@RestrictTo(Scope.LIBRARY_GROUP)
+public final class SurfaceCombination {
+
+ private final List<SurfaceConfiguration> surfaceConfigurationList = new ArrayList<>();
+ ;
+
+ public SurfaceCombination() {
+ }
+
+ private static void generateArrangements(
+ List<int[]> arrangementsResultList, int n, int[] result, int index) {
+ if (index >= result.length) {
+ arrangementsResultList.add(result.clone());
+ return;
+ }
+
+ for (int i = 0; i < n; i++) {
+ boolean included = false;
+
+ for (int j = 0; j < index; j++) {
+ if (i == result[j]) {
+ included = true;
+ break;
+ }
+ }
+
+ if (!included) {
+ result[index] = i;
+ generateArrangements(arrangementsResultList, n, result, index + 1);
+ }
+ }
+ }
+
+ public boolean addSurfaceConfiguration(SurfaceConfiguration surfaceConfiguration) {
+ if (surfaceConfiguration == null) {
+ return false;
+ }
+
+ return surfaceConfigurationList.add(surfaceConfiguration);
+ }
+
+ public boolean removeSurfaceConfiguration(SurfaceConfiguration surfaceConfiguration) {
+ if (surfaceConfiguration == null) {
+ return false;
+ }
+
+ return surfaceConfigurationList.remove(surfaceConfiguration);
+ }
+
+ public List<SurfaceConfiguration> getSurfaceConfigurationList() {
+ return surfaceConfigurationList;
+ }
+
+ /**
+ * Check whether the input surface configuration list is under the capability of the combination
+ * of this object.
+ *
+ * @param configurationList the surface configuration list to be compared
+ * @return the check result that whether it could be supported
+ */
+ public boolean isSupported(List<SurfaceConfiguration> configurationList) {
+ boolean isSupported = false;
+
+ if (configurationList == null || configurationList.isEmpty()) {
+ return true;
+ }
+
+ /**
+ * Sublist of this surfaceConfiguration may be able to support the desired configuration.
+ * For example, (PRIV, PREVIEW) + (PRIV, ANALYSIS) + (JPEG, MAXIMUM) can supported by the
+ * following level3 camera device combination - (PRIV, PREVIEW) + (PRIV, ANALYSIS) + (JPEG,
+ * MAXIMUM) + (RAW, MAXIMUM).
+ */
+ if (configurationList.size() > surfaceConfigurationList.size()) {
+ return false;
+ }
+
+ List<int[]> elementsArrangements = getElementsArrangements(surfaceConfigurationList.size());
+
+ for (int[] elementsArrangement : elementsArrangements) {
+ boolean checkResult = true;
+
+ for (int index = 0; index < surfaceConfigurationList.size(); index++) {
+ if (elementsArrangement[index] < configurationList.size()) {
+ checkResult &=
+ surfaceConfigurationList
+ .get(index)
+ .isSupported(configurationList.get(elementsArrangement[index]));
+
+ if (!checkResult) {
+ break;
+ }
+ }
+ }
+
+ if (checkResult) {
+ isSupported = true;
+ break;
+ }
+ }
+
+ return isSupported;
+ }
+
+ private List<int[]> getElementsArrangements(int n) {
+ List<int[]> arrangementsResultList = new ArrayList<>();
+
+ generateArrangements(arrangementsResultList, n, new int[n], 0);
+
+ return arrangementsResultList;
+ }
+}
diff --git a/camera/core/src/main/java/androidx/camera/core/SurfaceConfiguration.java b/camera/core/src/main/java/androidx/camera/core/SurfaceConfiguration.java
new file mode 100644
index 0000000..b1e6e80
--- /dev/null
+++ b/camera/core/src/main/java/androidx/camera/core/SurfaceConfiguration.java
@@ -0,0 +1,122 @@
+/*
+ * Copyright (C) 2019 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.core;
+
+import android.hardware.camera2.CameraCaptureSession.StateCallback;
+import android.os.Handler;
+
+import androidx.annotation.RestrictTo;
+import androidx.annotation.RestrictTo.Scope;
+
+import com.google.auto.value.AutoValue;
+
+import java.util.List;
+
+/**
+ * Surface configuration type and size pair
+ *
+ * <p>{@link android.hardware.camera2.CameraDevice#createCaptureSession} defines the default
+ * guaranteed stream combinations for different hardware level devices. It defines what combination
+ * of surface configuration type and size pairs can be supported for different hardware level camera
+ * devices.
+ *
+ * @hide
+ */
+@RestrictTo(Scope.LIBRARY_GROUP)
+@AutoValue
+public abstract class SurfaceConfiguration {
+ /** Prevent sublcassing */
+ SurfaceConfiguration() {
+ }
+
+ public static SurfaceConfiguration create(ConfigurationType type, ConfigurationSize size) {
+ return new AutoValue_SurfaceConfiguration(type, size);
+ }
+
+ public abstract ConfigurationType getConfigurationType();
+
+ public abstract ConfigurationSize getConfigurationSize();
+
+ /**
+ * Check whether the input surface configuration has a smaller size than this object and can be
+ * supported
+ *
+ * @param surfaceConfiguration the surface configuration to be compared
+ * @return the check result that whether it could be supported
+ */
+ public final boolean isSupported(SurfaceConfiguration surfaceConfiguration) {
+ boolean isSupported = false;
+ ConfigurationType configurationType = surfaceConfiguration.getConfigurationType();
+ ConfigurationSize configurationSize = surfaceConfiguration.getConfigurationSize();
+
+ // Check size and type to make sure it could be supported
+ if (configurationSize.id <= getConfigurationSize().id
+ && configurationType == getConfigurationType()) {
+ isSupported = true;
+ }
+ return isSupported;
+ }
+
+ /**
+ * The Camera2 configuration type for the surface.
+ *
+ * <p>These are the enumerations defined in {@link
+ * android.hardware.camera2.CameraDevice#createCaptureSession(List, StateCallback, Handler)}.
+ */
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ public enum ConfigurationType {
+ PRIV,
+ YUV,
+ JPEG,
+ RAW
+ }
+
+ /**
+ * The Camera2 stream sizes for the surface.
+ *
+ * <p>These are the enumerations defined in {@link
+ * android.hardware.camera2.CameraDevice#createCaptureSession(List, StateCallback, Handler)}.
+ */
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ public enum ConfigurationSize {
+ /** Default AYALYSIS size is 640x480. */
+ ANALYSIS(0),
+ /**
+ * PREVIEW refers to the best size match to the device's screen resolution, or to 1080p
+ * (1920x1080), whichever is smaller.
+ */
+ PREVIEW(1),
+ /**
+ * RECORD refers to the camera device's maximum supported recording resolution, as
+ * determined by CamcorderProfile.
+ */
+ RECORD(2),
+ /**
+ * MAXIMUM refers to the camera device's maximum output resolution for that format or target
+ * from StreamConfigurationMap.getOutputSizes(int)
+ */
+ MAXIMUM(3),
+ /** NOT_SUPPORT is for the size larger than MAXIMUM */
+ NOT_SUPPORT(4);
+
+ final int id;
+
+ ConfigurationSize(int id) {
+ this.id = id;
+ }
+ }
+}
diff --git a/camera/core/src/main/java/androidx/camera/core/SurfaceSizeDefinition.java b/camera/core/src/main/java/androidx/camera/core/SurfaceSizeDefinition.java
new file mode 100644
index 0000000..c79ce37
--- /dev/null
+++ b/camera/core/src/main/java/androidx/camera/core/SurfaceSizeDefinition.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright (C) 2019 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.core;
+
+import android.util.Size;
+
+import androidx.annotation.RestrictTo;
+import androidx.annotation.RestrictTo.Scope;
+
+import com.google.auto.value.AutoValue;
+
+import java.util.Map;
+
+/**
+ * Camera device surface size definition
+ *
+ * <p>{@link android.hardware.camera2.CameraDevice#createCaptureSession} defines the default
+ * guaranteed stream combinations for different hardware level devices.
+ *
+ * @hide
+ */
+@RestrictTo(Scope.LIBRARY_GROUP)
+@SuppressWarnings("AutoValueImmutableFields")
+@AutoValue
+public abstract class SurfaceSizeDefinition {
+
+ /** Prevent subclassing */
+ SurfaceSizeDefinition() {
+ }
+
+ /**
+ * Create a SurfaceSizeDenifition object with input analysis, preview, record and maximum sizes
+ *
+ * @param analysisSize Default AYALYSIS size is * 640x480.
+ * @param previewSize PREVIEW refers to the best size match to the device's screen
+ * resolution,
+ * or to 1080p * (1920x1080), whichever is smaller.
+ * @param recordSize RECORD refers to the camera device's maximum supported * recording
+ * resolution, as determined by CamcorderProfile.
+ * @param maximumSizeMap MAXIMUM refers to the camera * device's maximum output resolution for
+ * that format or target from * StreamConfigurationMap.getOutputSizes(int)
+ * @return new {@link SurfaceSizeDefinition} object
+ */
+ public static SurfaceSizeDefinition create(
+ Size analysisSize,
+ Size previewSize,
+ Size recordSize,
+ Map<Integer, Size> maximumSizeMap) {
+ return new AutoValue_SurfaceSizeDefinition(
+ analysisSize, previewSize, recordSize, maximumSizeMap);
+ }
+
+ public abstract Size getAnalysisSize();
+
+ public abstract Size getPreviewSize();
+
+ public abstract Size getRecordSize();
+
+ public abstract Map<Integer, Size> getMaximumSizeMap();
+}
diff --git a/camera/core/src/main/java/androidx/camera/core/TargetConfiguration.java b/camera/core/src/main/java/androidx/camera/core/TargetConfiguration.java
new file mode 100644
index 0000000..ce95a8a
--- /dev/null
+++ b/camera/core/src/main/java/androidx/camera/core/TargetConfiguration.java
@@ -0,0 +1,158 @@
+/*
+ * Copyright (C) 2019 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.core;
+
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.annotation.RestrictTo.Scope;
+
+import java.util.UUID;
+
+/**
+ * Configuration containing options used to identify the target class and object being configured.
+ *
+ * @param <T> The type of the object being configured.
+ */
+public interface TargetConfiguration<T> extends Configuration.Reader {
+
+ /**
+ * Option: camerax.core.target.name
+ *
+ * @hide
+ */
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ Option<String> OPTION_TARGET_NAME = Option.create("camerax.core.target.name", String.class);
+ /**
+ * Option: camerax.core.target.class
+ *
+ * @hide
+ */
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ Option<Class<?>> OPTION_TARGET_CLASS =
+ Option.create("camerax.core.target.class", new TypeReference<Class<?>>() {
+ });
+
+ /**
+ * Retrieves the class of the object being configured.
+ *
+ * @param valueIfMissing The value to return if this configuration option has not been set.
+ * @return The stored value or <code>valueIfMissing</code> if the value does not exist in this
+ * configuration.
+ */
+ @Nullable
+ default Class<T> getTargetClass(@Nullable Class<T> valueIfMissing) {
+ @SuppressWarnings("unchecked") // Value should only be added via Builder#setTargetClass()
+ Class<T> storedClass = (Class<T>) retrieveOption(OPTION_TARGET_CLASS,
+ valueIfMissing);
+ return storedClass;
+ }
+
+ /**
+ * Retrieves the class of the object being configured.
+ *
+ * @return The stored value, if it exists in this configuration.
+ * @throws IllegalArgumentException if the option does not exist in this configuration.
+ */
+ default Class<T> getTargetClass() {
+ @SuppressWarnings("unchecked") // Value should only be added via Builder#setTargetClass()
+ Class<T> storedClass = (Class<T>) retrieveOption(OPTION_TARGET_CLASS);
+ return storedClass;
+ }
+
+ /**
+ * Retrieves the name of the target object being configured.
+ *
+ * <p>The name should be a value that can uniquely identify an instance of the object being
+ * configured.
+ *
+ * @param valueIfMissing The value to return if this configuration option has not been set.
+ * @return The stored value or <code>valueIfMissing</code> if the value does not exist in this
+ * configuration.
+ */
+ @Nullable
+ default String getTargetName(@Nullable String valueIfMissing) {
+ return retrieveOption(OPTION_TARGET_NAME, valueIfMissing);
+ }
+
+ // Option Declarations:
+ // ***********************************************************************************************
+
+ /**
+ * Retrieves the name of the target object being configured.
+ *
+ * <p>The name should be a value that can uniquely identify an instance of the object being
+ * configured.
+ *
+ * @return The stored value, if it exists in this configuration.
+ * @throws IllegalArgumentException if the option does not exist in this configuration.
+ */
+ default String getTargetName() {
+ return retrieveOption(OPTION_TARGET_NAME);
+ }
+
+ /**
+ * Builder for a {@link TargetConfiguration}.
+ *
+ * <p>A {@link TargetConfiguration} contains options used to identify the target class and
+ * object being configured.
+ *
+ * @param <T> The type of the object being configured.
+ * @param <C> The top level configuration which will be generated by {@link #build()}.
+ * @param <B> The top level builder type for which this builder is composed with.
+ */
+ interface Builder<T, C extends Configuration, B extends Builder<T, C, B>>
+ extends Configuration.Builder<C, B> {
+
+ /**
+ * Sets the class of the object being configured.
+ *
+ * <p>Setting the target class will automatically generate a unique target name if one does
+ * not already exist in this configuration.
+ *
+ * @param targetClass A class object corresponding to the class of the object being
+ * configured.
+ * @return the current Builder.
+ * @hide
+ */
+ default B setTargetClass(Class<T> targetClass) {
+ getMutableConfiguration().insertOption(OPTION_TARGET_CLASS, targetClass);
+
+ // If no name is set yet, then generate a unique name
+ if (null == getMutableConfiguration().retrieveOption(OPTION_TARGET_NAME, null)) {
+ String targetName = targetClass.getCanonicalName() + "-" + UUID.randomUUID();
+ setTargetName(targetName);
+ }
+
+ return builder();
+ }
+
+ /**
+ * Sets the name of the target object being configured.
+ *
+ * <p>The name should be a value that can uniquely identify an instance of the object being
+ * configured.
+ *
+ * @param targetName A unique string identifier for the instance of the class being
+ * configured.
+ * @return the current Builder.
+ */
+ default B setTargetName(String targetName) {
+ getMutableConfiguration().insertOption(OPTION_TARGET_NAME, targetName);
+ return builder();
+ }
+ }
+}
diff --git a/camera/core/src/main/java/androidx/camera/core/ThreadConfiguration.java b/camera/core/src/main/java/androidx/camera/core/ThreadConfiguration.java
new file mode 100644
index 0000000..c727025
--- /dev/null
+++ b/camera/core/src/main/java/androidx/camera/core/ThreadConfiguration.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright (C) 2019 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.core;
+
+import android.os.Handler;
+
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.annotation.RestrictTo.Scope;
+
+/** Configuration containing options pertaining to threads used by the configured object. */
+public interface ThreadConfiguration extends Configuration.Reader {
+
+ /**
+ * Option: camerax.core.thread.callbackHandler
+ *
+ * @hide
+ */
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ Option<Handler> OPTION_CALLBACK_HANDLER =
+ Option.create("camerax.core.thread.callbackHandler", Handler.class);
+
+ /**
+ * Returns the default handler that will be used for callbacks.
+ *
+ * @param valueIfMissing The value to return if this configuration option has not been set.
+ * @return The stored value or <code>valueIfMissing</code> if the value does not exist in this
+ * configuration.
+ */
+ @Nullable
+ default Handler getCallbackHandler(@Nullable Handler valueIfMissing) {
+ return retrieveOption(OPTION_CALLBACK_HANDLER, valueIfMissing);
+ }
+
+ /**
+ * Returns the default handler that will be used for callbacks.
+ *
+ * @return The stored value, if it exists in this configuration.
+ * @throws IllegalArgumentException if the option does not exist in this configuration.
+ */
+ default Handler getCallbackHandler() {
+ return retrieveOption(OPTION_CALLBACK_HANDLER);
+ }
+
+ // Option Declarations:
+ // ***********************************************************************************************
+
+ /**
+ * Builder for a {@link ThreadConfiguration}.
+ *
+ * @param <C> The top level configuration which will be generated by {@link #build()}.
+ * @param <B> The top level builder type for which this builder is composed with.
+ */
+ interface Builder<C extends Configuration, B extends Builder<C, B>>
+ extends Configuration.Builder<C, B> {
+
+ /**
+ * {@inheritDoc}
+ *
+ * @hide
+ */
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ @Override
+ MutableConfiguration getMutableConfiguration();
+
+ /**
+ * Sets the default handler that will be used for callbacks.
+ *
+ * @param handler The handler which will be used to post callbacks.
+ * @return the current Builder.
+ */
+ default B setCallbackHandler(Handler handler) {
+ getMutableConfiguration().insertOption(OPTION_CALLBACK_HANDLER, handler);
+ return builder();
+ }
+ }
+}
diff --git a/camera/core/src/main/java/androidx/camera/core/TypeReference.java b/camera/core/src/main/java/androidx/camera/core/TypeReference.java
new file mode 100644
index 0000000..e2c3d02
--- /dev/null
+++ b/camera/core/src/main/java/androidx/camera/core/TypeReference.java
@@ -0,0 +1,376 @@
+/*
+ * Copyright (C) 2018 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.core;
+
+import androidx.annotation.RestrictTo;
+import androidx.annotation.RestrictTo.Scope;
+
+import java.lang.reflect.Array;
+import java.lang.reflect.GenericArrayType;
+import java.lang.reflect.ParameterizedType;
+import java.lang.reflect.Type;
+import java.lang.reflect.TypeVariable;
+import java.lang.reflect.WildcardType;
+
+/**
+ * Super type token; allows capturing generic types at runtime by forcing them to be reified.
+ *
+ * <p>Usage example:
+ *
+ * <pre>{@code
+ * // using anonymous classes (preferred)
+ * TypeReference<Integer> intToken = new TypeReference<Integer>() {{ }};
+ *
+ * // using named classes
+ * class IntTypeReference extends TypeReference<Integer> {...}
+ * TypeReference<Integer> intToken = new IntTypeReference();
+ * }</p>
+ * </pre>
+ *
+ * <p>Unlike the reference implementation, this bans nested TypeVariables; that is all dynamic types
+ * must equal to the static types.
+ *
+ * <p>See <a href="http://gafter.blogspot.com/2007/05/limitation-of-super-type-tokens.html">
+ * http://gafter.blogspot.com/2007/05/limitation-of-super-type-tokens.html</a> for more details.
+ *
+ * @hide
+ */
+@RestrictTo(Scope.LIBRARY_GROUP)
+public abstract class TypeReference<T> {
+ private final Type type;
+ private final int hash;
+
+ /**
+ * Create a new type reference for {@code T}.
+ *
+ * @throws IllegalArgumentException if {@code T}'s actual type contains a type variable
+ * @see TypeReference
+ */
+ protected TypeReference() {
+ ParameterizedType thisType = (ParameterizedType) getClass().getGenericSuperclass();
+
+ // extract the "T" from TypeReference<T>
+ type = thisType.getActualTypeArguments()[0];
+
+ /*
+ * Prohibit type references with type variables such as
+ *
+ * class GenericListToken<T> extends TypeReference<List<T>>
+ *
+ * Since the "T" there is not known without an instance of T, type equality would
+ * consider *all* Lists equal regardless of T. Allowing this would defeat
+ * some of the type safety of a type reference.
+ */
+ if (containsTypeVariable(type)) {
+ throw new IllegalArgumentException(
+ "Including a type variable in a type reference is not allowed");
+ }
+ hash = type.hashCode();
+ }
+
+ TypeReference(Type type) {
+ this.type = type;
+ if (containsTypeVariable(this.type)) {
+ throw new IllegalArgumentException(
+ "Including a type variable in a type reference is not allowed");
+ }
+ hash = this.type.hashCode();
+ }
+
+ /**
+ * Create a specialized type reference from a dynamic class instance, bypassing the standard
+ * compile-time checks.
+ *
+ * <p>As with a regular type reference, the {@code klass} must not contain any type variables.
+ *
+ * @param klass a non-{@code null} {@link Class} instance
+ * @return a type reference which captures {@code T} at runtime
+ * @throws IllegalArgumentException if {@code T} had any type variables
+ */
+ public static <T> TypeReference<T> createSpecializedTypeReference(Class<T> klass) {
+ return new SpecializedTypeReference<T>(klass);
+ }
+
+ private static final Class<?> getRawType(Type type) {
+ if (type == null) {
+ throw new NullPointerException("type must not be null");
+ }
+
+ if (type instanceof Class<?>) {
+ return (Class<?>) type;
+ } else if (type instanceof ParameterizedType) {
+ return (Class<?>) ((ParameterizedType) type).getRawType();
+ } else if (type instanceof GenericArrayType) {
+ return getArrayClass(getRawType(((GenericArrayType) type).getGenericComponentType()));
+ } else if (type instanceof WildcardType) {
+ // Should be at most 1 upper bound, but treat it like an array for simplicity
+ return getRawType(((WildcardType) type).getUpperBounds());
+ } else if (type instanceof TypeVariable) {
+ throw new AssertionError("Type variables are not allowed in type references");
+ } else {
+ // Impossible
+ throw new AssertionError("Unhandled branch to get raw type for type " + type);
+ }
+ }
+
+ private static final Class<?> getRawType(Type[] types) {
+ if (types == null) {
+ return null;
+ }
+
+ for (Type type : types) {
+ Class<?> klass = getRawType(type);
+ if (klass != null) {
+ return klass;
+ }
+ }
+
+ return null;
+ }
+
+ private static final Class<?> getArrayClass(Class<?> componentType) {
+ return Array.newInstance(componentType, 0).getClass();
+ }
+
+ /**
+ * Check if the {@code type} contains a {@link TypeVariable} recursively.
+ *
+ * <p>Intuitively, a type variable is a type in a type expression that refers to a generic type
+ * which is not known at the definition of the expression (commonly seen when type parameters
+ * are used, e.g. {@code class Foo<T>}).
+ *
+ * <p>See <a href="http://docs.oracle.com/javase/specs/jls/se7/html/jls-4.html#jls-4.4">
+ * http://docs.oracle.com/javase/specs/jls/se7/html/jls-4.html#jls-4.4</a> for a more formal
+ * definition of a type variable.
+ *
+ * @param type a type object ({@code null} is allowed)
+ * @return {@code true} if there were nested type variables; {@code false} otherwise
+ */
+ public static boolean containsTypeVariable(Type type) {
+ if (type == null) {
+ // Trivially false
+ return false;
+ } else if (type instanceof TypeVariable<?>) {
+ /*
+ * T -> trivially true
+ */
+ return true;
+ } else if (type instanceof Class<?>) {
+ /*
+ * class Foo -> no type variable
+ * class Foo<T> - has a type variable
+ *
+ * This also covers the case of class Foo<T> extends ... / implements ...
+ * since everything on the right hand side would either include a type variable T
+ * or have no type variables.
+ */
+ Class<?> klass = (Class<?>) type;
+
+ // Empty array => class is not generic
+ if (klass.getTypeParameters().length != 0) {
+ return true;
+ } else {
+ // Does the outer class(es) contain any type variables?
+
+ /*
+ * class Outer<T> {
+ * class Inner {
+ * T field;
+ * }
+ * }
+ *
+ * In this case 'Inner' has no type parameters itself, but it still has a type
+ * variable as part of the type definition.
+ */
+ return containsTypeVariable(klass.getDeclaringClass());
+ }
+ } else if (type instanceof ParameterizedType) {
+ /*
+ * This is the "Foo<T1, T2, T3, ... Tn>" in the scope of a
+ *
+ * // no type variables here, T1-Tn are known at this definition
+ * class X extends Foo<T1, T2, T3, ... Tn>
+ *
+ * // T1 is a type variable, T2-Tn are known at this definition
+ * class X<T1> extends Foo<T1, T2, T3, ... Tn>
+ */
+ ParameterizedType p = (ParameterizedType) type;
+
+ // This needs to be recursively checked
+ for (Type arg : p.getActualTypeArguments()) {
+ if (containsTypeVariable(arg)) {
+ return true;
+ }
+ }
+
+ return false;
+ } else if (type instanceof WildcardType) {
+ WildcardType wild = (WildcardType) type;
+
+ /*
+ * This is is the "?" inside of a
+ *
+ * Foo<?> --> unbounded; trivially no type variables
+ * Foo<? super T> --> lower bound; does T have a type variable?
+ * Foo<? extends T> --> upper bound; does T have a type variable?
+ */
+
+ /*
+ * According to JLS 4.5.1
+ * (http://java.sun.com/docs/books/jls/third_edition/html/typesValues.html#4.5.1):
+ *
+ * - More than 1 lower/upper bound is illegal
+ * - Both a lower and upper bound is illegal
+ *
+ * However, we use this 'array OR array' approach for readability
+ */
+ return containsTypeVariable(wild.getLowerBounds())
+ || containsTypeVariable(wild.getUpperBounds());
+ }
+
+ return false;
+ }
+
+ /**
+ * Check if any of the elements in this array contained a type variable.
+ *
+ * <p>Empty and null arrays trivially have no type variables.
+ *
+ * @param typeArray an array ({@code null} is ok) of types
+ * @return true if any elements contained a type variable; false otherwise
+ */
+ private static boolean containsTypeVariable(Type[] typeArray) {
+ if (typeArray == null) {
+ return false;
+ }
+
+ for (Type type : typeArray) {
+ if (containsTypeVariable(type)) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ private static void toString(Type type, StringBuilder out) {
+ if (type != null) {
+ if (type instanceof TypeVariable<?>) {
+ // T
+ out.append(((TypeVariable<?>) type).getName());
+ } else if (type instanceof Class<?>) {
+ Class<?> klass = (Class<?>) type;
+
+ out.append(klass.getName());
+ toString(klass.getTypeParameters(), out);
+ } else if (type instanceof ParameterizedType) {
+ // "Foo<T1, T2, T3, ... Tn>"
+ ParameterizedType p = (ParameterizedType) type;
+
+ out.append(((Class<?>) p.getRawType()).getName());
+ toString(p.getActualTypeArguments(), out);
+ } else if (type instanceof GenericArrayType) {
+ GenericArrayType gat = (GenericArrayType) type;
+
+ toString(gat.getGenericComponentType(), out);
+ out.append("[]");
+ } else { // WildcardType, BoundedType
+ // TODO:
+ out.append(type);
+ }
+ }
+ }
+
+ private static void toString(Type[] types, StringBuilder out) {
+ if (types == null) {
+ return;
+ } else if (types.length == 0) {
+ return;
+ }
+
+ out.append("<");
+
+ for (int i = 0; i < types.length; ++i) {
+ toString(types[i], out);
+ if (i != types.length - 1) {
+ out.append(", ");
+ }
+ }
+
+ out.append(">");
+ }
+
+ /** Return the dynamic {@link Type} corresponding to the captured type {@code T}. */
+ public Type getType() {
+ return type;
+ }
+
+ /**
+ * Returns the raw type of T.
+ *
+ * <p>
+ *
+ * <ul>
+ * <li>If T is a Class itself, T itself is returned.
+ * <li>If T is a ParameterizedType, the raw type of the parameterized type is returned.
+ * <li>If T is a GenericArrayType, the returned type is the corresponding array class. For
+ * example: {@code List<Integer>[]} => {@code List[]}.
+ * <li>If T is a type variable or a wildcard type, the raw type of the first upper bound is
+ * returned. For example: {@code <X extends Foo>} => {@code Foo}.
+ * </ul>
+ *
+ * @return the raw type of {@code T}
+ */
+ @SuppressWarnings("unchecked")
+ public final Class<? super T> getRawType() {
+ return (Class<? super T>) getRawType(type);
+ }
+
+ /**
+ * Compare two objects for equality.
+ *
+ * <p>A TypeReference is only equal to another TypeReference if their captured type {@code T} is
+ * also equal.
+ */
+ @Override
+ public boolean equals(Object o) {
+ // Note that this comparison could inaccurately return true when comparing types
+ // with nested type variables; therefore we ban type variables in the constructor.
+ return o instanceof TypeReference<?> && type.equals(((TypeReference<?>) o).type);
+ }
+
+ @Override
+ public int hashCode() {
+ return hash;
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder builder = new StringBuilder();
+ builder.append("TypeReference<");
+ toString(getType(), builder);
+ builder.append(">");
+
+ return builder.toString();
+ }
+
+ private static class SpecializedTypeReference<T> extends TypeReference<T> {
+ public SpecializedTypeReference(Class<T> klass) {
+ super(klass);
+ }
+ }
+}
diff --git a/camera/core/src/main/java/androidx/camera/core/UseCaseAttachState.java b/camera/core/src/main/java/androidx/camera/core/UseCaseAttachState.java
new file mode 100644
index 0000000..340b1d8
--- /dev/null
+++ b/camera/core/src/main/java/androidx/camera/core/UseCaseAttachState.java
@@ -0,0 +1,221 @@
+/*
+ * Copyright (C) 2019 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.core;
+
+import android.util.Log;
+
+import androidx.annotation.RestrictTo;
+import androidx.annotation.RestrictTo.Scope;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+
+/**
+ * Collection of use cases which are attached to a specific camera.
+ *
+ * <p>This class tracks the current state of activity for each use case. There are two states that
+ * the use case can be in: online and active. Online means the use case is currently ready for the
+ * camera capture, but not currently capturing. Active means the use case is either currently
+ * issuing a capture request or one has already been issued.
+ *
+ * @hide
+ */
+@RestrictTo(Scope.LIBRARY_GROUP)
+public final class UseCaseAttachState {
+ private static final String TAG = "UseCaseAttachState";
+ /** The name of the camera the use cases are attached to. */
+ private final String cameraId;
+ /** A map of the use cases to the corresponding state information. */
+ private final Map<BaseUseCase, UseCaseAttachInfo> attachedUseCasesToInfoMap = new HashMap<>();
+
+ /** Constructs an instance of the attach state which corresponds to the named camera. */
+ public UseCaseAttachState(String cameraId) {
+ this.cameraId = cameraId;
+ }
+
+ /**
+ * Sets the use case to an active state.
+ *
+ * <p>Adds the use case to the collection if not already in it.
+ */
+ public void setUseCaseActive(BaseUseCase useCase) {
+ UseCaseAttachInfo useCaseAttachInfo = getOrCreateUseCaseAttachInfo(useCase);
+ useCaseAttachInfo.active = true;
+ }
+
+ /**
+ * Sets the use case to an inactive state.
+ *
+ * <p>Removes the use case from the collection if also offline.
+ */
+ public void setUseCaseInactive(BaseUseCase useCase) {
+ if (!attachedUseCasesToInfoMap.containsKey(useCase)) {
+ return;
+ }
+
+ UseCaseAttachInfo useCaseAttachInfo = attachedUseCasesToInfoMap.get(useCase);
+ useCaseAttachInfo.active = false;
+ if (!useCaseAttachInfo.online) {
+ attachedUseCasesToInfoMap.remove(useCase);
+ }
+ }
+
+ /**
+ * Sets the use case to an online state.
+ *
+ * <p>Adds the use case to the collection if not already in it.
+ */
+ public void setUseCaseOnline(BaseUseCase useCase) {
+ UseCaseAttachInfo useCaseAttachInfo = getOrCreateUseCaseAttachInfo(useCase);
+ useCaseAttachInfo.online = true;
+ }
+
+ /**
+ * Sets the use case to an offline state.
+ *
+ * <p>Removes the use case from the collection if also inactive.
+ */
+ public void setUseCaseOffline(BaseUseCase useCase) {
+ if (!attachedUseCasesToInfoMap.containsKey(useCase)) {
+ return;
+ }
+ UseCaseAttachInfo useCaseAttachInfo = attachedUseCasesToInfoMap.get(useCase);
+ useCaseAttachInfo.online = false;
+ if (!useCaseAttachInfo.active) {
+ attachedUseCasesToInfoMap.remove(useCase);
+ }
+ }
+
+ public Collection<BaseUseCase> getOnlineUseCases() {
+ return Collections.unmodifiableCollection(
+ getUseCases(useCaseAttachInfo -> useCaseAttachInfo.online));
+ }
+
+ public Collection<BaseUseCase> getActiveAndOnlineUseCases() {
+ return Collections.unmodifiableCollection(
+ getUseCases(
+ useCaseAttachInfo -> useCaseAttachInfo.active && useCaseAttachInfo.online));
+ }
+
+ /**
+ * Updates the session configuration for a use case.
+ *
+ * <p>If the use case is not already in the collection, nothing is done.
+ */
+ public void updateUseCase(BaseUseCase useCase) {
+ if (!attachedUseCasesToInfoMap.containsKey(useCase)) {
+ return;
+ }
+
+ // Rebuild the attach info from scratch to get the updated SessionConfiguration.
+ UseCaseAttachInfo newUseCaseAttachInfo =
+ new UseCaseAttachInfo(useCase.getSessionConfiguration(cameraId));
+
+ // Retain the online and active flags.
+ UseCaseAttachInfo oldUseCaseAttachInfo = attachedUseCasesToInfoMap.get(useCase);
+ newUseCaseAttachInfo.online = oldUseCaseAttachInfo.online;
+ newUseCaseAttachInfo.active = oldUseCaseAttachInfo.active;
+ attachedUseCasesToInfoMap.put(useCase, newUseCaseAttachInfo);
+ }
+
+ /** Returns a session configuration builder for use cases which are both active and online. */
+ public SessionConfiguration.ValidatingBuilder getActiveAndOnlineBuilder() {
+ SessionConfiguration.ValidatingBuilder validatingBuilder =
+ new SessionConfiguration.ValidatingBuilder();
+
+ List<String> list = new ArrayList<>();
+ for (Entry<BaseUseCase, UseCaseAttachInfo> attachedUseCase :
+ attachedUseCasesToInfoMap.entrySet()) {
+ UseCaseAttachInfo useCaseAttachInfo = attachedUseCase.getValue();
+ if (useCaseAttachInfo.active && useCaseAttachInfo.online) {
+ BaseUseCase baseUseCase = attachedUseCase.getKey();
+ validatingBuilder.add(useCaseAttachInfo.sessionConfiguration);
+ list.add(baseUseCase.getName());
+ }
+ }
+ Log.d(TAG, "Active and online use case: " + list + " for camera: " + cameraId);
+ return validatingBuilder;
+ }
+
+ /** Returns a session configuration builder for use cases which are online. */
+ public SessionConfiguration.ValidatingBuilder getOnlineBuilder() {
+ SessionConfiguration.ValidatingBuilder validatingBuilder =
+ new SessionConfiguration.ValidatingBuilder();
+ List<String> list = new ArrayList<>();
+ for (Entry<BaseUseCase, UseCaseAttachInfo> attachedUseCase :
+ attachedUseCasesToInfoMap.entrySet()) {
+ UseCaseAttachInfo useCaseAttachInfo = attachedUseCase.getValue();
+ if (useCaseAttachInfo.online) {
+ validatingBuilder.add(useCaseAttachInfo.sessionConfiguration);
+ BaseUseCase baseUseCase = attachedUseCase.getKey();
+ list.add(baseUseCase.getName());
+ }
+ }
+ Log.d(TAG, "All use case: " + list + " for camera: " + cameraId);
+ return validatingBuilder;
+ }
+
+ private UseCaseAttachInfo getOrCreateUseCaseAttachInfo(BaseUseCase useCase) {
+ UseCaseAttachInfo useCaseAttachInfo = attachedUseCasesToInfoMap.get(useCase);
+ if (useCaseAttachInfo == null) {
+ useCaseAttachInfo = new UseCaseAttachInfo(useCase.getSessionConfiguration(cameraId));
+ attachedUseCasesToInfoMap.put(useCase, useCaseAttachInfo);
+ }
+ return useCaseAttachInfo;
+ }
+
+ private Collection<BaseUseCase> getUseCases(AttachStateFilter attachStateFilter) {
+ List<BaseUseCase> useCases = new ArrayList<>();
+ for (Entry<BaseUseCase, UseCaseAttachInfo> attachedUseCase :
+ attachedUseCasesToInfoMap.entrySet()) {
+ if (attachStateFilter == null || attachStateFilter.filter(attachedUseCase.getValue())) {
+ useCases.add(attachedUseCase.getKey());
+ }
+ }
+ return useCases;
+ }
+
+ private interface AttachStateFilter {
+ boolean filter(UseCaseAttachInfo attachInfo);
+ }
+
+ /** The set of state and configuration information for an attached use case. */
+ private static final class UseCaseAttachInfo {
+ /** The configurations required of the camera for the use case. */
+ final SessionConfiguration sessionConfiguration;
+ /**
+ * True if the use case is currently online (i.e. camera should have a capture session
+ * configured for it).
+ */
+ boolean online = false;
+
+ /**
+ * True if the use case is currently active (i.e. camera should be issuing capture requests
+ * for it).
+ */
+ boolean active = false;
+
+ UseCaseAttachInfo(SessionConfiguration sessionConfiguration) {
+ this.sessionConfiguration = sessionConfiguration;
+ }
+ }
+}
diff --git a/camera/core/src/main/java/androidx/camera/core/UseCaseConfiguration.java b/camera/core/src/main/java/androidx/camera/core/UseCaseConfiguration.java
new file mode 100644
index 0000000..454b1a9
--- /dev/null
+++ b/camera/core/src/main/java/androidx/camera/core/UseCaseConfiguration.java
@@ -0,0 +1,220 @@
+/*
+ * Copyright (C) 2019 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.core;
+
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.annotation.RestrictTo.Scope;
+import androidx.camera.core.SessionConfiguration.OptionUnpacker;
+
+/**
+ * Configuration containing options for use cases.
+ *
+ * @param <T> The use case being configured.
+ */
+public interface UseCaseConfiguration<T extends BaseUseCase> extends TargetConfiguration<T> {
+
+ /**
+ * Option: camerax.core.useCase.defaultSessionConfig
+ *
+ * @hide
+ */
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ Option<SessionConfiguration> OPTION_DEFAULT_SESSION_CONFIG =
+ Option.create("camerax.core.useCase.defaultSessionConfig", SessionConfiguration.class);
+ /**
+ * Option: camerax.core.useCase.configUnpacker
+ *
+ * <p>TODO(b/120949879): This may be removed when SessionConfig removes all camera2
+ * dependencies.
+ *
+ * @hide
+ */
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ Option<OptionUnpacker> OPTION_CONFIG_UNPACKER =
+ Option.create("camerax.core.useCase.configUnpacker", OptionUnpacker.class);
+ /**
+ * Option: camerax.core.useCase.surfaceOccypyPriority
+ *
+ * @hide
+ */
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ Option<Integer> OPTION_SURFACE_OCCUPANCY_PRIORITY =
+ Option.create("camerax.core.useCase.surfaceOccupancyPriority", int.class);
+
+ /**
+ * Retrieves the default session configuration for this use case.
+ *
+ * <p>This configuration is used to initialize the use case's session configuration with default
+ * values.
+ *
+ * @param valueIfMissing The value to return if this configuration option has not been set.
+ * @return The stored value or <code>valueIfMissing</code> if the value does not exist in this
+ * configuration.
+ * @hide
+ */
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ @Nullable
+ default SessionConfiguration getDefaultSessionConfiguration(
+ @Nullable SessionConfiguration valueIfMissing) {
+ return retrieveOption(OPTION_DEFAULT_SESSION_CONFIG, valueIfMissing);
+ }
+
+ /**
+ * Retrieves the default session configuration for this use case.
+ *
+ * <p>This configuration is used to initialize the use case's session configuration with default
+ * values.
+ *
+ * @return The stored value, if it exists in this configuration.
+ * @throws IllegalArgumentException if the option does not exist in this configuration.
+ * @hide
+ */
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ default SessionConfiguration getDefaultSessionConfiguration() {
+ return retrieveOption(OPTION_DEFAULT_SESSION_CONFIG);
+ }
+
+ /**
+ * Retrieves the {@link SessionConfiguration.OptionUnpacker} for this use case.
+ *
+ * <p>This unpacker is used to initialize the use case's session configuration.
+ *
+ * <p>TODO(b/120949879): This may be removed when SessionConfig removes all camera2
+ * dependencies.
+ *
+ * @param valueIfMissing The value to return if this configuration option has not been set.
+ * @return The stored value or <code>valueIfMissing</code> if the value does not exist in this
+ * configuration.
+ * @hide
+ */
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ @Nullable
+ default SessionConfiguration.OptionUnpacker getOptionUnpacker(
+ @Nullable SessionConfiguration.OptionUnpacker valueIfMissing) {
+ return retrieveOption(OPTION_CONFIG_UNPACKER, valueIfMissing);
+ }
+
+ /**
+ * Retrieves the {@link SessionConfiguration.OptionUnpacker} for this use case.
+ *
+ * <p>This unpacker is used to initialize the use case's session configuration.
+ *
+ * <p>TODO(b/120949879): This may be removed when SessionConfig removes all camera2
+ * dependencies.
+ *
+ * @return The stored value, if it exists in this configuration.
+ * @throws IllegalArgumentException if the option does not exist in this configuration.
+ * @hide
+ */
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ default SessionConfiguration.OptionUnpacker getOptionUnpacker() {
+ return retrieveOption(OPTION_CONFIG_UNPACKER);
+ }
+
+ // Option Declarations:
+ // ***********************************************************************************************
+
+ /**
+ * Retrieves the surface occupancy priority of the target intending to use from this
+ * configuration.
+ *
+ * @param valueIfMissing The value to return if this configuration option has not been set.
+ * @return The stored value or <code>valueIfMissing</code> if the value does not exist in this
+ * configuration.
+ * @hide
+ */
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ default int getSurfaceOccupancyPriority(int valueIfMissing) {
+ return retrieveOption(OPTION_SURFACE_OCCUPANCY_PRIORITY, valueIfMissing);
+ }
+
+ /**
+ * Retrieves the surface occupancy priority of the target intending to use from this
+ * configuration.
+ *
+ * @return The stored value, if it exists in this configuration.
+ * @throws IllegalArgumentException if the option does not exist in this configuration.
+ * @hide
+ */
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ default int getSurfaceOccupancyPriority() {
+ return retrieveOption(OPTION_SURFACE_OCCUPANCY_PRIORITY);
+ }
+
+ /**
+ * Builder for a {@link UseCaseConfiguration}.
+ *
+ * @param <T> The type of the object being configured.
+ * @param <C> The top level configuration which will be generated by {@link #build()}.
+ * @param <B> The top level builder type for which this builder is composed with.
+ * @hide
+ */
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ interface Builder<T, C extends Configuration, B extends Builder<T, C, B>>
+ extends TargetConfiguration.Builder<T, C, B> {
+
+ /**
+ * Sets the default session configuration for this use case.
+ *
+ * @param sessionConfig The default session configuration to use for this use case.
+ * @return the current Builder.
+ * @hide
+ */
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ default B setDefaultSessionConfiguration(SessionConfiguration sessionConfig) {
+ getMutableConfiguration().insertOption(OPTION_DEFAULT_SESSION_CONFIG, sessionConfig);
+ return builder();
+ }
+
+ /**
+ * Sets the Option Unpacker for translating this configuration into a {@link
+ * SessionConfiguration}
+ *
+ * <p>TODO(b/120949879): This may be removed when SessionConfig removes all camera2
+ * dependencies.
+ *
+ * @param optionUnpacker The option unpacker for to use for this use case.
+ * @return the current Builder.
+ * @hide
+ */
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ default B setOptionUnpacker(SessionConfiguration.OptionUnpacker optionUnpacker) {
+ getMutableConfiguration().insertOption(OPTION_CONFIG_UNPACKER, optionUnpacker);
+ return builder();
+ }
+
+ /**
+ * Sets the surface occupancy priority of the intended target from this configuration.
+ *
+ * <p>The stream resource of {@link android.hardware.camera2.CameraDevice} is limited. When
+ * one use case occupies a larger stream resource, it will impact the other use cases to get
+ * smaller stream resource. Use this to determine which use case can have higher priority to
+ * occupancy stream resource first.
+ *
+ * @param priority The priority to occupancy the available stream resource. Higher value
+ * will have higher priority.
+ * @return The current Builder.
+ * @hide
+ */
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ default B setSurfaceOccupancyPriority(int priority) {
+ getMutableConfiguration().insertOption(OPTION_SURFACE_OCCUPANCY_PRIORITY, priority);
+ return builder();
+ }
+ }
+}
diff --git a/camera/core/src/main/java/androidx/camera/core/UseCaseConfigurationFactory.java b/camera/core/src/main/java/androidx/camera/core/UseCaseConfigurationFactory.java
new file mode 100644
index 0000000..c9e5e58
--- /dev/null
+++ b/camera/core/src/main/java/androidx/camera/core/UseCaseConfigurationFactory.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2019 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.core;
+
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.annotation.RestrictTo.Scope;
+
+/**
+ * A Repository for generating use case configurations.
+ *
+ * @hide
+ */
+@RestrictTo(Scope.LIBRARY_GROUP)
+public interface UseCaseConfigurationFactory {
+
+ /**
+ * Returns the configuration for the given type, or <code>null</code> if the configuration
+ * cannot be produced.
+ */
+ @Nullable
+ <C extends UseCaseConfiguration<?>> C getConfiguration(Class<C> configType);
+}
diff --git a/camera/core/src/main/java/androidx/camera/core/UseCaseGroup.java b/camera/core/src/main/java/androidx/camera/core/UseCaseGroup.java
new file mode 100644
index 0000000..eef55e8
--- /dev/null
+++ b/camera/core/src/main/java/androidx/camera/core/UseCaseGroup.java
@@ -0,0 +1,175 @@
+/*
+ * Copyright (C) 2019 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.core;
+
+import android.util.Log;
+
+import androidx.annotation.GuardedBy;
+import androidx.annotation.RestrictTo;
+import androidx.annotation.RestrictTo.Scope;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * A collection of {@link BaseUseCase}.
+ *
+ * <p>The group of {@link BaseUseCase} instances have synchronized interactions with the {@link
+ * BaseCamera}.
+ *
+ * @hide
+ */
+@RestrictTo(Scope.LIBRARY_GROUP)
+public final class UseCaseGroup {
+ private static final String TAG = "UseCaseGroup";
+
+ /**
+ * The lock for the single {@link StateChangeListener} held by the group.
+ *
+ * <p>This lock is always acquired prior to acquiring the useCasesLock so that there is no
+ * lock-ordering deadlock.
+ */
+ private final Object listenerLock = new Object();
+ /**
+ * The lock for accessing the map of use case types to use case instances.
+ *
+ * <p>This lock is always acquired after acquiring the listenerLock so that there is no
+ * lock-ordering deadlock.
+ */
+ private final Object useCasesLock = new Object();
+ @GuardedBy("useCasesLock")
+ private final Set<BaseUseCase> useCases = new HashSet<>();
+ @GuardedBy("listenerLock")
+ private StateChangeListener listener;
+
+ /** Starts all the use cases so that they are brought into an online state. */
+ void start() {
+ synchronized (listenerLock) {
+ if (listener != null) {
+ listener.onGroupActive(this);
+ }
+ }
+ }
+
+ /** Stops all the use cases so that they are brought into an offline state. */
+ void stop() {
+ synchronized (listenerLock) {
+ if (listener != null) {
+ listener.onGroupInactive(this);
+ }
+ }
+ }
+
+ void setListener(StateChangeListener listener) {
+ synchronized (listenerLock) {
+ this.listener = listener;
+ }
+ }
+
+ /**
+ * Adds the {@link BaseUseCase} to the group.
+ *
+ * @return true if the use case is added, or false if the use case already exists in the group.
+ */
+ public boolean addUseCase(BaseUseCase useCase) {
+ synchronized (useCasesLock) {
+ return useCases.add(useCase);
+ }
+ }
+
+ /** Returns true if the {@link BaseUseCase} is contained in the group. */
+ boolean contains(BaseUseCase useCase) {
+ synchronized (useCasesLock) {
+ return useCases.contains(useCase);
+ }
+ }
+
+ /**
+ * Removes the {@link BaseUseCase} from the group.
+ *
+ * @return Returns true if the use case is removed. Otherwise returns false (if the use case did
+ * not exist in the group).
+ */
+ boolean removeUseCase(BaseUseCase useCase) {
+ synchronized (useCasesLock) {
+ return useCases.remove(useCase);
+ }
+ }
+
+ /** Clears all use cases from this group. */
+ public void clear() {
+ List<BaseUseCase> useCasesToClear = new ArrayList<>();
+ synchronized (useCasesLock) {
+ useCasesToClear.addAll(useCases);
+ useCases.clear();
+ }
+ for (BaseUseCase useCase : useCasesToClear) {
+ Log.d(TAG, "Clearing use case: " + useCase.getName());
+ useCase.clear();
+ }
+ }
+
+ /** Returns the collection of all the use cases currently contained by the UseCaseGroup. */
+ Collection<BaseUseCase> getUseCases() {
+ synchronized (useCasesLock) {
+ return Collections.unmodifiableCollection(useCases);
+ }
+ }
+
+ Map<String, Set<BaseUseCase>> getCameraIdToUseCaseMap() {
+ Map<String, Set<BaseUseCase>> cameraIdToUseCases = new HashMap<>();
+ synchronized (useCasesLock) {
+ for (BaseUseCase useCase : useCases) {
+ for (String cameraId : useCase.getAttachedCameraIds()) {
+ Set<BaseUseCase> useCaseSet = cameraIdToUseCases.get(cameraId);
+ if (useCaseSet == null) {
+ useCaseSet = new HashSet<>();
+ }
+ useCaseSet.add(useCase);
+ cameraIdToUseCases.put(cameraId, useCaseSet);
+ }
+ }
+ }
+ return Collections.unmodifiableMap(cameraIdToUseCases);
+ }
+
+ /** Listener called when a {@link UseCaseGroup} transitions between active/inactive states. */
+ interface StateChangeListener {
+ /**
+ * Called when a {@link UseCaseGroup} becomes active.
+ *
+ * <p>When a UseCaseGroup is active then all the contained {@link BaseUseCase} become
+ * online. This means that the {@link BaseCamera} should transition to a state as close as
+ * possible to producing, but prior to actually producing data for the use case.
+ */
+ void onGroupActive(UseCaseGroup useCaseGroup);
+
+ /**
+ * Called when a {@link UseCaseGroup} becomes inactive.
+ *
+ * <p>When a UseCaseGroup is active then all the contained {@link BaseUseCase} become
+ * offline.
+ */
+ void onGroupInactive(UseCaseGroup useCaseGroup);
+ }
+}
diff --git a/camera/core/src/main/java/androidx/camera/core/UseCaseGroupLifecycleController.java b/camera/core/src/main/java/androidx/camera/core/UseCaseGroupLifecycleController.java
new file mode 100644
index 0000000..3a83213
--- /dev/null
+++ b/camera/core/src/main/java/androidx/camera/core/UseCaseGroupLifecycleController.java
@@ -0,0 +1,105 @@
+/*
+ * Copyright (C) 2019 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.core;
+
+import androidx.annotation.GuardedBy;
+import androidx.lifecycle.DefaultLifecycleObserver;
+import androidx.lifecycle.Lifecycle;
+import androidx.lifecycle.Lifecycle.State;
+import androidx.lifecycle.LifecycleOwner;
+
+/** A {@link UseCaseGroup} whose starting and stopping is controlled by a {@link Lifecycle}. */
+final class UseCaseGroupLifecycleController implements DefaultLifecycleObserver {
+ private final Object useCaseGroupLock = new Object();
+
+ @GuardedBy("useCaseGroupLock")
+ private final UseCaseGroup useCaseGroup;
+
+ /** The lifecycle that controls the useCaseGroup. */
+ private final Lifecycle lifecycle;
+
+ /** Creates a new {@link UseCaseGroup} which gets controlled by lifecycle transitions. */
+ UseCaseGroupLifecycleController(Lifecycle lifecycle) {
+ this(lifecycle, new UseCaseGroup());
+ }
+
+ /** Wraps an existing {@link UseCaseGroup} so it is controlled by lifecycle transitions. */
+ UseCaseGroupLifecycleController(Lifecycle lifecycle, UseCaseGroup useCaseGroup) {
+ this.useCaseGroup = useCaseGroup;
+ this.lifecycle = lifecycle;
+ lifecycle.addObserver(this);
+ }
+
+ @Override
+ public void onStart(LifecycleOwner lifecycleOwner) {
+ synchronized (useCaseGroupLock) {
+ useCaseGroup.start();
+ }
+ }
+
+ @Override
+ public void onStop(LifecycleOwner lifecycleOwner) {
+ synchronized (useCaseGroupLock) {
+ useCaseGroup.stop();
+ }
+ }
+
+ @Override
+ public void onDestroy(LifecycleOwner lifecycleOwner) {
+ synchronized (useCaseGroupLock) {
+ useCaseGroup.clear();
+ }
+ }
+
+ /**
+ * Starts the underlying {@link UseCaseGroup} so that its {@link
+ * UseCaseGroup.StateChangeListener} can be notified.
+ *
+ * <p>This is required when the contained {@link Lifecycle} is in a STARTED state, since the
+ * default state for a {@link UseCaseGroup} is inactive. The explicit call forces a check on the
+ * actual state of the group.
+ */
+ void notifyState() {
+ synchronized (useCaseGroupLock) {
+ if (lifecycle.getCurrentState().isAtLeast(State.STARTED)) {
+ useCaseGroup.start();
+ }
+ for (BaseUseCase useCase : useCaseGroup.getUseCases()) {
+ useCase.notifyState();
+ }
+ }
+ }
+
+ UseCaseGroup getUseCaseGroup() {
+ synchronized (useCaseGroupLock) {
+ return useCaseGroup;
+ }
+ }
+
+ /**
+ * Stops observing lifecycle changes.
+ *
+ * <p>Once released the wrapped {@link UseCaseGroup} is still valid, but will no longer be
+ * triggered by lifecycle state transitions. In order to observe lifecycle changes again a new
+ * {@link UseCaseGroupLifecycleController} instance should be created.
+ *
+ * <p>Calls subsequent to the first time will do nothing.
+ */
+ void release() {
+ lifecycle.removeObserver(this);
+ }
+}
diff --git a/camera/core/src/main/java/androidx/camera/core/UseCaseGroupRepository.java b/camera/core/src/main/java/androidx/camera/core/UseCaseGroupRepository.java
new file mode 100644
index 0000000..c945fff
--- /dev/null
+++ b/camera/core/src/main/java/androidx/camera/core/UseCaseGroupRepository.java
@@ -0,0 +1,149 @@
+/*
+ * Copyright (C) 2019 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.core;
+
+import androidx.annotation.GuardedBy;
+import androidx.annotation.VisibleForTesting;
+import androidx.lifecycle.DefaultLifecycleObserver;
+import androidx.lifecycle.Lifecycle.State;
+import androidx.lifecycle.LifecycleOwner;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * A repository of {@link UseCaseGroupLifecycleController} instances.
+ *
+ * <p>Each {@link UseCaseGroupLifecycleController} is associated with a {@link LifecycleOwner} that
+ * regulates the common lifecycle shared by all the use cases in the group.
+ */
+final class UseCaseGroupRepository {
+ final Object useCasesLock = new Object();
+
+ @GuardedBy("useCasesLock")
+ final Map<LifecycleOwner, UseCaseGroupLifecycleController> useCasesMap =
+ new HashMap<>();
+
+ /**
+ * Gets an existing {@link UseCaseGroupLifecycleController} associated with the given {@link
+ * LifecycleOwner}, or creates a new {@link UseCaseGroupLifecycleController} if a group does not
+ * already exist.
+ *
+ * <p>The {@link UseCaseGroupLifecycleController} is set to be an observer of the {@link
+ * LifecycleOwner}.
+ *
+ * @param lifecycleOwner to associate with the group
+ */
+ UseCaseGroupLifecycleController getOrCreateUseCaseGroup(LifecycleOwner lifecycleOwner) {
+ return getOrCreateUseCaseGroup(lifecycleOwner, useCaseGroup -> {
+ });
+ }
+
+ /**
+ * Gets an existing {@link UseCaseGroupLifecycleController} associated with the given {@link
+ * LifecycleOwner}, or creates a new {@link UseCaseGroupLifecycleController} if a group does not
+ * already exist.
+ *
+ * <p>The {@link UseCaseGroupLifecycleController} is set to be an observer of the {@link
+ * LifecycleOwner}.
+ *
+ * @param lifecycleOwner to associate with the group
+ * @param groupSetup additional setup to do on the group if a new instance is created
+ */
+ UseCaseGroupLifecycleController getOrCreateUseCaseGroup(
+ LifecycleOwner lifecycleOwner, UseCaseGroupSetup groupSetup) {
+ UseCaseGroupLifecycleController useCaseGroupLifecycleController;
+ synchronized (useCasesLock) {
+ useCaseGroupLifecycleController = useCasesMap.get(lifecycleOwner);
+ if (useCaseGroupLifecycleController == null) {
+ useCaseGroupLifecycleController = createUseCaseGroup(lifecycleOwner);
+ groupSetup.setup(useCaseGroupLifecycleController.getUseCaseGroup());
+ }
+ }
+ return useCaseGroupLifecycleController;
+ }
+
+ /**
+ * Creates a new {@link UseCaseGroupLifecycleController} associated with the given {@link
+ * LifecycleOwner} and adds the group to the repository.
+ *
+ * <p>The {@link UseCaseGroupLifecycleController} is set to be an observer of the {@link
+ * LifecycleOwner}.
+ *
+ * @param lifecycleOwner to associate with the group
+ * @return a new {@link UseCaseGroupLifecycleController}
+ * @throws IllegalArgumentException if the {@link android.arch.lifecycle.Lifecycle} of
+ * lifecycleOwner is already
+ * {@link android.arch.lifecycle.Lifecycle.State.DESTROYED}.
+ */
+ private UseCaseGroupLifecycleController createUseCaseGroup(LifecycleOwner lifecycleOwner) {
+ if (lifecycleOwner.getLifecycle().getCurrentState() == State.DESTROYED) {
+ throw new IllegalArgumentException(
+ "Trying to create use case group with destroyed lifecycle.");
+ }
+
+ UseCaseGroupLifecycleController useCaseGroupLifecycleController =
+ new UseCaseGroupLifecycleController(lifecycleOwner.getLifecycle());
+ lifecycleOwner.getLifecycle().addObserver(createRemoveOnDestroyObserver());
+ synchronized (useCasesLock) {
+ useCasesMap.put(lifecycleOwner, useCaseGroupLifecycleController);
+ }
+ return useCaseGroupLifecycleController;
+ }
+
+ /**
+ * Creates a {@link DefaultLifecycleObserver} which removes any {@link
+ * UseCaseGroupLifecycleController} associated with a {@link LifecycleOwner} from this
+ * repository when that lifecycle is destroyed.
+ *
+ * @return a new {@link DefaultLifecycleObserver}
+ */
+ private DefaultLifecycleObserver createRemoveOnDestroyObserver() {
+ return new DefaultLifecycleObserver() {
+ @Override
+ public void onDestroy(LifecycleOwner lifecycleOwner) {
+ synchronized (useCasesLock) {
+ useCasesMap.remove(lifecycleOwner);
+ }
+ lifecycleOwner.getLifecycle().removeObserver(this);
+ }
+ };
+ }
+
+ Collection<UseCaseGroupLifecycleController> getUseCaseGroups() {
+ synchronized (useCasesLock) {
+ return Collections.unmodifiableCollection(useCasesMap.values());
+ }
+ }
+
+ @VisibleForTesting
+ Map<LifecycleOwner, UseCaseGroupLifecycleController> getUseCasesMap() {
+ synchronized (useCasesLock) {
+ return useCasesMap;
+ }
+ }
+
+ /**
+ * The interface for doing additional setup work on a newly created {@link UseCaseGroup}
+ * instance.
+ */
+ public interface UseCaseGroupSetup {
+ void setup(UseCaseGroup useCaseGroup);
+ }
+}
diff --git a/camera/core/src/main/java/androidx/camera/core/VideoCaptureUseCase.java b/camera/core/src/main/java/androidx/camera/core/VideoCaptureUseCase.java
new file mode 100644
index 0000000..7f39ce4
--- /dev/null
+++ b/camera/core/src/main/java/androidx/camera/core/VideoCaptureUseCase.java
@@ -0,0 +1,900 @@
+/*
+ * Copyright (C) 2019 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.core;
+
+import android.location.Location;
+import android.media.AudioFormat;
+import android.media.AudioRecord;
+import android.media.CamcorderProfile;
+import android.media.MediaCodec;
+import android.media.MediaCodec.BufferInfo;
+import android.media.MediaCodecInfo;
+import android.media.MediaCodecInfo.CodecCapabilities;
+import android.media.MediaFormat;
+import android.media.MediaMuxer;
+import android.media.MediaRecorder.AudioSource;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Looper;
+import android.util.Log;
+import android.util.Rational;
+import android.util.Size;
+import android.view.Display;
+import android.view.Surface;
+
+import androidx.annotation.GuardedBy;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.annotation.RestrictTo.Scope;
+import androidx.camera.core.CameraX.LensFacing;
+import androidx.camera.core.ImageOutputConfiguration.RotationValue;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+/**
+ * A use case for taking a video.
+ *
+ * <p>This class is designed for simple video capturing. It gives basic configuration of the
+ * recorded video such as resolution and file format.
+ */
+public class VideoCaptureUseCase extends BaseUseCase {
+
+ /**
+ * Provides a static configuration with implementation-agnostic options.
+ *
+ * @hide
+ */
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ public static final Defaults DEFAULT_CONFIG = new Defaults();
+ private static final Metadata EMPTY_METADATA = new Metadata();
+ private static final String TAG = "VideoCaptureUseCase";
+ /** Amount of time to wait for dequeuing a buffer from the videoEncoder. */
+ private static final int DEQUE_TIMEOUT_USEC = 10000;
+ /** Android preferred mime type for AVC video. */
+ private static final String VIDEO_MIME_TYPE = "video/avc";
+ private static final String AUDIO_MIME_TYPE = "audio/mp4a-latm";
+ /** Camcorder profiles quality list */
+ private static final int[] CamcorderQuality = {
+ CamcorderProfile.QUALITY_2160P,
+ CamcorderProfile.QUALITY_1080P,
+ CamcorderProfile.QUALITY_720P,
+ CamcorderProfile.QUALITY_480P
+ };
+ /**
+ * Audio encoding
+ *
+ * <p>the result of PCM_8BIT and PCM_FLOAT are not good. Set PCM_16BIT as the first option.
+ */
+ private static final short[] audioEncoding = {
+ AudioFormat.ENCODING_PCM_16BIT,
+ AudioFormat.ENCODING_PCM_8BIT,
+ AudioFormat.ENCODING_PCM_FLOAT
+ };
+ private final BufferInfo videoBufferInfo = new BufferInfo();
+ private final Object muxerLock = new Object();
+ /** Thread on which all encoding occurs. */
+ private final HandlerThread videoHandlerThread =
+ new HandlerThread(CameraXThreads.TAG + "video encoding thread");
+ private final Handler videoHandler;
+ /** Thread on which audio encoding occurs. */
+ private final HandlerThread audioHandlerThread =
+ new HandlerThread(CameraXThreads.TAG + "audio encoding thread");
+ private final Handler audioHandler;
+ private final AtomicBoolean endOfVideoStreamSignal = new AtomicBoolean(true);
+ private final AtomicBoolean endOfAudioStreamSignal = new AtomicBoolean(true);
+ private final AtomicBoolean endOfAudioVideoSignal = new AtomicBoolean(true);
+ private final BufferInfo audioBufferInfo = new BufferInfo();
+ /** For record the first sample written time. */
+ private final AtomicBoolean isFirstVideoSampleWrite = new AtomicBoolean(false);
+ private final AtomicBoolean isFirstAudioSampleWrite = new AtomicBoolean(false);
+ private final VideoCaptureUseCaseConfiguration.Builder useCaseConfigBuilder;
+ @NonNull
+ private MediaCodec videoEncoder;
+ @NonNull
+ private MediaCodec audioEncoder;
+ /** The muxer that writes the encoding data to file. */
+ @GuardedBy("muxerLock")
+ private MediaMuxer muxer;
+ private boolean muxerStarted = false;
+ /** The index of the video track used by the muxer. */
+ private int videoTrackIndex;
+ /** The index of the audio track used by the muxer. */
+ private int audioTrackIndex;
+ /** Surface the camera writes to, which the videoEncoder uses as input. */
+ private Surface cameraSurface;
+ /** audio raw data */
+ @NonNull
+ private AudioRecord audioRecorder;
+ private int audioBufferSize;
+ private boolean isRecording = false;
+ private int audioChannelCount;
+ private int audioSampleRate;
+ private int audioBitRate;
+
+ /**
+ * Creates a new video capture use case from the given configuration.
+ *
+ * @param configuration for this use case instance
+ */
+ public VideoCaptureUseCase(VideoCaptureUseCaseConfiguration configuration) {
+ super(configuration);
+ useCaseConfigBuilder = VideoCaptureUseCaseConfiguration.Builder.fromConfig(configuration);
+
+ // video thread start
+ videoHandlerThread.start();
+ videoHandler = new Handler(videoHandlerThread.getLooper());
+
+ // audio thread start
+ audioHandlerThread.start();
+ audioHandler = new Handler(audioHandlerThread.getLooper());
+ }
+
+ /** Creates a {@link MediaFormat} using parameters from the configuration */
+ private static MediaFormat createMediaFormat(
+ VideoCaptureUseCaseConfiguration configuration, Size resolution) {
+ MediaFormat format =
+ MediaFormat.createVideoFormat(
+ VIDEO_MIME_TYPE, resolution.getWidth(), resolution.getHeight());
+ format.setInteger(MediaFormat.KEY_COLOR_FORMAT, CodecCapabilities.COLOR_FormatSurface);
+ format.setInteger(MediaFormat.KEY_BIT_RATE, configuration.getBitRate());
+ format.setInteger(MediaFormat.KEY_FRAME_RATE, configuration.getVideoFrameRate());
+ format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, configuration.getIFrameInterval());
+
+ return format;
+ }
+
+ private static final String getCameraIdUnchecked(LensFacing lensFacing) {
+ try {
+ return CameraX.getCameraWithLensFacing(lensFacing);
+ } catch (Exception e) {
+ throw new IllegalArgumentException(
+ "Unable to get camera id for camera lens facing " + lensFacing, e);
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * @hide
+ */
+ @Override
+ @Nullable
+ protected UseCaseConfiguration.Builder<?, ?, ?> getDefaultBuilder() {
+ VideoCaptureUseCaseConfiguration defaults =
+ CameraX.getDefaultUseCaseConfiguration(VideoCaptureUseCaseConfiguration.class);
+ if (defaults != null) {
+ return VideoCaptureUseCaseConfiguration.Builder.fromConfig(defaults);
+ }
+
+ return null;
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * @hide
+ */
+ @Override
+ protected Map<String, Size> onSuggestedResolutionUpdated(
+ Map<String, Size> suggestedResolutionMap) {
+ VideoCaptureUseCaseConfiguration configuration =
+ (VideoCaptureUseCaseConfiguration) getUseCaseConfiguration();
+ if (cameraSurface != null) {
+ videoEncoder.stop();
+ videoEncoder.release();
+ audioEncoder.stop();
+ audioEncoder.release();
+ cameraSurface.release();
+ }
+
+ try {
+ videoEncoder = MediaCodec.createEncoderByType(VIDEO_MIME_TYPE);
+ audioEncoder = MediaCodec.createEncoderByType(AUDIO_MIME_TYPE);
+ } catch (IOException e) {
+ throw new IllegalStateException("Unable to create MediaCodec due to: " + e.getCause());
+ }
+
+ String cameraId = getCameraIdUnchecked(configuration.getLensFacing());
+ Size resolution = suggestedResolutionMap.get(cameraId);
+ if (resolution == null) {
+ throw new IllegalArgumentException(
+ "Suggested resolution map missing resolution for camera " + cameraId);
+ }
+
+ setupEncoder(resolution);
+ return suggestedResolutionMap;
+ }
+
+ /**
+ * Starts recording video, which continues until {@link VideoCaptureUseCase#stopRecording()} is
+ * called.
+ *
+ * <p>StartRecording() is asynchronous. User needs to check if any error occurs by setting the
+ * {@link OnVideoSavedListener#onError(UseCaseError, String, Throwable)}.
+ *
+ * @param saveLocation Location to save the video capture
+ * @param listener Listener to call for the recorded video
+ */
+ public void startRecording(File saveLocation, OnVideoSavedListener listener) {
+ isFirstVideoSampleWrite.set(false);
+ isFirstAudioSampleWrite.set(false);
+ startRecording(saveLocation, listener, EMPTY_METADATA);
+ }
+
+ /**
+ * Starts recording video, which continues until {@link VideoCaptureUseCase#stopRecording()} is
+ * called.
+ *
+ * <p>StartRecording() is asynchronous. User needs to check if any error occurs by setting the
+ * {@link OnVideoSavedListener#onError(UseCaseError, String, Throwable)}.
+ *
+ * @param saveLocation Location to save the video capture
+ * @param listener Listener to call for the recorded video
+ * @param metadata Metadata to save with the recorded video
+ */
+ public void startRecording(
+ File saveLocation, OnVideoSavedListener listener, Metadata metadata) {
+ Log.i(TAG, "startRecording");
+
+ if (!endOfAudioVideoSignal.get()) {
+ listener.onError(
+ UseCaseError.RECORDING_IN_PROGRESS, "It is still in video recording!", null);
+ return;
+ }
+
+ try {
+ // audioRecord start
+ audioRecorder.startRecording();
+ } catch (IllegalStateException e) {
+ listener.onError(UseCaseError.ENCODER_ERROR, "AudioRecorder start fail", e);
+ return;
+ }
+
+ String cameraId =
+ getCameraIdUnchecked(
+ ((CameraDeviceConfiguration) getUseCaseConfiguration()).getLensFacing());
+ try {
+ // video encoder start
+ Log.i(TAG, "videoEncoder start");
+ videoEncoder.start();
+ // audio encoder start
+ Log.i(TAG, "audioEncoder start");
+ audioEncoder.start();
+
+ } catch (IllegalStateException e) {
+ setupEncoder(getAttachedSurfaceResolution(cameraId));
+ listener.onError(UseCaseError.ENCODER_ERROR, "Audio/Video encoder start fail", e);
+ return;
+ }
+
+ // Get the relative rotation or default to 0 if the camera info is unavailable
+ int relativeRotation = 0;
+ try {
+ CameraInfo cameraInfo = CameraX.getCameraInfo(cameraId);
+ relativeRotation =
+ cameraInfo.getSensorRotationDegrees(
+ ((ImageOutputConfiguration) getUseCaseConfiguration())
+ .getTargetRotation(Surface.ROTATION_0));
+ } catch (CameraInfoUnavailableException e) {
+ Log.e(TAG, "Unable to retrieve camera sensor orientation.", e);
+ }
+
+ try {
+ synchronized (muxerLock) {
+ muxer =
+ new MediaMuxer(
+ saveLocation.getAbsolutePath(),
+ MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);
+
+ muxer.setOrientationHint(relativeRotation);
+ if (metadata.location != null) {
+ muxer.setLocation(
+ (float) metadata.location.getLatitude(),
+ (float) metadata.location.getLongitude());
+ }
+ }
+ } catch (IOException e) {
+ setupEncoder(getAttachedSurfaceResolution(cameraId));
+ listener.onError(UseCaseError.MUXER_ERROR, "MediaMuxer creation failed!", e);
+ return;
+ }
+
+ endOfVideoStreamSignal.set(false);
+ endOfAudioStreamSignal.set(false);
+ endOfAudioVideoSignal.set(false);
+ isRecording = true;
+
+ notifyActive();
+ audioHandler.post(
+ () -> {
+ audioEncode(listener);
+ });
+
+ videoHandler.post(
+ () -> {
+ boolean errorOccurred = videoEncode(listener);
+ if (!errorOccurred) {
+ listener.onVideoSaved(saveLocation);
+ }
+ });
+ }
+
+ /**
+ * Stops recording video, this must be called after {@link
+ * VideoCaptureUseCase#startRecording(File, OnVideoSavedListener, Metadata)} is called.
+ *
+ * <p>stopRecording() is asynchronous API. User need to check if {@link
+ * OnVideoSavedListener#onVideoSaved(File)} or {@link OnVideoSavedListener#onError(UseCaseError,
+ * String, Throwable)} be called before startRecording.
+ */
+ public void stopRecording() {
+ Log.i(TAG, "stopRecording");
+ notifyInactive();
+ if (!endOfAudioVideoSignal.get() && isRecording) {
+ // stop audio encoder thread, and wait video encoder and muxer stop.
+ endOfAudioStreamSignal.set(true);
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * @hide
+ */
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ @Override
+ public void clear() {
+ videoHandlerThread.quitSafely();
+
+ if (videoEncoder != null) {
+ videoEncoder.release();
+ videoEncoder = null;
+ }
+
+ // audio encoder release
+ audioHandlerThread.quitSafely();
+ if (audioEncoder != null) {
+ audioEncoder.release();
+ audioEncoder = null;
+ }
+
+ if (audioRecorder != null) {
+ audioRecorder.release();
+ audioRecorder = null;
+ }
+
+ if (cameraSurface != null) {
+ cameraSurface.release();
+ cameraSurface = null;
+ }
+ super.clear();
+ }
+
+ /**
+ * Sets the desired rotation of the output video.
+ *
+ * <p>In most cases this should be set to the current rotation returned by {@link
+ * Display#getRotation()}.
+ *
+ * @param rotation Desired rotation of the output video.
+ */
+ public void setTargetRotation(@RotationValue int rotation) {
+ ImageOutputConfiguration oldconfig = (ImageOutputConfiguration) getUseCaseConfiguration();
+ int oldRotation = oldconfig.getTargetRotation(ImageOutputConfiguration.INVALID_ROTATION);
+ if (oldRotation == ImageOutputConfiguration.INVALID_ROTATION || oldRotation != rotation) {
+ useCaseConfigBuilder.setTargetRotation(rotation);
+ updateUseCaseConfiguration(useCaseConfigBuilder.build());
+
+ // TODO(b/122846516): Update session configuration and possibly reconfigure session.
+ }
+ }
+
+ /**
+ * Setup the {@link MediaCodec} for encoding video from a camera {@link Surface} and encoding
+ * audio from selected audio source.
+ */
+ private void setupEncoder(Size resolution) {
+ VideoCaptureUseCaseConfiguration configuration =
+ (VideoCaptureUseCaseConfiguration) getUseCaseConfiguration();
+
+ // video encoder setup
+ videoEncoder.reset();
+ videoEncoder.configure(
+ createMediaFormat(configuration, resolution), /*surface*/
+ null, /*crypto*/
+ null,
+ MediaCodec.CONFIGURE_FLAG_ENCODE);
+ if (cameraSurface != null) {
+ cameraSurface.release();
+ }
+ cameraSurface = videoEncoder.createInputSurface();
+
+ SessionConfiguration.Builder builder =
+ SessionConfiguration.Builder.createFrom(configuration);
+ builder.addSurface(new ImmediateSurface(cameraSurface));
+
+ String cameraId = getCameraIdUnchecked(configuration.getLensFacing());
+ attachToCamera(cameraId, builder.build());
+
+ // audio encoder setup
+ setAudioParametersByCamcorderProfile(resolution, cameraId);
+ audioEncoder.reset();
+ audioEncoder.configure(
+ createAudioMediaFormat(), null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
+
+ if (audioRecorder != null) {
+ audioRecorder.release();
+ }
+ audioRecorder = autoConfigAudioRecordSource(configuration);
+ // check audioRecorder
+ if (audioRecorder == null) {
+ Log.e(TAG, "AudioRecord object cannot initialized correctly!");
+ }
+
+ videoTrackIndex = -1;
+ audioTrackIndex = -1;
+ isRecording = false;
+ }
+
+ /**
+ * Write a buffer that has been encoded to file.
+ *
+ * @param bufferIndex the index of the buffer in the videoEncoder that has available data
+ * @return returns true if this buffer is the end of the stream
+ */
+ private boolean writeVideoEncodedBuffer(int bufferIndex) {
+ if (bufferIndex < 0) {
+ Log.e(TAG, "Output buffer should not have negative index: " + bufferIndex);
+ return false;
+ }
+ // Get data from buffer
+ ByteBuffer outputBuffer = videoEncoder.getOutputBuffer(bufferIndex);
+
+ // Check if buffer is valid, if not then return
+ if (outputBuffer == null) {
+ Log.d(TAG, "OutputBuffer was null.");
+ return false;
+ }
+
+ // Write data to muxer if available
+ if (audioTrackIndex >= 0 && videoTrackIndex >= 0 && videoBufferInfo.size > 0) {
+ outputBuffer.position(videoBufferInfo.offset);
+ outputBuffer.limit(videoBufferInfo.offset + videoBufferInfo.size);
+ videoBufferInfo.presentationTimeUs = (System.nanoTime() / 1000);
+
+ synchronized (muxerLock) {
+ if (!isFirstVideoSampleWrite.get()) {
+ Log.i(TAG, "First video sample written.");
+ isFirstVideoSampleWrite.set(true);
+ }
+ muxer.writeSampleData(videoTrackIndex, outputBuffer, videoBufferInfo);
+ }
+ }
+
+ // Release data
+ videoEncoder.releaseOutputBuffer(bufferIndex, false);
+
+ // Return true if EOS is set
+ return (videoBufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0;
+ }
+
+ private boolean writeAudioEncodedBuffer(int bufferIndex) {
+ ByteBuffer buffer = getOutputBuffer(audioEncoder, bufferIndex);
+ buffer.position(audioBufferInfo.offset);
+ if (audioTrackIndex >= 0
+ && videoTrackIndex >= 0
+ && audioBufferInfo.size > 0
+ && audioBufferInfo.presentationTimeUs > 0) {
+ try {
+ synchronized (muxerLock) {
+ if (!isFirstAudioSampleWrite.get()) {
+ Log.i(TAG, "First audio sample written.");
+ isFirstAudioSampleWrite.set(true);
+ }
+ muxer.writeSampleData(audioTrackIndex, buffer, audioBufferInfo);
+ }
+ } catch (Exception e) {
+ Log.e(
+ TAG,
+ "audio error:size="
+ + audioBufferInfo.size
+ + "/offset="
+ + audioBufferInfo.offset
+ + "/timeUs="
+ + audioBufferInfo.presentationTimeUs);
+ e.printStackTrace();
+ }
+ }
+ audioEncoder.releaseOutputBuffer(bufferIndex, false);
+ return (audioBufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0;
+ }
+
+ /**
+ * Encoding which runs indefinitely until end of stream is signaled. This should not run on the
+ * main thread otherwise it will cause the application to block.
+ *
+ * @return returns {@code true} if an error condition occurred, otherwise returns {@code false}
+ */
+ private boolean videoEncode(OnVideoSavedListener videoSavedListener) {
+ VideoCaptureUseCaseConfiguration configuration =
+ (VideoCaptureUseCaseConfiguration) getUseCaseConfiguration();
+ // Main encoding loop. Exits on end of stream.
+ boolean errorOccurred = false;
+ boolean videoEos = false;
+ while (!videoEos && !errorOccurred) {
+ // Check for end of stream from main thread
+ if (endOfVideoStreamSignal.get()) {
+ videoEncoder.signalEndOfInputStream();
+ endOfVideoStreamSignal.set(false);
+ }
+
+ // Deque buffer to check for processing step
+ int outputBufferId =
+ videoEncoder.dequeueOutputBuffer(videoBufferInfo, DEQUE_TIMEOUT_USEC);
+ switch (outputBufferId) {
+ case MediaCodec.INFO_OUTPUT_FORMAT_CHANGED:
+ if (muxerStarted) {
+ videoSavedListener.onError(
+ UseCaseError.ENCODER_ERROR,
+ "Unexpected change in video encoding format.",
+ null);
+ errorOccurred = true;
+ }
+
+ synchronized (muxerLock) {
+ videoTrackIndex = muxer.addTrack(videoEncoder.getOutputFormat());
+ if (audioTrackIndex >= 0 && videoTrackIndex >= 0) {
+ muxerStarted = true;
+ Log.i(TAG, "media muxer start");
+ muxer.start();
+ }
+ }
+ break;
+ case MediaCodec.INFO_TRY_AGAIN_LATER:
+ // Timed out. Just wait until next attempt to deque.
+ case MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED:
+ // Ignore output buffers changed since we dequeue a single buffer instead of
+ // multiple
+ break;
+ default:
+ videoEos = writeVideoEncodedBuffer(outputBufferId);
+ }
+ }
+
+ try {
+ Log.i(TAG, "videoEncoder stop");
+ videoEncoder.stop();
+ } catch (IllegalStateException e) {
+ videoSavedListener.onError(UseCaseError.ENCODER_ERROR, "Video encoder stop failed!", e);
+ errorOccurred = true;
+ }
+
+ try {
+ // new MediaMuxer instance required for each new file written, and release current one.
+ synchronized (muxerLock) {
+ if (muxer != null) {
+ if (muxerStarted) {
+ muxer.stop();
+ }
+ muxer.release();
+ muxer = null;
+ }
+ }
+ } catch (IllegalStateException e) {
+ videoSavedListener.onError(UseCaseError.MUXER_ERROR, "Muxer stop failed!", e);
+ errorOccurred = true;
+ }
+
+ muxerStarted = false;
+ // Do the setup of the videoEncoder at the end of video recording instead of at the start of
+ // recording because it requires attaching a new Surface. This causes a glitch so we don't
+ // want
+ // that to incur latency at the start of capture.
+ setupEncoder(
+ getAttachedSurfaceResolution(getCameraIdUnchecked(configuration.getLensFacing())));
+ notifyReset();
+
+ // notify the UI thread that the video recording has finished
+ endOfAudioVideoSignal.set(true);
+
+ Log.i(TAG, "Video encode thread end.");
+ return errorOccurred;
+ }
+
+ private boolean audioEncode(OnVideoSavedListener videoSavedListener) {
+ // Audio encoding loop. Exits on end of stream.
+ boolean audioEos = false;
+ int outIndex;
+ while (!audioEos && isRecording) {
+ // Check for end of stream from main thread
+ if (endOfAudioStreamSignal.get()) {
+ endOfAudioStreamSignal.set(false);
+ isRecording = false;
+ }
+
+ // get audio deque input buffer
+ if (audioEncoder != null && audioRecorder != null) {
+ int index = audioEncoder.dequeueInputBuffer(-1);
+ if (index >= 0) {
+ final ByteBuffer buffer = getInputBuffer(audioEncoder, index);
+ buffer.clear();
+ int length = audioRecorder.read(buffer, audioBufferSize);
+ if (length > 0) {
+ audioEncoder.queueInputBuffer(
+ index,
+ 0,
+ length,
+ (System.nanoTime() / 1000),
+ isRecording ? 0 : MediaCodec.BUFFER_FLAG_END_OF_STREAM);
+ }
+ }
+
+ // start to dequeue audio output buffer
+ do {
+ outIndex = audioEncoder.dequeueOutputBuffer(audioBufferInfo, 0);
+ switch (outIndex) {
+ case MediaCodec.INFO_OUTPUT_FORMAT_CHANGED:
+ synchronized (muxerLock) {
+ audioTrackIndex = muxer.addTrack(audioEncoder.getOutputFormat());
+ if (audioTrackIndex >= 0 && videoTrackIndex >= 0) {
+ muxerStarted = true;
+ muxer.start();
+ }
+ }
+ break;
+ case MediaCodec.INFO_TRY_AGAIN_LATER:
+ break;
+ default:
+ audioEos = writeAudioEncodedBuffer(outIndex);
+ }
+ } while (outIndex >= 0 && !audioEos); // end of dequeue output buffer
+ }
+ } // end of while loop
+
+ // Audio Stop
+ try {
+ Log.i(TAG, "audioRecorder stop");
+ audioRecorder.stop();
+ } catch (IllegalStateException e) {
+ videoSavedListener.onError(
+ UseCaseError.ENCODER_ERROR, "Audio recorder stop failed!", e);
+ }
+
+ try {
+ audioEncoder.stop();
+ } catch (IllegalStateException e) {
+ videoSavedListener.onError(UseCaseError.ENCODER_ERROR, "Audio encoder stop failed!", e);
+ }
+
+ Log.i(TAG, "Audio encode thread end");
+ // Use AtomicBoolean to signal because MediaCodec.signalEndOfInputStream() is not thread
+ // safe
+ endOfVideoStreamSignal.set(true);
+
+ return false;
+ }
+
+ private ByteBuffer getInputBuffer(MediaCodec codec, int index) {
+ return codec.getInputBuffer(index);
+ }
+
+ private ByteBuffer getOutputBuffer(MediaCodec codec, int index) {
+ return codec.getOutputBuffer(index);
+ }
+
+ /** Creates a {@link MediaFormat} using parameters for audio from the configuration */
+ private MediaFormat createAudioMediaFormat() {
+ MediaFormat format =
+ MediaFormat.createAudioFormat(AUDIO_MIME_TYPE, audioSampleRate, audioChannelCount);
+ format.setInteger(
+ MediaFormat.KEY_AAC_PROFILE, MediaCodecInfo.CodecProfileLevel.AACObjectLC);
+ format.setInteger(MediaFormat.KEY_BIT_RATE, audioBitRate);
+
+ return format;
+ }
+
+ /** Create a AudioRecord object to get raw data */
+ private AudioRecord autoConfigAudioRecordSource(
+ VideoCaptureUseCaseConfiguration configuration) {
+ for (short audioFormat : audioEncoding) {
+
+ // Use channel count to determine stereo vs mono
+ int channelConfig =
+ audioChannelCount == 1
+ ? AudioFormat.CHANNEL_IN_MONO
+ : AudioFormat.CHANNEL_IN_STEREO;
+ int source = configuration.getAudioRecordSource();
+
+ try {
+ int bufferSize =
+ AudioRecord.getMinBufferSize(audioSampleRate, channelConfig, audioFormat);
+
+ if (bufferSize <= 0) {
+ bufferSize = configuration.getAudioMinBufferSize();
+ }
+
+ AudioRecord recorder =
+ new AudioRecord(
+ source,
+ audioSampleRate,
+ channelConfig,
+ audioFormat,
+ bufferSize * 2);
+
+ if (recorder.getState() == AudioRecord.STATE_INITIALIZED) {
+ audioBufferSize = bufferSize;
+ Log.i(
+ TAG,
+ "source: "
+ + source
+ + " audioSampleRate: "
+ + audioSampleRate
+ + " channelConfig: "
+ + channelConfig
+ + " audioFormat: "
+ + audioFormat
+ + " bufferSize: "
+ + bufferSize);
+ return recorder;
+ }
+ } catch (Exception e) {
+ Log.e(TAG, "Exception, keep trying.", e);
+ }
+ }
+
+ return null;
+ }
+
+ /** Set audio record parameters by CamcorderProfile */
+ private void setAudioParametersByCamcorderProfile(Size currentResolution, String cameraId) {
+ CamcorderProfile profile;
+ boolean isCamcorderProfileFound = false;
+
+ for (int quality : CamcorderQuality) {
+ if (CamcorderProfile.hasProfile(Integer.parseInt(cameraId), quality)) {
+ profile = CamcorderProfile.get(Integer.parseInt(cameraId), quality);
+ if (currentResolution.getWidth() == profile.videoFrameWidth
+ && currentResolution.getHeight() == profile.videoFrameHeight) {
+ audioChannelCount = profile.audioChannels;
+ audioSampleRate = profile.audioSampleRate;
+ audioBitRate = profile.audioBitRate;
+ isCamcorderProfileFound = true;
+ break;
+ }
+ }
+ }
+
+ // In case no corresponding camcorder profile can be founded, * get default value from
+ // VideoCaptureUseCaseConfiguration.
+ if (!isCamcorderProfileFound) {
+ VideoCaptureUseCaseConfiguration config =
+ (VideoCaptureUseCaseConfiguration) getUseCaseConfiguration();
+ audioChannelCount = config.getAudioChannelCount();
+ audioSampleRate = config.getAudioSampleRate();
+ audioBitRate = config.getAudioBitRate();
+ }
+ }
+
+ /**
+ * Describes the error that occurred during video capture operations.
+ *
+ * <p>This is a parameter sent to the error callback functions set in listeners such as {@link
+ * VideoCaptureUseCase.OnVideoSavedListener.onError}.
+ *
+ * <p>See message parameter in onError callback or log for more details.
+ */
+ public enum UseCaseError {
+ /**
+ * An unknown error occurred.
+ *
+ * <p>See message parameter in onError callback or log for more details.
+ */
+ UNKNOWN_ERROR,
+ /**
+ * An error occurred with encoder state, either when trying to change state or when an
+ * unexpected state change occurred.
+ */
+ ENCODER_ERROR,
+ /** An error with muxer state such as during creation or when stopping. */
+ MUXER_ERROR,
+ /**
+ * An error indicating start recording was called when video recording is still in progress.
+ */
+ RECORDING_IN_PROGRESS
+ }
+
+ /** Listener containing callbacks for video file I/O events. */
+ public interface OnVideoSavedListener {
+ /** Called when the video has been successfully saved. */
+ void onVideoSaved(File file);
+
+ /** Called when an error occurs while attempting to save the video. */
+ void onError(UseCaseError useCaseError, String message, @Nullable Throwable cause);
+ }
+
+ /**
+ * Provides a base static default configuration for the VideoCaptureUseCase
+ *
+ * <p>These values may be overridden by the implementation. They only provide a minimum set of
+ * defaults that are implementation independent.
+ *
+ * @hide
+ */
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ public static final class Defaults
+ implements ConfigurationProvider<VideoCaptureUseCaseConfiguration> {
+ private static final Handler DEFAULT_HANDLER = new Handler(Looper.getMainLooper());
+ private static final Rational DEFAULT_ASPECT_RATIO = new Rational(16, 9);
+ private static final int DEFAULT_VIDEO_FRAME_RATE = 30;
+ /** 8Mb/s the recommend rate for 30fps 1080p */
+ private static final int DEFAULT_BIT_RATE = 8 * 1024 * 1024;
+ /** Seconds between each key frame */
+ private static final int DEFAULT_INTRA_FRAME_INTERVAL = 1;
+ /** audio bit rate */
+ private static final int DEFAULT_AUDIO_BIT_RATE = 64000;
+ /** audio sample rate */
+ private static final int DEFAULT_AUDIO_SAMPLE_RATE = 8000;
+ /** audio channel count */
+ private static final int DEFAULT_AUDIO_CHANNEL_COUNT = 1;
+ /** audio record source */
+ private static final int DEFAULT_AUDIO_RECORD_SOURCE = AudioSource.MIC;
+ /** audio default minimum buffer size */
+ private static final int DEFAULT_AUDIO_MIN_BUFFER_SIZE = 1024;
+ /** Current max resolution of VideoCaptureUseCase is set as FHD */
+ private static final Size DEFAULT_MAX_RESOLUTION = new Size(1920, 1080);
+ /** Surface occupancy prioirty to this use case */
+ private static final int DEFAULT_SURFACE_OCCUPANCY_PRIORITY = 3;
+
+ private static final VideoCaptureUseCaseConfiguration DEFAULT_CONFIG;
+
+ static {
+ VideoCaptureUseCaseConfiguration.Builder builder =
+ new VideoCaptureUseCaseConfiguration.Builder()
+ .setCallbackHandler(DEFAULT_HANDLER)
+ .setTargetAspectRatio(DEFAULT_ASPECT_RATIO)
+ .setVideoFrameRate(DEFAULT_VIDEO_FRAME_RATE)
+ .setBitRate(DEFAULT_BIT_RATE)
+ .setIFrameInterval(DEFAULT_INTRA_FRAME_INTERVAL)
+ .setAudioBitRate(DEFAULT_AUDIO_BIT_RATE)
+ .setAudioSampleRate(DEFAULT_AUDIO_SAMPLE_RATE)
+ .setAudioChannelCount(DEFAULT_AUDIO_CHANNEL_COUNT)
+ .setAudioRecordSource(DEFAULT_AUDIO_RECORD_SOURCE)
+ .setAudioMinBufferSize(DEFAULT_AUDIO_MIN_BUFFER_SIZE)
+ .setMaxResolution(DEFAULT_MAX_RESOLUTION)
+ .setSurfaceOccupancyPriority(DEFAULT_SURFACE_OCCUPANCY_PRIORITY);
+
+ DEFAULT_CONFIG = builder.build();
+ }
+
+ @Override
+ public VideoCaptureUseCaseConfiguration getConfiguration() {
+ return DEFAULT_CONFIG;
+ }
+ }
+
+ /** Holder class for metadata that should be saved alongside captured video. */
+ public static final class Metadata {
+ /** Data representing a geographic location. */
+ public @Nullable
+ Location location;
+ }
+}
diff --git a/camera/core/src/main/java/androidx/camera/core/VideoCaptureUseCaseConfiguration.java b/camera/core/src/main/java/androidx/camera/core/VideoCaptureUseCaseConfiguration.java
new file mode 100644
index 0000000..7de0058
--- /dev/null
+++ b/camera/core/src/main/java/androidx/camera/core/VideoCaptureUseCaseConfiguration.java
@@ -0,0 +1,413 @@
+/*
+ * Copyright (C) 2019 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.core;
+
+import androidx.annotation.RestrictTo;
+import androidx.annotation.RestrictTo.Scope;
+
+/** Configuration for a video capture use case. */
+public final class VideoCaptureUseCaseConfiguration
+ implements UseCaseConfiguration<VideoCaptureUseCase>,
+ ImageOutputConfiguration,
+ CameraDeviceConfiguration,
+ ThreadConfiguration {
+
+ // Option Declarations:
+ // ***********************************************************************************************
+ static final Option<Integer> OPTION_VIDEO_FRAME_RATE =
+ Option.create("camerax.core.videoCapture.recordingFrameRate", int.class);
+ static final Option<Integer> OPTION_BIT_RATE =
+ Option.create("camerax.core.videoCapture.bitRate", int.class);
+ static final Option<Integer> OPTION_INTRA_FRAME_INTERVAL =
+ Option.create("camerax.core.videoCapture.intraFrameInterval", int.class);
+ static final Option<Integer> OPTION_AUDIO_BIT_RATE =
+ Option.create("camerax.core.videoCapture.audioBitRate", int.class);
+ static final Option<Integer> OPTION_AUDIO_SAMPLE_RATE =
+ Option.create("camerax.core.videoCapture.audioSampleRate", int.class);
+ static final Option<Integer> OPTION_AUDIO_CHANNEL_COUNT =
+ Option.create("camerax.core.videoCapture.audioChannelCount", int.class);
+ static final Option<Integer> OPTION_AUDIO_RECORD_SOURCE =
+ Option.create("camerax.core.videoCapture.audioRecordSource", int.class);
+ static final Option<Integer> OPTION_AUDIO_MIN_BUFFER_SIZE =
+ Option.create("camerax.core.videoCapture.audioMinBufferSize", int.class);
+ private final OptionsBundle config;
+
+ VideoCaptureUseCaseConfiguration(OptionsBundle config) {
+ this.config = config;
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * @hide
+ */
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ @Override
+ public Configuration getConfiguration() {
+ return config;
+ }
+
+ /**
+ * Returns the recording frames per second.
+ *
+ * @param valueIfMissing The value to return if this configuration option has not been set.
+ * @return The stored value or <code>valueIfMissing</code> if the value does not exist in this
+ * configuration.
+ */
+ public int getVideoFrameRate(int valueIfMissing) {
+ return getConfiguration().retrieveOption(OPTION_VIDEO_FRAME_RATE, valueIfMissing);
+ }
+
+ /**
+ * Returns the recording frames per second.
+ *
+ * @return The stored value, if it exists in this configuration.
+ * @throws IllegalArgumentException if the option does not exist in this configuration.
+ */
+ public int getVideoFrameRate() {
+ return getConfiguration().retrieveOption(OPTION_VIDEO_FRAME_RATE);
+ }
+
+ /**
+ * Returns the encoding bit rate.
+ *
+ * @param valueIfMissing The value to return if this configuration option has not been set.
+ * @return The stored value or <code>valueIfMissing</code> if the value does not exist in this
+ * configuration.
+ */
+ public int getBitRate(int valueIfMissing) {
+ return getConfiguration().retrieveOption(OPTION_BIT_RATE, valueIfMissing);
+ }
+
+ /**
+ * Returns the encoding bit rate.
+ *
+ * @return The stored value, if it exists in this configuration.
+ * @throws IllegalArgumentException if the option does not exist in this configuration.
+ */
+ public int getBitRate() {
+ return getConfiguration().retrieveOption(OPTION_BIT_RATE);
+ }
+
+ /**
+ * Returns the number of seconds between each key frame.
+ *
+ * @param valueIfMissing The value to return if this configuration option has not been set.
+ * @return The stored value or <code>valueIfMissing</code> if the value does not exist in this
+ * configuration.
+ */
+ public int getIFrameInterval(int valueIfMissing) {
+ return getConfiguration().retrieveOption(OPTION_INTRA_FRAME_INTERVAL, valueIfMissing);
+ }
+
+ /**
+ * Returns the number of seconds between each key frame.
+ *
+ * @return The stored value, if it exists in this configuration.
+ * @throws IllegalArgumentException if the option does not exist in this configuration.
+ */
+ public int getIFrameInterval() {
+ return getConfiguration().retrieveOption(OPTION_INTRA_FRAME_INTERVAL);
+ }
+
+ /**
+ * Returns the audio encoding bit rate.
+ *
+ * @param valueIfMissing The value to return if this configuration option has not been set.
+ * @return The stored value or <code>valueIfMissing</code> if the value does not exist in this
+ * configuration.
+ * @hide
+ */
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ public int getAudioBitRate(int valueIfMissing) {
+ return getConfiguration().retrieveOption(OPTION_AUDIO_BIT_RATE, valueIfMissing);
+ }
+
+ /**
+ * Returns the audio encoding bit rate.
+ *
+ * @return The stored value, if it exists in this configuration.
+ * @throws IllegalArgumentException if the option does not exist in this configuration.
+ * @hide
+ */
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ public int getAudioBitRate() {
+ return getConfiguration().retrieveOption(OPTION_AUDIO_BIT_RATE);
+ }
+
+ /**
+ * Returns the audio sample rate.
+ *
+ * @param valueIfMissing The value to return if this configuration option has not been set.
+ * @return The stored value or <code>valueIfMissing</code> if the value does not exist in this
+ * configuration.
+ * @hide
+ */
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ public int getAudioSampleRate(int valueIfMissing) {
+ return getConfiguration().retrieveOption(OPTION_AUDIO_SAMPLE_RATE, valueIfMissing);
+ }
+
+ /**
+ * Returns the audio sample rate.
+ *
+ * @return The stored value, if it exists in this configuration.
+ * @throws IllegalArgumentException if the option does not exist in this configuration.
+ * @hide
+ */
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ public int getAudioSampleRate() {
+ return getConfiguration().retrieveOption(OPTION_AUDIO_SAMPLE_RATE);
+ }
+
+ /**
+ * Returns the audio channel count.
+ *
+ * @param valueIfMissing The value to return if this configuration option has not been set.
+ * @return The stored value or <code>valueIfMissing</code> if the value does not exist in this
+ * configuration.
+ * @hide
+ */
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ public int getAudioChannelCount(int valueIfMissing) {
+ return getConfiguration().retrieveOption(OPTION_AUDIO_CHANNEL_COUNT, valueIfMissing);
+ }
+
+ /**
+ * Returns the audio channel count.
+ *
+ * @return The stored value, if it exists in this configuration.
+ * @throws IllegalArgumentException if the option does not exist in this configuration.
+ * @hide
+ */
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ public int getAudioChannelCount() {
+ return getConfiguration().retrieveOption(OPTION_AUDIO_CHANNEL_COUNT);
+ }
+
+ /**
+ * Returns the audio recording source.
+ *
+ * @param valueIfMissing The value to return if this configuration option has not been set.
+ * @return The stored value or <code>valueIfMissing</code> if the value does not exist in this
+ * configuration.
+ * @hide
+ */
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ public int getAudioRecordSource(int valueIfMissing) {
+ return getConfiguration().retrieveOption(OPTION_AUDIO_RECORD_SOURCE, valueIfMissing);
+ }
+
+ /**
+ * Returns the audio recording source.
+ *
+ * @return The stored value, if it exists in this configuration.
+ * @throws IllegalArgumentException if the option does not exist in this configuration.
+ * @hide
+ */
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ public int getAudioRecordSource() {
+ return getConfiguration().retrieveOption(OPTION_AUDIO_RECORD_SOURCE);
+ }
+
+ /**
+ * Returns the audio minimum buffer size, in bytes.
+ *
+ * @param valueIfMissing The value to return if this configuration option has not been set.
+ * @return The stored value or <code>valueIfMissing</code> if the value does not exist in this
+ * configuration.
+ * @hide
+ */
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ public int getAudioMinBufferSize(int valueIfMissing) {
+ return getConfiguration().retrieveOption(OPTION_AUDIO_MIN_BUFFER_SIZE, valueIfMissing);
+ }
+
+ /**
+ * Returns the audio minimum buffer size, in bytes.
+ *
+ * @return The stored value, if it exists in this configuration.
+ * @throws IllegalArgumentException if the option does not exist in this configuration.
+ * @hide
+ */
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ public int getAudioMinBufferSize() {
+ return getConfiguration().retrieveOption(OPTION_AUDIO_MIN_BUFFER_SIZE);
+ }
+
+ /** Builder for a {@link VideoCaptureUseCaseConfiguration}. */
+ public static final class Builder
+ implements UseCaseConfiguration.Builder<
+ VideoCaptureUseCase, VideoCaptureUseCaseConfiguration, Builder>,
+ ImageOutputConfiguration.Builder<VideoCaptureUseCaseConfiguration, Builder>,
+ CameraDeviceConfiguration.Builder<VideoCaptureUseCaseConfiguration, Builder>,
+ ThreadConfiguration.Builder<VideoCaptureUseCaseConfiguration, Builder> {
+
+ private final MutableOptionsBundle mutableConfig;
+
+ /** Creates a new Builder object. */
+ public Builder() {
+ this(MutableOptionsBundle.create());
+ }
+
+ private Builder(MutableOptionsBundle mutableConfig) {
+ this.mutableConfig = mutableConfig;
+
+ Class<?> oldConfigClass =
+ mutableConfig.retrieveOption(TargetConfiguration.OPTION_TARGET_CLASS, null);
+ if (oldConfigClass != null && !oldConfigClass.equals(VideoCaptureUseCase.class)) {
+ throw new IllegalArgumentException(
+ "Invalid target class configuration for "
+ + Builder.this
+ + ": "
+ + oldConfigClass);
+ }
+
+ setTargetClass(VideoCaptureUseCase.class);
+ }
+
+ /**
+ * Generates a Builder from another Configuration object
+ *
+ * @param configuration An immutable configuration to pre-populate this builder.
+ * @return The new Builder.
+ */
+ public static Builder fromConfig(VideoCaptureUseCaseConfiguration configuration) {
+ return new Builder(MutableOptionsBundle.from(configuration));
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * @hide
+ */
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ @Override
+ public MutableConfiguration getMutableConfiguration() {
+ return mutableConfig;
+ }
+
+ /** The solution for the unchecked cast warning. */
+ @Override
+ public Builder builder() {
+ return this;
+ }
+
+ @Override
+ public VideoCaptureUseCaseConfiguration build() {
+ return new VideoCaptureUseCaseConfiguration(OptionsBundle.from(mutableConfig));
+ }
+
+ /**
+ * Sets the recording frames per second.
+ *
+ * @param videoFrameRate The requested interval in seconds.
+ * @return The current Builder.
+ */
+ public Builder setVideoFrameRate(int videoFrameRate) {
+ getMutableConfiguration().insertOption(OPTION_VIDEO_FRAME_RATE, videoFrameRate);
+ return builder();
+ }
+
+ /**
+ * Sets the encoding bit rate.
+ *
+ * @param bitRate The requested bit rate in bits per second.
+ * @return The current Builder.
+ */
+ public Builder setBitRate(int bitRate) {
+ getMutableConfiguration().insertOption(OPTION_BIT_RATE, bitRate);
+ return builder();
+ }
+
+ /**
+ * Sets number of seconds between each key frame in seconds.
+ *
+ * @param interval The requested interval in seconds.
+ * @return The current Builder.
+ */
+ public Builder setIFrameInterval(int interval) {
+ getMutableConfiguration().insertOption(OPTION_INTRA_FRAME_INTERVAL, interval);
+ return builder();
+ }
+
+ /**
+ * Sets the bit rate of the audio stream.
+ *
+ * @param bitRate The requested bit rate in bits/s.
+ * @return The current Builder.
+ * @hide
+ */
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ public Builder setAudioBitRate(int bitRate) {
+ getMutableConfiguration().insertOption(OPTION_AUDIO_BIT_RATE, bitRate);
+ return builder();
+ }
+
+ /**
+ * Sets the sample rate of the audio stream.
+ *
+ * @param sampleRate The requested sample rate in bits/s.
+ * @return The current Builder.
+ * @hide
+ */
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ public Builder setAudioSampleRate(int sampleRate) {
+ getMutableConfiguration().insertOption(OPTION_AUDIO_SAMPLE_RATE, sampleRate);
+ return builder();
+ }
+
+ /**
+ * Sets the number of audio channels.
+ *
+ * @param channelCount The requested number of audio channels.
+ * @return The current Builder.
+ * @hide
+ */
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ public Builder setAudioChannelCount(int channelCount) {
+ getMutableConfiguration().insertOption(OPTION_AUDIO_CHANNEL_COUNT, channelCount);
+ return builder();
+ }
+
+ /**
+ * Sets the audio source.
+ *
+ * @param source The audio source. Currently only AudioSource.MIC is supported.
+ * @return The current Builder.
+ * @hide
+ */
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ public Builder setAudioRecordSource(int source) {
+ getMutableConfiguration().insertOption(OPTION_AUDIO_RECORD_SOURCE, source);
+ return builder();
+ }
+
+ /**
+ * Sets the audio min buffer size.
+ *
+ * @param minBufferSize The requested audio minimum buffer size, in bytes.
+ * @return The current Builder.
+ * @hide
+ */
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ public Builder setAudioMinBufferSize(int minBufferSize) {
+ getMutableConfiguration().insertOption(OPTION_AUDIO_MIN_BUFFER_SIZE, minBufferSize);
+ return builder();
+ }
+ }
+}
diff --git a/camera/core/src/main/java/androidx/camera/core/ViewFinderUseCase.java b/camera/core/src/main/java/androidx/camera/core/ViewFinderUseCase.java
new file mode 100644
index 0000000..627fb1c
--- /dev/null
+++ b/camera/core/src/main/java/androidx/camera/core/ViewFinderUseCase.java
@@ -0,0 +1,453 @@
+/*
+ * Copyright (C) 2019 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.core;
+
+import android.graphics.Rect;
+import android.graphics.SurfaceTexture;
+import android.os.Handler;
+import android.os.Looper;
+import android.util.Log;
+import android.util.Rational;
+import android.util.Size;
+import android.view.Display;
+import android.view.Surface;
+
+import androidx.annotation.MainThread;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.annotation.RestrictTo.Scope;
+import androidx.annotation.UiThread;
+import androidx.camera.core.CameraX.LensFacing;
+import androidx.camera.core.ImageOutputConfiguration.RotationValue;
+
+import com.google.auto.value.AutoValue;
+
+import java.util.Map;
+import java.util.Objects;
+
+/**
+ * A use case that provides a camera preview stream for a view finder.
+ *
+ * <p>The preview stream is connected to an underlying {@link SurfaceTexture}. The caller is still
+ * responsible for deciding how this texture is shown.
+ */
+public class ViewFinderUseCase extends BaseUseCase {
+ /**
+ * Provides a static configuration with implementation-agnostic options.
+ *
+ * @hide
+ */
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ public static final Defaults DEFAULT_CONFIG = new Defaults();
+ private static final String TAG = "ViewFinderUseCase";
+ private final Handler mainHandler = new Handler(Looper.getMainLooper());
+ private final CheckedSurfaceTexture.OnTextureChangedListener surfaceTextureListener =
+ (newSurfaceTexture, newResolution) ->
+ ViewFinderUseCase.this.updateOutput(newSurfaceTexture, newResolution);
+ private final CheckedSurfaceTexture checkedSurfaceTexture =
+ new CheckedSurfaceTexture(surfaceTextureListener, mainHandler);
+ private final ViewFinderUseCaseConfiguration.Builder useCaseConfigBuilder;
+ @Nullable
+ private OnViewFinderOutputUpdateListener subscribedViewFinderOutputListener;
+ @Nullable
+ private ViewFinderOutput latestViewFinderOutput;
+ private boolean surfaceDispatched = false;
+ /**
+ * Creates a new view finder use case from the given configuration.
+ *
+ * @param configuration for this use case instance
+ */
+ @MainThread
+ public ViewFinderUseCase(ViewFinderUseCaseConfiguration configuration) {
+ super(configuration);
+ useCaseConfigBuilder = ViewFinderUseCaseConfiguration.Builder.fromConfig(configuration);
+ }
+
+ private static SessionConfiguration.Builder createFrom(
+ ViewFinderUseCaseConfiguration configuration, DeferrableSurface surface) {
+ SessionConfiguration.Builder sessionConfigBuilder =
+ SessionConfiguration.Builder.createFrom(configuration);
+ sessionConfigBuilder.addSurface(surface);
+ return sessionConfigBuilder;
+ }
+
+ private static final String getCameraIdUnchecked(LensFacing lensFacing) {
+ try {
+ return CameraX.getCameraWithLensFacing(lensFacing);
+ } catch (Exception e) {
+ throw new IllegalArgumentException(
+ "Unable to get camera id for camera lens facing " + lensFacing, e);
+ }
+ }
+
+ /**
+ * Removes previously ViewFinderOutput listener.
+ *
+ * <p>This is equivalent to calling {@code setOnViewFinderOutputUpdateListener(null)}.
+ */
+ @UiThread
+ public void removeViewFinderOutputListener() {
+ setOnViewFinderOutputUpdateListener(null);
+ }
+
+ /**
+ * Gets {@link OnViewFinderOutputUpdateListener}
+ *
+ * @return the last set listener or {@code null} if no listener is set
+ */
+ @UiThread
+ @Nullable
+ public OnViewFinderOutputUpdateListener getOnViewFinderOutputUpdateListener() {
+ return subscribedViewFinderOutputListener;
+ }
+
+ /**
+ * Sets a listener to get the {@link ViewFinderOutput} updates.
+ *
+ * <p>Setting this listener will signal to the camera that the use case is ready to receive
+ * data. Setting the listener to {@code null} will signal to the camera that the camera should
+ * no longer stream data to the last {@link ViewFinderOutput}.
+ *
+ * <p>Once {@link OnViewFinderOutputUpdateListener#onUpdated(ViewFinderOutput)} is called,
+ * ownership of the {@link ViewFinderOutput} and its contents is transferred to the user. It is
+ * the user's responsibility to release the last {@link SurfaceTexture} returned by {@link
+ * ViewFinderOutput#getSurfaceTexture()} when a new SurfaceTexture is provided via an update or
+ * when the user is finished with the use case.
+ *
+ * @param listener The listener which will receive {@link ViewFinderOutput} updates.
+ */
+ @UiThread
+ public void setOnViewFinderOutputUpdateListener(
+ @Nullable OnViewFinderOutputUpdateListener newListener) {
+ OnViewFinderOutputUpdateListener oldListener = subscribedViewFinderOutputListener;
+ subscribedViewFinderOutputListener = newListener;
+ if (oldListener == null && newListener != null) {
+ notifyActive();
+ if (latestViewFinderOutput != null) {
+ surfaceDispatched = true;
+ newListener.onUpdated(latestViewFinderOutput);
+ }
+ } else if (oldListener != null && newListener == null) {
+ notifyInactive();
+ } else if (oldListener != null && oldListener != newListener) {
+ if (latestViewFinderOutput != null) {
+ checkedSurfaceTexture.resetSurfaceTexture();
+ }
+ }
+ }
+
+ // TODO: Timeout may be exposed as a ViewFinderUseCaseConfiguration(moved to CameraControl)
+
+ private CameraControl getCurrentCameraControl() {
+ ViewFinderUseCaseConfiguration configuration =
+ (ViewFinderUseCaseConfiguration) getUseCaseConfiguration();
+ String cameraId = getCameraIdUnchecked(configuration.getLensFacing());
+ return getCameraControl(cameraId);
+ }
+
+ /**
+ * Adjusts the view finder according to the properties in some local regions.
+ *
+ * <p>The auto-focus (AF) and auto-exposure (AE) properties will be recalculated from the local
+ * regions.
+ *
+ * @param focus rectangle with dimensions in sensor coordinate frame for focus
+ * @param metering rectangle with dimensions in sensor coordinate frame for metering
+ */
+ public void focus(Rect focus, Rect metering) {
+ focus(focus, metering, null);
+ }
+
+ /**
+ * Adjusts the view finder according to the properties in some local regions with a callback
+ * called once focus scan has completed.
+ *
+ * <p>The auto-focus (AF) and auto-exposure (AE) properties will be recalculated from the local
+ * regions.
+ *
+ * @param focus rectangle with dimensions in sensor coordinate frame for focus
+ * @param metering rectangle with dimensions in sensor coordinate frame for metering
+ * @param listener listener for when focus has completed
+ */
+ public void focus(Rect focus, Rect metering, @Nullable OnFocusCompletedListener listener) {
+ getCurrentCameraControl().focus(focus, metering, listener, mainHandler);
+ }
+
+ /**
+ * Adjusts the view finder to zoom to a local region.
+ *
+ * @param crop rectangle with dimensions in sensor coordinate frame for zooming
+ */
+ public void zoom(Rect crop) {
+ getCurrentCameraControl().setCropRegion(crop);
+ }
+
+ /**
+ * Sets torch on/off.
+ *
+ * @param torch True if turn on torch, otherwise false
+ */
+ public void enableTorch(boolean torch) {
+ getCurrentCameraControl().enableTorch(torch);
+ }
+
+ /** True if the torch is on */
+ public boolean isTorchOn() {
+ return getCurrentCameraControl().isTorchOn();
+ }
+
+ /**
+ * Sets the rotation of the surface texture consumer.
+ *
+ * <p>In most cases this should be set to the current rotation returned by {@link
+ * Display#getRotation()}. This will update the rotation value in {@link ViewFinderOutput} to
+ * reflect the angle the ViewFinderOutput should be rotated to match the supplied rotation.
+ *
+ * @param rotation Rotation of the surface texture consumer.
+ */
+ public void setTargetRotation(@RotationValue int rotation) {
+ ImageOutputConfiguration oldconfig = (ImageOutputConfiguration) getUseCaseConfiguration();
+ int oldRotation = oldconfig.getTargetRotation(ImageOutputConfiguration.INVALID_ROTATION);
+ if (oldRotation == ImageOutputConfiguration.INVALID_ROTATION || oldRotation != rotation) {
+ useCaseConfigBuilder.setTargetRotation(rotation);
+ updateUseCaseConfiguration(useCaseConfigBuilder.build());
+
+ // TODO(b/122846516): Update session configuration and possibly reconfigure session.
+ // For now we'll just attempt to update the rotation metadata.
+ invalidateMetadata();
+ }
+ }
+
+ @Override
+ public String toString() {
+ return TAG + ":" + getName();
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * @hide
+ */
+ @Override
+ @Nullable
+ protected UseCaseConfiguration.Builder<?, ?, ?> getDefaultBuilder() {
+ ViewFinderUseCaseConfiguration defaults =
+ CameraX.getDefaultUseCaseConfiguration(ViewFinderUseCaseConfiguration.class);
+ if (defaults != null) {
+ return ViewFinderUseCaseConfiguration.Builder.fromConfig(defaults);
+ }
+
+ return null;
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * @hide
+ */
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ @Override
+ public void clear() {
+ checkedSurfaceTexture.release();
+ removeViewFinderOutputListener();
+ notifyInactive();
+
+ SurfaceTexture oldTexture =
+ (latestViewFinderOutput == null)
+ ? null
+ : latestViewFinderOutput.getSurfaceTexture();
+ if (oldTexture != null && !surfaceDispatched) {
+ oldTexture.release();
+ }
+
+ super.clear();
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * @hide
+ */
+ @Override
+ protected Map<String, Size> onSuggestedResolutionUpdated(
+ Map<String, Size> suggestedResolutionMap) {
+ ViewFinderUseCaseConfiguration configuration =
+ (ViewFinderUseCaseConfiguration) getUseCaseConfiguration();
+ String cameraId = getCameraIdUnchecked(configuration.getLensFacing());
+ Size resolution = suggestedResolutionMap.get(cameraId);
+ if (resolution == null) {
+ throw new IllegalArgumentException(
+ "Suggested resolution map missing resolution for camera " + cameraId);
+ }
+
+ checkedSurfaceTexture.setResolution(resolution);
+ checkedSurfaceTexture.resetSurfaceTexture();
+
+ SessionConfiguration.Builder sessionConfigBuilder =
+ createFrom(configuration, checkedSurfaceTexture);
+ attachToCamera(cameraId, sessionConfigBuilder.build());
+
+ return suggestedResolutionMap;
+ }
+
+ @UiThread
+ private void invalidateMetadata() {
+ if (latestViewFinderOutput != null) {
+ // Only update the output if we have a SurfaceTexture. Otherwise we'll wait until a
+ // SurfaceTexture is ready.
+ updateOutput(
+ latestViewFinderOutput.getSurfaceTexture(),
+ latestViewFinderOutput.getTextureSize());
+ }
+ }
+
+ @UiThread
+ private void updateOutput(SurfaceTexture surfaceTexture, Size resolution) {
+ ViewFinderUseCaseConfiguration useCaseConfig =
+ (ViewFinderUseCaseConfiguration) getUseCaseConfiguration();
+
+ int relativeRotation =
+ (latestViewFinderOutput == null) ? 0 : latestViewFinderOutput.getRotationDegrees();
+ try {
+ // Attempt to get the camera ID. If this fails, we probably don't have permission, so we
+ // will rely on the updated UseCaseConfiguration to set the correct rotation in
+ // onSuggestedResolutionUpdated()
+ String cameraId = CameraX.getCameraWithLensFacing(useCaseConfig.getLensFacing());
+ CameraInfo cameraInfo = CameraX.getCameraInfo(cameraId);
+ relativeRotation =
+ cameraInfo.getSensorRotationDegrees(
+ useCaseConfig.getTargetRotation(Surface.ROTATION_0));
+ } catch (CameraInfoUnavailableException e) {
+ Log.e(TAG, "Unable to update output metadata: " + e);
+ }
+
+ ViewFinderOutput newOutput =
+ ViewFinderOutput.create(surfaceTexture, resolution, relativeRotation);
+
+ // Only update the output if something has changed
+ if (!Objects.equals(latestViewFinderOutput, newOutput)) {
+ SurfaceTexture oldTexture =
+ (latestViewFinderOutput == null)
+ ? null
+ : latestViewFinderOutput.getSurfaceTexture();
+ OnViewFinderOutputUpdateListener outputListener = getOnViewFinderOutputUpdateListener();
+
+ latestViewFinderOutput = newOutput;
+
+ boolean textureChanged = oldTexture != surfaceTexture;
+ if (textureChanged) {
+ // If the old surface was never dispatched, we can safely release the old
+ // SurfaceTexture.
+ if (oldTexture != null && !surfaceDispatched) {
+ oldTexture.release();
+ }
+
+ // Keep track of whether this SurfaceTexture is dispatched
+ surfaceDispatched = false;
+ }
+
+ if (outputListener != null) {
+ // If we have a listener, then we should be active and we require a reset if the
+ // SurfaceTexture changed.
+ if (textureChanged) {
+ notifyReset();
+ }
+
+ surfaceDispatched = true;
+ outputListener.onUpdated(newOutput);
+ }
+ }
+ }
+
+ /** Describes the error that occurred during viewfinder operation. */
+ public enum UseCaseError {
+ /** Unknown error occurred. See message or log for more details. */
+ UNKNOWN_ERROR
+ }
+
+ /** A listener of {@link ViewFinderOutput}. */
+ public interface OnViewFinderOutputUpdateListener {
+ /** Callback when ViewFinderOutput has been updated. */
+ void onUpdated(ViewFinderOutput output);
+ }
+
+ /**
+ * Provides a base static default configuration for the ViewFinderUseCase
+ *
+ * <p>These values may be overridden by the implementation. They only provide a minimum set of
+ * defaults that are implementation independent.
+ *
+ * @hide
+ */
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ public static final class Defaults
+ implements ConfigurationProvider<ViewFinderUseCaseConfiguration> {
+ private static final Handler DEFAULT_HANDLER = new Handler(Looper.getMainLooper());
+ private static final Rational DEFAULT_ASPECT_RATIO = new Rational(16, 9);
+ private static final Size DEFAULT_MAX_RESOLUTION =
+ CameraX.getSurfaceManager().getPreviewSize();
+ private static final int DEFAULT_SURFACE_OCCUPANCY_PRIORITY = 2;
+
+ private static final ViewFinderUseCaseConfiguration DEFAULT_CONFIG;
+
+ static {
+ ViewFinderUseCaseConfiguration.Builder builder =
+ new ViewFinderUseCaseConfiguration.Builder()
+ .setCallbackHandler(DEFAULT_HANDLER)
+ .setTargetAspectRatio(DEFAULT_ASPECT_RATIO)
+ .setMaxResolution(DEFAULT_MAX_RESOLUTION)
+ .setSurfaceOccupancyPriority(DEFAULT_SURFACE_OCCUPANCY_PRIORITY);
+ DEFAULT_CONFIG = builder.build();
+ }
+
+ @Override
+ public ViewFinderUseCaseConfiguration getConfiguration() {
+ return DEFAULT_CONFIG;
+ }
+ }
+
+ /**
+ * A bundle containing a {@link SurfaceTexture} and properties needed to display a ViewFinder.
+ */
+ @AutoValue
+ public abstract static class ViewFinderOutput {
+
+ ViewFinderOutput() {
+ }
+
+ static ViewFinderOutput create(
+ SurfaceTexture surfaceTexture, Size textureSize, int rotationDegrees) {
+ return new AutoValue_ViewFinderUseCase_ViewFinderOutput(
+ surfaceTexture, textureSize, rotationDegrees);
+ }
+
+ /** Returns the ViewFinderOutput that receives image data. */
+ public abstract SurfaceTexture getSurfaceTexture();
+
+ /** Returns the dimensions of the ViewFinderOutput. */
+ public abstract Size getTextureSize();
+
+ /**
+ * Returns the rotation required, in degrees, to transform the ViewFinderOutput to match the
+ * orientation given by ImageOutputConfiguration#getTargetRotation(int).
+ *
+ * <p>This number is independent of any rotation value that can be derived from the
+ * ViewFinderOutput's {@link SurfaceTexture#getTransformMatrix(float[])}.
+ */
+ public abstract int getRotationDegrees();
+ }
+}
diff --git a/camera/core/src/main/java/androidx/camera/core/ViewFinderUseCaseConfiguration.java b/camera/core/src/main/java/androidx/camera/core/ViewFinderUseCaseConfiguration.java
new file mode 100644
index 0000000..8f2fc6d
--- /dev/null
+++ b/camera/core/src/main/java/androidx/camera/core/ViewFinderUseCaseConfiguration.java
@@ -0,0 +1,161 @@
+/*
+ * Copyright (C) 2019 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.core;
+
+import android.util.Size;
+
+import androidx.annotation.RestrictTo;
+import androidx.annotation.RestrictTo.Scope;
+
+/** Configuration for an image capture use case. */
+public final class ViewFinderUseCaseConfiguration
+ implements UseCaseConfiguration<ViewFinderUseCase>,
+ ImageOutputConfiguration,
+ CameraDeviceConfiguration,
+ ThreadConfiguration {
+
+ private final OptionsBundle config;
+
+ /** Creates a new configuration instance. */
+ ViewFinderUseCaseConfiguration(OptionsBundle config) {
+ this.config = config;
+ }
+
+ /**
+ * Retrieves the resolution of the target intending to use from this configuration.
+ *
+ * @param valueIfMissing The value to return if this configuration option has not been set.
+ * @return The stored value or {@code valueIfMissing} if the value does not exist in this
+ * configuration.
+ */
+ @Override
+ public Size getTargetResolution(Size valueIfMissing) {
+ return getConfiguration()
+ .retrieveOption(ImageOutputConfiguration.OPTION_TARGET_RESOLUTION, valueIfMissing);
+ }
+
+ /**
+ * Retrieves the resolution of the target intending to use from this configuration.
+ *
+ * @return The stored value, if it exists in this configuration.
+ * @throws IllegalArgumentException if the option does not exist in this configuration.
+ */
+ @Override
+ public Size getTargetResolution() {
+ return getConfiguration().retrieveOption(ImageOutputConfiguration.OPTION_TARGET_RESOLUTION);
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * @hide
+ */
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ @Override
+ public Configuration getConfiguration() {
+ return config;
+ }
+
+ /** Builder for a {@link ViewFinderUseCaseConfiguration}. */
+ public static final class Builder
+ implements UseCaseConfiguration.Builder<
+ ViewFinderUseCase, ViewFinderUseCaseConfiguration, Builder>,
+ ImageOutputConfiguration.Builder<ViewFinderUseCaseConfiguration, Builder>,
+ CameraDeviceConfiguration.Builder<ViewFinderUseCaseConfiguration, Builder>,
+ ThreadConfiguration.Builder<ViewFinderUseCaseConfiguration, Builder> {
+
+ private final MutableOptionsBundle mutableConfig;
+
+ /** Creates a new Builder object. */
+ public Builder() {
+ this(MutableOptionsBundle.create());
+ }
+
+ private Builder(MutableOptionsBundle mutableConfig) {
+ this.mutableConfig = mutableConfig;
+
+ Class<?> oldConfigClass =
+ mutableConfig.retrieveOption(TargetConfiguration.OPTION_TARGET_CLASS, null);
+ if (oldConfigClass != null && !oldConfigClass.equals(ViewFinderUseCase.class)) {
+ throw new IllegalArgumentException(
+ "Invalid target class configuration for "
+ + Builder.this
+ + ": "
+ + oldConfigClass);
+ }
+
+ setTargetClass(ViewFinderUseCase.class);
+ }
+
+ /**
+ * Generates a Builder from another Configuration object
+ *
+ * @param configuration An immutable configuration to pre-populate this builder.
+ * @return The new Builder.
+ */
+ public static Builder fromConfig(ViewFinderUseCaseConfiguration configuration) {
+ return new Builder(MutableOptionsBundle.from(configuration));
+ }
+
+ /**
+ * Sets the resolution of the intended target from this configuration.
+ *
+ * <p>The target resolution attempts to establish a minimum bound for the view finder
+ * resolution. The actual view finder resolution will be the closest available resolution in
+ * size that is not smaller than the target resolution, as determined by the Camera
+ * implementation. However, if no resolution exists that is equal to or larger than the
+ * target resolution, the nearest available resolution smaller than the target resolution
+ * will be chosen.
+ *
+ * @param resolution The target resolution to choose from supported output sizes list.
+ * @return The current Builder.
+ */
+ @Override
+ public Builder setTargetResolution(Size resolution) {
+ getMutableConfiguration()
+ .insertOption(ImageOutputConfiguration.OPTION_TARGET_RESOLUTION, resolution);
+ return builder();
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * @hide
+ */
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ @Override
+ public MutableConfiguration getMutableConfiguration() {
+ return mutableConfig;
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * @hide
+ */
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ @Override
+ public Builder builder() {
+ return this;
+ }
+
+ @Override
+ public ViewFinderUseCaseConfiguration build() {
+ return new ViewFinderUseCaseConfiguration(OptionsBundle.from(mutableConfig));
+ }
+ }
+}
diff --git a/camera/core/src/test/java/androidx/camera/core/AppConfigurationRobolectricTest.java b/camera/core/src/test/java/androidx/camera/core/AppConfigurationRobolectricTest.java
new file mode 100644
index 0000000..69013d2
--- /dev/null
+++ b/camera/core/src/test/java/androidx/camera/core/AppConfigurationRobolectricTest.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright (C) 2019 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.core;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import androidx.camera.testing.fakes.FakeAppConfiguration;
+import androidx.camera.testing.fakes.FakeCameraDeviceSurfaceManager;
+import androidx.camera.testing.fakes.FakeCameraFactory;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.internal.DoNotInstrument;
+
+@RunWith(RobolectricTestRunner.class)
+@DoNotInstrument
+public class AppConfigurationRobolectricTest {
+
+ private AppConfiguration appConfiguration;
+
+ @Before
+ public void setUp() {
+ appConfiguration = FakeAppConfiguration.create();
+ }
+
+ @Test
+ public void canGetConfigTarget() {
+ Class<CameraX> configTarget = appConfiguration.getTargetClass(/*valueIfMissing=*/ null);
+ assertThat(configTarget).isEqualTo(CameraX.class);
+ }
+
+ @Test
+ public void canGetCameraFactory() {
+ CameraFactory cameraFactory = appConfiguration.getCameraFactory(/*valueIfMissing=*/ null);
+ assertThat(cameraFactory).isInstanceOf(FakeCameraFactory.class);
+ }
+
+ @Test
+ public void canGetDeviceSurfaceManager() {
+ CameraDeviceSurfaceManager surfaceManager =
+ appConfiguration.getDeviceSurfaceManager(/*valueIfMissing=*/ null);
+ assertThat(surfaceManager).isInstanceOf(FakeCameraDeviceSurfaceManager.class);
+ }
+}
diff --git a/camera/core/src/test/java/androidx/camera/core/CheckedSurfaceTextureRobolectricTest.java b/camera/core/src/test/java/androidx/camera/core/CheckedSurfaceTextureRobolectricTest.java
new file mode 100644
index 0000000..576e94a
--- /dev/null
+++ b/camera/core/src/test/java/androidx/camera/core/CheckedSurfaceTextureRobolectricTest.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright (C) 2019 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.core;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.graphics.SurfaceTexture;
+import android.os.Handler;
+import android.os.Looper;
+import android.util.Size;
+
+import androidx.annotation.Nullable;
+import androidx.camera.core.CheckedSurfaceTexture.OnTextureChangedListener;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.internal.DoNotInstrument;
+
+@RunWith(RobolectricTestRunner.class)
+@DoNotInstrument
+public class CheckedSurfaceTextureRobolectricTest {
+
+ private final Handler mainThreadHandler = new Handler(Looper.getMainLooper());
+ private Size defaultResolution;
+ private CheckedSurfaceTexture checkedSurfaceTexture;
+ private SurfaceTexture latestSurfaceTexture;
+ private final CheckedSurfaceTexture.OnTextureChangedListener textureChangedListener =
+ new OnTextureChangedListener() {
+ @Override
+ public void onTextureChanged(
+ @Nullable SurfaceTexture newOutput, @Nullable Size newResolution) {
+ latestSurfaceTexture = newOutput;
+ }
+ };
+
+ @Before
+ public void setup() {
+ defaultResolution = new Size(640, 480);
+ checkedSurfaceTexture =
+ new CheckedSurfaceTexture(textureChangedListener, mainThreadHandler);
+ checkedSurfaceTexture.setResolution(defaultResolution);
+ }
+
+ @Test
+ public void viewFinderOutputUpdatesWhenReset() {
+ // Create the initial surface texture
+ checkedSurfaceTexture.resetSurfaceTexture();
+
+ // Surface texture should have been set
+ SurfaceTexture initialOutput = latestSurfaceTexture;
+
+ // Create a new surface texture
+ checkedSurfaceTexture.resetSurfaceTexture();
+
+ assertThat(initialOutput).isNotNull();
+ assertThat(latestSurfaceTexture).isNotNull();
+ assertThat(latestSurfaceTexture).isNotEqualTo(initialOutput);
+ }
+}
diff --git a/camera/core/src/test/java/androidx/camera/core/ExifRobolectricTest.java b/camera/core/src/test/java/androidx/camera/core/ExifRobolectricTest.java
new file mode 100644
index 0000000..49a0781
--- /dev/null
+++ b/camera/core/src/test/java/androidx/camera/core/ExifRobolectricTest.java
@@ -0,0 +1,254 @@
+/*
+ * Copyright (C) 2019 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.core;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.location.Location;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.internal.DoNotInstrument;
+import org.robolectric.shadows.ShadowLog;
+import org.robolectric.shadows.ShadowSystemClock;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+@RunWith(RobolectricTestRunner.class)
+@DoNotInstrument
+public class ExifRobolectricTest {
+ private static final InputStream FAKE_INPUT_STREAM =
+ new InputStream() {
+ @Override
+ public int read() throws IOException {
+ return 0;
+ }
+ };
+ private Exif exif;
+
+ @Before
+ public void setup() throws Exception {
+ ShadowLog.stream = System.out;
+ exif = Exif.createFromInputStream(FAKE_INPUT_STREAM);
+ }
+
+ @Test
+ public void defaultsAreExpectedValues() {
+ assertThat(exif.getRotation()).isEqualTo(0);
+ assertThat(exif.isFlippedHorizontally()).isFalse();
+ assertThat(exif.isFlippedVertically()).isFalse();
+ assertThat(exif.getTimestamp()).isEqualTo(Exif.INVALID_TIMESTAMP);
+ assertThat(exif.getLocation()).isNull();
+ assertThat(exif.getDescription()).isNull();
+ }
+
+ @Test
+ public void rotateProducesCorrectRotation() {
+ assertThat(exif.getRotation()).isEqualTo(0);
+ exif.rotate(90);
+ assertThat(exif.getRotation()).isEqualTo(90);
+ exif.rotate(90);
+ assertThat(exif.getRotation()).isEqualTo(180);
+ exif.rotate(90);
+ assertThat(exif.getRotation()).isEqualTo(270);
+ exif.rotate(90);
+ assertThat(exif.getRotation()).isEqualTo(0);
+ exif.rotate(-90);
+ assertThat(exif.getRotation()).isEqualTo(270);
+ exif.rotate(360);
+ assertThat(exif.getRotation()).isEqualTo(270);
+ exif.rotate(500 * 360 - 90);
+ assertThat(exif.getRotation()).isEqualTo(180);
+ }
+
+ @Test
+ public void flipHorizontallyWillToggle() {
+ assertThat(exif.isFlippedHorizontally()).isFalse();
+ exif.flipHorizontally();
+ assertThat(exif.isFlippedHorizontally()).isTrue();
+ exif.flipHorizontally();
+ assertThat(exif.isFlippedHorizontally()).isFalse();
+ }
+
+ @Test
+ public void flipVerticallyWillToggle() {
+ assertThat(exif.isFlippedVertically()).isFalse();
+ exif.flipVertically();
+ assertThat(exif.isFlippedVertically()).isTrue();
+ exif.flipVertically();
+ assertThat(exif.isFlippedVertically()).isFalse();
+ }
+
+ @Test
+ public void flipAndRotateUpdatesHorizontalAndVerticalFlippedState() {
+ assertThat(exif.getRotation()).isEqualTo(0);
+ assertThat(exif.isFlippedHorizontally()).isFalse();
+ assertThat(exif.isFlippedVertically()).isFalse();
+
+ exif.rotate(-90);
+ assertThat(exif.getRotation()).isEqualTo(270);
+
+ exif.flipHorizontally();
+ assertThat(exif.getRotation()).isEqualTo(90);
+ assertThat(exif.isFlippedVertically()).isTrue();
+
+ exif.flipVertically();
+ assertThat(exif.getRotation()).isEqualTo(90);
+ assertThat(exif.isFlippedHorizontally()).isFalse();
+ assertThat(exif.isFlippedVertically()).isFalse();
+
+ exif.rotate(90);
+ assertThat(exif.getRotation()).isEqualTo(180);
+
+ exif.flipVertically();
+ assertThat(exif.getRotation()).isEqualTo(0);
+ assertThat(exif.isFlippedHorizontally()).isTrue();
+ assertThat(exif.isFlippedVertically()).isFalse();
+
+ exif.flipHorizontally();
+ assertThat(exif.getRotation()).isEqualTo(0);
+ assertThat(exif.isFlippedHorizontally()).isFalse();
+ assertThat(exif.isFlippedVertically()).isFalse();
+ }
+
+ @Test
+ public void timestampCanBeAttachedAndRemoved() {
+ assertThat(exif.getTimestamp()).isEqualTo(Exif.INVALID_TIMESTAMP);
+
+ exif.attachTimestamp();
+ assertThat(exif.getTimestamp()).isNotEqualTo(Exif.INVALID_TIMESTAMP);
+
+ exif.removeTimestamp();
+ assertThat(exif.getTimestamp()).isEqualTo(Exif.INVALID_TIMESTAMP);
+ }
+
+ @Test
+ public void attachedTimestampUsesSystemWallTime() {
+ long beforeTimestamp = System.currentTimeMillis();
+
+ // The Exif class is instrumented since it's in the androidx.* namespace.
+ // Set the ShadowSystemClock to match the real system clock.
+ ShadowSystemClock.setNanoTime(System.currentTimeMillis() * 1000 * 1000);
+ exif.attachTimestamp();
+ long afterTimestamp = System.currentTimeMillis();
+
+ // Check that the attached timestamp is in the closed range [beforeTimestamp,
+ // afterTimestamp].
+ long attachedTimestamp = exif.getTimestamp();
+ assertThat(attachedTimestamp).isAtLeast(beforeTimestamp);
+ assertThat(attachedTimestamp).isAtMost(afterTimestamp);
+ }
+
+ @Test
+ public void locationCanBeAttachedAndRemoved() {
+ assertThat(exif.getLocation()).isNull();
+
+ Location location = new Location("TEST");
+ location.setLatitude(22.3);
+ location.setLongitude(114);
+ location.setTime(System.currentTimeMillis() / 1000 * 1000);
+ exif.attachLocation(location);
+ assertThat(location.toString()).isEqualTo(exif.getLocation().toString());
+
+ exif.removeLocation();
+ assertThat(exif.getLocation()).isNull();
+ }
+
+ @Test
+ public void locationWithAltitudeCanBeAttached() {
+ Location location = new Location("TEST");
+ location.setLatitude(22.3);
+ location.setLongitude(114);
+ location.setTime(System.currentTimeMillis() / 1000 * 1000);
+ location.setAltitude(5.0);
+ exif.attachLocation(location);
+ assertThat(location.toString()).isEqualTo(exif.getLocation().toString());
+ }
+
+ @Test
+ public void locationWithSpeedCanBeAttached() {
+ Location location = new Location("TEST");
+ location.setLatitude(22.3);
+ location.setLongitude(114);
+ location.setTime(System.currentTimeMillis() / 1000 * 1000);
+ location.setSpeed(5.0f);
+ exif.attachLocation(location);
+ // Location loses precision when set through attachLocation(), so check the locations are
+ // roughly equal first
+ Location exifLocation = exif.getLocation();
+ assertThat(location.getSpeed()).isWithin(0.01f).of(exifLocation.getSpeed());
+
+ // Remove speed and compare the rest by string
+ exifLocation.removeSpeed();
+ location.removeSpeed();
+ assertThat(location.toString()).isEqualTo(exifLocation.toString());
+ }
+
+ @Test
+ public void descriptionCanBeAttachedAndRemoved() {
+ assertThat(exif.getDescription()).isNull();
+
+ exif.setDescription("Hello World");
+ assertThat(exif.getDescription()).isEqualTo("Hello World");
+
+ exif.setDescription(null);
+ assertThat(exif.getDescription()).isNull();
+ }
+
+ @Test
+ public void saveUpdatesLastModifiedTimestampUnlessRemoved() {
+ assertThat(exif.getLastModifiedTimestamp()).isEqualTo(Exif.INVALID_TIMESTAMP);
+
+ try {
+ exif.save();
+ } catch (IOException e) {
+ // expected
+ }
+
+ assertThat(exif.getLastModifiedTimestamp()).isNotEqualTo(Exif.INVALID_TIMESTAMP);
+
+ // removeTimestamp should also be clearing the last modified timestamp
+ exif.removeTimestamp();
+ assertThat(exif.getLastModifiedTimestamp()).isEqualTo(Exif.INVALID_TIMESTAMP);
+
+ // Even when saving again
+ try {
+ exif.save();
+ } catch (IOException e) {
+ // expected
+ }
+
+ assertThat(exif.getLastModifiedTimestamp()).isEqualTo(Exif.INVALID_TIMESTAMP);
+ }
+
+ @Test
+ public void toStringProducesNonNullString() {
+ assertThat(exif.toString()).isNotNull();
+ exif.setDescription("Hello World");
+ exif.attachTimestamp();
+ Location location = new Location("TEST");
+ location.setLatitude(22.3);
+ location.setLongitude(114);
+ location.setTime(System.currentTimeMillis() / 1000 * 1000);
+ location.setAltitude(5.0);
+ exif.attachLocation(location);
+ assertThat(exif.toString()).isNotNull();
+ }
+}
diff --git a/camera/core/src/test/java/androidx/camera/core/ExtendableUseCaseConfigFactoryRobolectricTest.java b/camera/core/src/test/java/androidx/camera/core/ExtendableUseCaseConfigFactoryRobolectricTest.java
new file mode 100644
index 0000000..5b81226
--- /dev/null
+++ b/camera/core/src/test/java/androidx/camera/core/ExtendableUseCaseConfigFactoryRobolectricTest.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 2019 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.core;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.internal.DoNotInstrument;
+
+@RunWith(RobolectricTestRunner.class)
+@DoNotInstrument
+public class ExtendableUseCaseConfigFactoryRobolectricTest {
+
+ private ExtendableUseCaseConfigFactory factory;
+
+ @Before
+ public void setUp() {
+ factory = new ExtendableUseCaseConfigFactory();
+ }
+
+ @Test
+ public void canInstallProvider_andRetrieveConfig() {
+ factory.installDefaultProvider(
+ FakeUseCaseConfiguration.class, new FakeUseCaseConfigurationProvider());
+
+ FakeUseCaseConfiguration config = factory.getConfiguration(FakeUseCaseConfiguration.class);
+ assertThat(config).isNotNull();
+ assertThat(config.getTargetClass(null)).isEqualTo(FakeUseCase.class);
+ }
+
+ private static class FakeUseCaseConfigurationProvider
+ implements ConfigurationProvider<FakeUseCaseConfiguration> {
+
+ @Override
+ public FakeUseCaseConfiguration getConfiguration() {
+ return new FakeUseCaseConfiguration.Builder().build();
+ }
+ }
+}
diff --git a/camera/core/src/test/java/androidx/camera/core/MutableOptionsBundleRobolectricTest.java b/camera/core/src/test/java/androidx/camera/core/MutableOptionsBundleRobolectricTest.java
new file mode 100644
index 0000000..a125b7a
--- /dev/null
+++ b/camera/core/src/test/java/androidx/camera/core/MutableOptionsBundleRobolectricTest.java
@@ -0,0 +1,80 @@
+/*
+ * Copyright (C) 2019 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.core;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import androidx.camera.core.Configuration.Option;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.internal.DoNotInstrument;
+
+@RunWith(RobolectricTestRunner.class)
+@DoNotInstrument
+public class MutableOptionsBundleRobolectricTest {
+
+ private static final Option<Object> OPTION_1 = Option.create("option.1", Object.class);
+ private static final Option<Object> OPTION_1_A = Option.create("option.1.a", Object.class);
+ private static final Option<Object> OPTION_2 = Option.create("option.2", Object.class);
+
+ private static final Object VALUE_1 = new Object();
+ private static final Object VALUE_1_A = new Object();
+ private static final Object VALUE_2 = new Object();
+ private static final Object VALUE_MISSING = new Object();
+
+ @Test
+ public void canCreateEmptyBundle() {
+ MutableOptionsBundle bundle = MutableOptionsBundle.create();
+ assertThat(bundle).isNotNull();
+ }
+
+ @Test
+ public void canAddValue() {
+ MutableOptionsBundle bundle = MutableOptionsBundle.create();
+ bundle.insertOption(OPTION_1, VALUE_1);
+
+ assertThat(bundle.retrieveOption(OPTION_1, VALUE_MISSING)).isSameAs(VALUE_1);
+ }
+
+ @Test
+ public void canRemoveValue() {
+ MutableOptionsBundle bundle = MutableOptionsBundle.create();
+ bundle.insertOption(OPTION_1, VALUE_1);
+ bundle.removeOption(OPTION_1);
+
+ assertThat(bundle.retrieveOption(OPTION_1, VALUE_MISSING)).isSameAs(VALUE_MISSING);
+ }
+
+ @Test
+ public void canCreateFromConfiguration_andAddMore() {
+ MutableOptionsBundle mutOpts = MutableOptionsBundle.create();
+ mutOpts.insertOption(OPTION_1, VALUE_1);
+ mutOpts.insertOption(OPTION_1_A, VALUE_1_A);
+
+ Configuration config = OptionsBundle.from(mutOpts);
+
+ MutableOptionsBundle mutOpts2 = MutableOptionsBundle.from(config);
+ mutOpts2.insertOption(OPTION_2, VALUE_2);
+
+ Configuration config2 = OptionsBundle.from(mutOpts2);
+
+ assertThat(config.listOptions()).containsExactly(OPTION_1, OPTION_1_A);
+ assertThat(config2.listOptions()).containsExactly(OPTION_1, OPTION_1_A, OPTION_2);
+ }
+}
diff --git a/camera/core/src/test/java/androidx/camera/core/OptionRobolectricTest.java b/camera/core/src/test/java/androidx/camera/core/OptionRobolectricTest.java
new file mode 100644
index 0000000..33358a8
--- /dev/null
+++ b/camera/core/src/test/java/androidx/camera/core/OptionRobolectricTest.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright (C) 2019 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.core;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import androidx.camera.core.Configuration.Option;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.internal.DoNotInstrument;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+@RunWith(RobolectricTestRunner.class)
+@DoNotInstrument
+public class OptionRobolectricTest {
+
+ private static final String OPTION_1_ID = "option.1";
+
+ private static final Object TOKEN = new Object();
+
+ @Test
+ public void canCreateOption_andRetrieveId() {
+ Option<Integer> option = Option.create(OPTION_1_ID, Integer.class);
+ assertThat(option.getId()).isEqualTo(OPTION_1_ID);
+ }
+
+ @Test
+ public void canCreateOption_fromClass_andRetrieveClass() {
+ Option<Integer> option = Option.create(OPTION_1_ID, Integer.class);
+ assertThat(option.getValueClass()).isEqualTo(Integer.class);
+ }
+
+ @Test
+ public void canCreateOption_fromPrimitiveClass_andRetrievePrimitiveClass() {
+ Option<Integer> option = Option.create(OPTION_1_ID, int.class);
+ assertThat(option.getValueClass()).isEqualTo(int.class);
+ }
+
+ @Test
+ public void canCreateOption_fromTypeReference() {
+ Option<List<Integer>> option =
+ Option.create(OPTION_1_ID, new TypeReference<List<Integer>>() {
+ });
+ assertThat(option).isNotNull();
+ }
+
+ @Test
+ public void canCreateOption_withNullToken() {
+ Option<Integer> option = Option.create(OPTION_1_ID, Integer.class);
+ assertThat(option.getToken()).isNull();
+ }
+
+ @Test
+ public void canCreateOption_withToken() {
+ Option<Integer> option = Option.create(OPTION_1_ID, Integer.class, TOKEN);
+ assertThat(option.getToken()).isSameAs(TOKEN);
+ }
+
+ @Test
+ public void canRetrieveOption_fromMap_usingSeparateOptionInstances() {
+ Option<Integer> option = Option.create(OPTION_1_ID, Integer.class);
+ Option<Integer> optionCopy = Option.create(OPTION_1_ID, Integer.class);
+
+ Map<Option<?>, Object> map = new HashMap<>();
+ map.put(option, 1);
+
+ assertThat(map).containsKey(optionCopy);
+ assertThat(map.get(optionCopy)).isEqualTo(1);
+ }
+}
diff --git a/camera/core/src/test/java/androidx/camera/core/OptionsBundleRobolectricTest.java b/camera/core/src/test/java/androidx/camera/core/OptionsBundleRobolectricTest.java
new file mode 100644
index 0000000..9cc5690
--- /dev/null
+++ b/camera/core/src/test/java/androidx/camera/core/OptionsBundleRobolectricTest.java
@@ -0,0 +1,157 @@
+/*
+ * Copyright (C) 2019 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.core;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+
+import androidx.camera.core.Configuration.Option;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.internal.DoNotInstrument;
+
+import java.util.Set;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicReference;
+
+@RunWith(RobolectricTestRunner.class)
+@DoNotInstrument
+public class OptionsBundleRobolectricTest {
+
+ private static final Option<Object> OPTION_1 = Option.create("option.1", Object.class);
+ private static final Option<Object> OPTION_1_A = Option.create("option.1.a", Object.class);
+ private static final Option<Object> OPTION_2 = Option.create("option.2", Object.class);
+ private static final Option<Object> OPTION_MISSING =
+ Option.create("option.missing", Object.class);
+
+ private static final Object VALUE_1 = new Object();
+ private static final Object VALUE_1_A = new Object();
+ private static final Object VALUE_2 = new Object();
+ private static final Object VALUE_MISSING = new Object();
+
+ private OptionsBundle allOpts;
+
+ @Before
+ public void setUp() {
+ MutableOptionsBundle mutOpts = MutableOptionsBundle.create();
+ mutOpts.insertOption(OPTION_1, VALUE_1);
+ mutOpts.insertOption(OPTION_1_A, VALUE_1_A);
+ mutOpts.insertOption(OPTION_2, VALUE_2);
+
+ allOpts = OptionsBundle.from(mutOpts);
+ }
+
+ @Test
+ public void canRetrieveValue() {
+ assertThat(allOpts.retrieveOption(OPTION_1)).isSameAs(VALUE_1);
+ assertThat(allOpts.retrieveOption(OPTION_1_A)).isSameAs(VALUE_1_A);
+ assertThat(allOpts.retrieveOption(OPTION_2)).isSameAs(VALUE_2);
+ }
+
+ @Test
+ public void willReturnDefault_ifOptionIsMissing() {
+ Object value = allOpts.retrieveOption(OPTION_MISSING, VALUE_MISSING);
+ assertThat(value).isSameAs(VALUE_MISSING);
+ }
+
+ @Test
+ public void willReturnStoredValue_whenGivenDefault() {
+ Object value = allOpts.retrieveOption(OPTION_1, VALUE_MISSING);
+ assertThat(value).isSameAs(VALUE_1);
+ }
+
+ @Test
+ public void canListOptions() {
+ Set<Option<?>> list = allOpts.listOptions();
+ for (Option<?> opt : list) {
+ assertThat(opt).isAnyOf(OPTION_1, OPTION_1_A, OPTION_2);
+ }
+
+ assertThat(list).hasSize(3);
+ }
+
+ @Test
+ public void canCreateCopyOptionsBundle() {
+ OptionsBundle copyBundle = OptionsBundle.from(allOpts);
+
+ assertThat(copyBundle.containsOption(OPTION_1)).isTrue();
+ assertThat(copyBundle.containsOption(OPTION_1_A)).isTrue();
+ assertThat(copyBundle.containsOption(OPTION_2)).isTrue();
+ }
+
+ @Test
+ public void canFindPartialIds() {
+ allOpts.findOptions(
+ "option.1",
+ option -> {
+ assertThat(option).isAnyOf(OPTION_1, OPTION_1_A);
+ return true;
+ });
+ }
+
+ @Test
+ public void canStopSearchingAfterFirstMatch() {
+ AtomicInteger count = new AtomicInteger();
+ allOpts.findOptions(
+ "option",
+ option -> {
+ count.getAndIncrement();
+ return false;
+ });
+
+ assertThat(count.get()).isEqualTo(1);
+ }
+
+ @Test
+ public void canGetZeroResults_fromFind() {
+ AtomicInteger count = new AtomicInteger();
+ allOpts.findOptions(
+ "invalid_find_string",
+ option -> {
+ count.getAndIncrement();
+ return false;
+ });
+
+ assertThat(count.get()).isEqualTo(0);
+ }
+
+ @Test
+ public void canRetrieveValue_fromFindLambda() {
+ AtomicReference<Object> value = new AtomicReference<>(VALUE_MISSING);
+ allOpts.findOptions(
+ "option.2",
+ option -> {
+ value.set(allOpts.retrieveOption(option));
+ return true;
+ });
+
+ assertThat(value.get()).isSameAs(VALUE_2);
+ }
+
+ @Test
+ public void retrieveMissingOption_willThrow() {
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> {
+ allOpts.retrieveOption(OPTION_MISSING);
+ });
+ }
+}
diff --git a/camera/gradle.properties b/camera/gradle.properties
new file mode 100644
index 0000000..c0384c8
--- /dev/null
+++ b/camera/gradle.properties
@@ -0,0 +1,8 @@
+# Improve build performance
+org.gradle.jvmargs=-Xmx6g -XX:ReservedCodeCacheSize=2g -Dfile.encoding=UTF-8
+org.gradle.parallel=true
+org.gradle.configureondemand=true
+org.gradle.caching=true
+# For offline storage of dependencies. This string should be considered a child
+# directory of the main directory defined by ${rootProject.projectDir}.
+offlineRepositoryRoot=offline-repository
diff --git a/camera/integration-tests/camera2interopburst/build.gradle b/camera/integration-tests/camera2interopburst/build.gradle
new file mode 100644
index 0000000..b4227dd5
--- /dev/null
+++ b/camera/integration-tests/camera2interopburst/build.gradle
@@ -0,0 +1,79 @@
+/*
+ * Copyright (C) 2019 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.
+ */
+
+apply plugin: 'com.android.application'
+
+android {
+ compileOptions {
+ sourceCompatibility project.ext.javaVersion
+ targetCompatibility project.ext.javaVersion
+ }
+
+ compileSdkVersion project.ext.compileSdk
+
+ defaultConfig {
+ applicationId "androidx.camera.testapp.camera2interopburst"
+ minSdkVersion project.ext.minSdk
+ targetSdkVersion project.ext.targetSdk
+ versionCode 1
+ versionName project.ext.version
+ multiDexEnabled true
+ }
+
+ sourceSets {
+ main.manifest.srcFile 'src/main/AndroidManifest.xml'
+ main.java.srcDirs = ['src/main/java']
+ main.java.excludes = ['**/build/**']
+ main.java.includes = ['**/*.java']
+ main.res.srcDirs = ['src/main/res']
+ }
+
+ buildTypes {
+ debug {
+ testCoverageEnabled true
+ }
+
+ release {
+ minifyEnabled true
+ proguardFiles getDefaultProguardFile('proguard-android.txt')
+ }
+ }
+
+ lintOptions {
+ abortOnError false
+ }
+}
+
+dependencies {
+ // Internal library
+ implementation project(':camera2')
+ implementation project(':core')
+
+ // Lifecycle and LiveData
+ implementation "android.arch.lifecycle:extensions:${project.ext.lifecycleVersion}"
+ implementation "android.arch.lifecycle:livedata:${project.ext.lifecycleVersion}"
+ implementation "android.arch.lifecycle:runtime:${project.ext.lifecycleVersion}"
+ implementation "android.arch.lifecycle:common-java8:${project.ext.lifecycleVersion}"
+
+ // Android Support Library
+ implementation "com.android.support:appcompat-v7:${project.ext.supportLibraryVersion}"
+ implementation "com.android.support.constraint:constraint-layout:1.0.2"
+ implementation "com.android.support:support-compat:${project.ext.supportLibraryVersion}"
+ implementation "com.android.support:support-annotations:${project.ext.supportLibraryVersion}"
+ implementation "com.android.support:support-v13:${project.ext.supportLibraryVersion}"
+}
+
diff --git a/camera/integration-tests/camera2interopburst/src/main/AndroidManifest.xml b/camera/integration-tests/camera2interopburst/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..ade8c53
--- /dev/null
+++ b/camera/integration-tests/camera2interopburst/src/main/AndroidManifest.xml
@@ -0,0 +1,43 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2019 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.
+-->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="androidx.camera.testapp.camera2interopburst">
+
+ <uses-sdk
+ android:minSdkVersion="21"
+ android:targetSdkVersion="27" />
+
+ <uses-permission android:name="android.permission.CAMERA" />
+
+ <uses-feature android:name="android.hardware.camera" />
+
+ <application
+ android:allowBackup="true"
+ android:label="@string/app_name"
+ android:theme="@style/Theme.AppCompat">
+
+ <activity
+ android:name=".MainActivity"
+ android:label="@string/app_name"
+ android:screenOrientation="portrait">
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN" />
+ <category android:name="android.intent.category.LAUNCHER" />
+ </intent-filter>
+ </activity>
+ </application>
+
+</manifest>
diff --git a/camera/integration-tests/camera2interopburst/src/main/java/androidx/camera/testapp/camera2interopburst/MainActivity.java b/camera/integration-tests/camera2interopburst/src/main/java/androidx/camera/testapp/camera2interopburst/MainActivity.java
new file mode 100644
index 0000000..451221b
--- /dev/null
+++ b/camera/integration-tests/camera2interopburst/src/main/java/androidx/camera/testapp/camera2interopburst/MainActivity.java
@@ -0,0 +1,359 @@
+/*
+ * Copyright (C) 2019 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.testapp.camera2interopburst;
+
+import android.Manifest;
+import android.app.Activity;
+import android.arch.lifecycle.LifecycleOwner;
+import android.arch.lifecycle.MutableLiveData;
+import android.content.pm.PackageManager;
+import android.graphics.Bitmap;
+import android.graphics.Color;
+import android.hardware.camera2.CameraAccessException;
+import android.hardware.camera2.CameraCaptureSession;
+import android.hardware.camera2.CaptureRequest;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.support.annotation.GuardedBy;
+import android.support.annotation.Nullable;
+import android.support.v4.app.ActivityCompat;
+import android.support.v4.content.ContextCompat;
+import android.support.v7.app.AppCompatActivity;
+import android.util.Log;
+import android.view.MotionEvent;
+import android.view.TextureView;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ImageView;
+
+import androidx.camera.camera2.Camera2Configuration;
+import androidx.camera.core.CameraX;
+import androidx.camera.core.ImageAnalysisUseCase;
+import androidx.camera.core.ImageAnalysisUseCaseConfiguration;
+import androidx.camera.core.ImageProxy;
+import androidx.camera.core.ViewFinderUseCase;
+import androidx.camera.core.ViewFinderUseCaseConfiguration;
+
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutionException;
+
+/**
+ * An activity using CameraX-Camera2 interop to capture a burst.
+ *
+ * <p>First, the activity uses CameraX to set up a ViewFinderUseCase and ImageAnalysisUseCase. The
+ * ImageAnalysisUseCase converts Image instances into Bitmap instances. During the setup, custom
+ * CameraCaptureSession.StateCallback and CameraCaptureSession.CaptureCallback instances are passed
+ * to CameraX. These callbacks enable the activity to get references to the CameraCaptureSession and
+ * repeating CaptureRequest created internally by CameraX.
+ *
+ * <p>Then, when the user clicks on the viewfinder, CameraX's repeating request is stopped and a new
+ * burst capture request is issued, using the active CameraCaptureSession still owned by CameraX.
+ * The images captured during the burst are shown in an overlay mosaic for visualization.
+ *
+ * <p>Finally, after the burst capture concludes, CameraX's previous repeating request is resumed,
+ * until the next time the user starts a burst.
+ */
+@SuppressWarnings("AndroidJdkLibsChecker") // CompletableFuture not generally available yet.
+public class MainActivity extends AppCompatActivity {
+ private static final String TAG = MainActivity.class.getName();
+ private static final int CAMERA_REQUEST_CODE = 101;
+ private static final int BURST_FRAME_COUNT = 30;
+ private static final int MOSAIC_ROWS = 3;
+ private static final int MOSAIC_COLS = BURST_FRAME_COUNT / MOSAIC_ROWS;
+ // TODO: Figure out dynamically to fill the screen, instead of hard-coding.
+ private static final int TILE_WIDTH = 102;
+ private static final int TILE_HEIGHT = 72;
+ ;
+
+ // Waiting for the permissions approval.
+ private final CompletableFuture<Integer> completableFuture = new CompletableFuture<>();
+
+ // For handling touch events on the TextureView.
+ private final View.OnTouchListener onTouchListener = new OnTouchListener();
+
+ // Tracks the burst state.
+ private final Object burstLock = new Object();
+ // Camera2 interop objects.
+ private final SessionUpdatingSessionStateCallback sessionStateCallback =
+ new SessionUpdatingSessionStateCallback();
+ private final RequestUpdatingSessionCaptureCallback sessionCaptureCallback =
+ new RequestUpdatingSessionCaptureCallback();
+ private final Object mosaicLock = new Object();
+ @GuardedBy("mosaicLock")
+ private final Bitmap mosaic =
+ Bitmap.createBitmap(
+ MOSAIC_COLS * TILE_WIDTH, MOSAIC_ROWS * TILE_HEIGHT, Bitmap.Config.ARGB_8888);
+ private final MutableLiveData<Bitmap> analysisResult = new MutableLiveData<>();
+ @GuardedBy("burstLock")
+ private boolean burstInProgress = false;
+ @GuardedBy("burstLock")
+ private int burstFrameCount = 0;
+ // For visualizing the images captured in the burst.
+ private ImageView imageView;
+ // For running ops on a background thread.
+ private Handler backgroundHandler;
+ private HandlerThread backgroundHandlerThread;
+
+ private static void makePermissionRequest(Activity context) {
+ ActivityCompat.requestPermissions(
+ context, new String[]{Manifest.permission.CAMERA}, CAMERA_REQUEST_CODE);
+ }
+
+ @Override
+ protected void onCreate(@Nullable Bundle bundle) {
+ super.onCreate(bundle);
+ setContentView(R.layout.activity_main);
+
+ backgroundHandlerThread = new HandlerThread("Background");
+ backgroundHandlerThread.start();
+ backgroundHandler = new Handler(backgroundHandlerThread.getLooper());
+
+ new Thread(
+ () -> {
+ setupCamera();
+ })
+ .start();
+ setupPermissions(this);
+ }
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ backgroundHandler.removeCallbacksAndMessages(null);
+ backgroundHandlerThread.quitSafely();
+ }
+
+ @Override
+ protected void onPause() {
+ synchronized (burstLock) {
+ burstInProgress = false;
+ }
+ super.onPause();
+ }
+
+ private void setupCamera() {
+ try {
+ // Wait for permissions before proceeding.
+ if (completableFuture.get() == PackageManager.PERMISSION_DENIED) {
+ Log.e(TAG, "Permission to open camera denied.");
+ return;
+ }
+ } catch (InterruptedException | ExecutionException e) {
+ Log.e(TAG, "Exception occurred getting permission future: " + e);
+ }
+ LifecycleOwner lifecycleOwner = this;
+
+ // Run this on the UI thread to manipulate the Textures & Views.
+ MainActivity.this.runOnUiThread(
+ () -> {
+ imageView = findViewById(R.id.imageView);
+
+ ViewFinderUseCaseConfiguration.Builder viewFinderConfigBuilder =
+ new ViewFinderUseCaseConfiguration.Builder()
+ .setTargetName("ViewFinder");
+
+ new Camera2Configuration.Extender(viewFinderConfigBuilder)
+ .setSessionStateCallback(sessionStateCallback)
+ .setSessionCaptureCallback(sessionCaptureCallback);
+
+ ViewFinderUseCaseConfiguration viewFinderConfig =
+ viewFinderConfigBuilder.build();
+ TextureView textureView = findViewById(R.id.textureView);
+ textureView.setOnTouchListener(onTouchListener);
+ ViewFinderUseCase viewFinderUseCase = new ViewFinderUseCase(viewFinderConfig);
+
+ viewFinderUseCase.setOnViewFinderOutputUpdateListener(
+ output -> {
+ // If TextureView was already created, need to re-add it to change
+ // the SurfaceTexture.
+ ViewGroup v = (ViewGroup) textureView.getParent();
+ v.removeView(textureView);
+ v.addView(textureView);
+ textureView.setSurfaceTexture(output.getSurfaceTexture());
+ });
+
+ CameraX.bindToLifecycle(lifecycleOwner, viewFinderUseCase);
+
+ ImageAnalysisUseCaseConfiguration analysisConfig =
+ new ImageAnalysisUseCaseConfiguration.Builder()
+ .setTargetName("ImageAnalysis")
+ .setCallbackHandler(backgroundHandler)
+ .build();
+ ImageAnalysisUseCase analysisUseCase = new ImageAnalysisUseCase(analysisConfig);
+ CameraX.bindToLifecycle(lifecycleOwner, analysisUseCase);
+ analysisUseCase.setAnalyzer(
+ (image, rotationDegrees) -> {
+ analysisResult.postValue(convertYuv420ImageToBitmap(image));
+ });
+ analysisResult.observe(
+ lifecycleOwner,
+ bitmap -> {
+ synchronized (burstLock) {
+ if (burstInProgress) {
+ // Update the mosaic.
+ insertIntoMosaic(bitmap, burstFrameCount++);
+ MainActivity.this.runOnUiThread(
+ () -> {
+ synchronized (mosaicLock) {
+ imageView.setImageBitmap(mosaic);
+ }
+ });
+
+ // Detect the end of the burst.
+ if (burstFrameCount == BURST_FRAME_COUNT) {
+ burstInProgress = false;
+ submitRepeatingRequest();
+ }
+ }
+ }
+ });
+ });
+ }
+
+ private void setupPermissions(Activity context) {
+ int permission = ContextCompat.checkSelfPermission(context, Manifest.permission.CAMERA);
+ if (permission != PackageManager.PERMISSION_GRANTED) {
+ makePermissionRequest(context);
+ } else {
+ completableFuture.complete(permission);
+ }
+ }
+
+ @Override
+ public void onRequestPermissionsResult(
+ int requestCode, String[] permissions, int[] grantResults) {
+ switch (requestCode) {
+ case CAMERA_REQUEST_CODE: {
+ // If request is cancelled, the result arrays are empty.
+ if (grantResults.length > 0
+ && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
+ Log.i(TAG, "Camera Permission Granted.");
+ } else {
+ Log.i(TAG, "Camera Permission Denied.");
+ }
+ completableFuture.complete(grantResults[0]);
+ return;
+ }
+ default: {
+ }
+ }
+ }
+
+ private void submitCaptureBurstRequest() {
+ try {
+ // Use the existing session created by CameraX.
+ CameraCaptureSession session = sessionStateCallback.getSession();
+ // Use the previous request created by CameraX.
+ CaptureRequest request = sessionCaptureCallback.getRequest();
+ List<CaptureRequest> requests = new ArrayList<>(BURST_FRAME_COUNT);
+ for (int i = 0; i < BURST_FRAME_COUNT; ++i) {
+ requests.add(request);
+ }
+ session.captureBurst(requests, /*callback=*/ null, backgroundHandler);
+ } catch (CameraAccessException e) {
+ throw new RuntimeException("Could not submit the burst capture request.", e);
+ }
+ }
+
+ private void submitRepeatingRequest() {
+ try {
+ // Use the existing session created by CameraX.
+ CameraCaptureSession session = sessionStateCallback.getSession();
+ // Use the previous request created by CameraX.
+ CaptureRequest request = sessionCaptureCallback.getRequest();
+ // TODO: This capture callback is not the same as that used by CameraX internally.
+ // Find a way to use exactly that same callback.
+ session.setRepeatingRequest(request, sessionCaptureCallback, backgroundHandler);
+ } catch (CameraAccessException e) {
+ throw new RuntimeException("Could not submit the repeating request.", e);
+ }
+ }
+
+ // TODO: Do proper YUV420-to-RGB conversion, instead of just taking the Y channel and
+ // propagating it to all 3 channels.
+ private Bitmap convertYuv420ImageToBitmap(ImageProxy image) {
+ ImageProxy.PlaneProxy plane = image.getPlanes()[0];
+ ByteBuffer buffer = plane.getBuffer();
+ final int bytesCount = buffer.remaining();
+ byte[] imageBytes = new byte[bytesCount];
+ buffer.get(imageBytes);
+
+ // TODO: Reuse a bitmap from a pool.
+ Bitmap bitmap =
+ Bitmap.createBitmap(image.getWidth(), image.getHeight(), Bitmap.Config.ARGB_8888);
+
+ int[] bitmapPixels = new int[bitmap.getWidth() * bitmap.getHeight()];
+ for (int row = 0; row < bitmap.getHeight(); ++row) {
+ int imageBytesPosition = row * plane.getRowStride();
+ int bitmapPixelsPosition = row * bitmap.getWidth();
+ for (int col = 0; col < bitmap.getWidth(); ++col) {
+ int channelValue = (imageBytes[imageBytesPosition++] & 0xFF);
+ bitmapPixels[bitmapPixelsPosition++] =
+ Color.rgb(channelValue, channelValue, channelValue);
+ }
+ }
+ bitmap.setPixels(
+ bitmapPixels, 0, bitmap.getWidth(), 0, 0, bitmap.getWidth(), bitmap.getHeight());
+ return bitmap;
+ }
+
+ private void insertIntoMosaic(Bitmap bitmap, int position) {
+ // TODO: Reuse a bitmap from a pool.
+ Bitmap rescaledBitmap =
+ Bitmap.createScaledBitmap(bitmap, TILE_WIDTH, TILE_HEIGHT, /*filter=*/ false);
+
+ int tileRowOffset = (position / MOSAIC_COLS) * TILE_HEIGHT;
+ int tileColOffset = (position % MOSAIC_COLS) * TILE_WIDTH;
+ for (int row = 0; row < rescaledBitmap.getHeight(); ++row) {
+ for (int col = 0; col < rescaledBitmap.getWidth(); ++col) {
+ int color = rescaledBitmap.getPixel(col, row);
+ synchronized (mosaicLock) {
+ mosaic.setPixel(col + tileColOffset, row + tileRowOffset, color);
+ }
+ }
+ }
+ }
+
+ /** An on-touch listener which submits a capture burst request when the view is touched. */
+ private class OnTouchListener implements View.OnTouchListener {
+ @Override
+ public boolean onTouch(View view, MotionEvent event) {
+ if (event.getActionMasked() == MotionEvent.ACTION_UP) {
+ synchronized (burstLock) {
+ if (!burstInProgress) {
+ burstFrameCount = 0;
+ synchronized (mosaicLock) {
+ mosaic.eraseColor(0);
+ }
+ try {
+ sessionStateCallback.getSession().stopRepeating();
+ } catch (CameraAccessException e) {
+ throw new RuntimeException("Could not stop the repeating request.", e);
+ }
+ submitCaptureBurstRequest();
+ burstInProgress = true;
+ }
+ }
+ }
+ return true;
+ }
+ }
+}
diff --git a/camera/integration-tests/camera2interopburst/src/main/java/androidx/camera/testapp/camera2interopburst/RequestUpdatingSessionCaptureCallback.java b/camera/integration-tests/camera2interopburst/src/main/java/androidx/camera/testapp/camera2interopburst/RequestUpdatingSessionCaptureCallback.java
new file mode 100644
index 0000000..b548c41
--- /dev/null
+++ b/camera/integration-tests/camera2interopburst/src/main/java/androidx/camera/testapp/camera2interopburst/RequestUpdatingSessionCaptureCallback.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright (C) 2019 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.testapp.camera2interopburst;
+
+import android.hardware.camera2.CameraCaptureSession;
+import android.hardware.camera2.CaptureFailure;
+import android.hardware.camera2.CaptureRequest;
+import android.hardware.camera2.CaptureResult;
+import android.hardware.camera2.TotalCaptureResult;
+import android.view.Surface;
+
+import java.util.concurrent.atomic.AtomicReference;
+
+/** A capture session capture callback which updates a reference to the capture request. */
+final class RequestUpdatingSessionCaptureCallback extends CameraCaptureSession.CaptureCallback {
+ private final AtomicReference<CaptureRequest> request = new AtomicReference<>();
+
+ @Override
+ public void onCaptureBufferLost(
+ CameraCaptureSession session, CaptureRequest request, Surface surface, long frame) {
+ }
+
+ @Override
+ public void onCaptureCompleted(
+ CameraCaptureSession session, CaptureRequest request, TotalCaptureResult result) {
+ }
+
+ @Override
+ public void onCaptureFailed(
+ CameraCaptureSession session, CaptureRequest request, CaptureFailure failure) {
+ }
+
+ @Override
+ public void onCaptureProgressed(
+ CameraCaptureSession session, CaptureRequest request, CaptureResult partialResult) {
+ }
+
+ @Override
+ public void onCaptureSequenceAborted(CameraCaptureSession session, int sequenceId) {
+ }
+
+ @Override
+ public void onCaptureSequenceCompleted(
+ CameraCaptureSession session, int sequenceId, long frame) {
+ }
+
+ @Override
+ public void onCaptureStarted(
+ CameraCaptureSession session, CaptureRequest request, long timestamp, long frame) {
+ this.request.set(request);
+ }
+
+ CaptureRequest getRequest() {
+ return request.get();
+ }
+}
diff --git a/camera/integration-tests/camera2interopburst/src/main/java/androidx/camera/testapp/camera2interopburst/SessionUpdatingSessionStateCallback.java b/camera/integration-tests/camera2interopburst/src/main/java/androidx/camera/testapp/camera2interopburst/SessionUpdatingSessionStateCallback.java
new file mode 100644
index 0000000..3094c91
--- /dev/null
+++ b/camera/integration-tests/camera2interopburst/src/main/java/androidx/camera/testapp/camera2interopburst/SessionUpdatingSessionStateCallback.java
@@ -0,0 +1,86 @@
+/*
+ * Copyright (C) 2019 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.testapp.camera2interopburst;
+
+import android.hardware.camera2.CameraCaptureSession;
+import android.support.annotation.GuardedBy;
+import android.util.Log;
+import android.view.Surface;
+
+/** A capture session state callback which updates a reference to the capture session. */
+final class SessionUpdatingSessionStateCallback extends CameraCaptureSession.StateCallback {
+ private static final String TAG = "SessionUpdatingSessionStateCallback";
+
+ private final Object sessionLock = new Object();
+
+ @GuardedBy("sessionLock")
+ private CameraCaptureSession session;
+
+ @Override
+ public void onConfigured(CameraCaptureSession session) {
+ Log.d(TAG, "onConfigured: session=" + session);
+ synchronized (sessionLock) {
+ this.session = session;
+ }
+ }
+
+ @Override
+ public void onActive(CameraCaptureSession session) {
+ Log.d(TAG, "onActive: session=" + session);
+ }
+
+ @Override
+ public void onClosed(CameraCaptureSession session) {
+ Log.d(TAG, "onClosed: session=" + session);
+ synchronized (sessionLock) {
+ if (this.session == session) {
+ this.session = null;
+ }
+ }
+ }
+
+ @Override
+ public void onReady(CameraCaptureSession session) {
+ Log.d(TAG, "onReady: session=" + session);
+ }
+
+ @Override
+ public void onCaptureQueueEmpty(CameraCaptureSession session) {
+ Log.d(TAG, "onCaptureQueueEmpty: session=" + session);
+ }
+
+ @Override
+ public void onSurfacePrepared(CameraCaptureSession session, Surface surface) {
+ Log.d(TAG, "onSurfacePrepared: session=" + session + ", surface=" + surface);
+ }
+
+ @Override
+ public void onConfigureFailed(CameraCaptureSession session) {
+ Log.d(TAG, "onConfigureFailed: session=" + session);
+ synchronized (sessionLock) {
+ if (this.session == session) {
+ this.session = null;
+ }
+ }
+ }
+
+ CameraCaptureSession getSession() {
+ synchronized (sessionLock) {
+ return session;
+ }
+ }
+}
diff --git a/camera/integration-tests/camera2interopburst/src/main/res/layout/activity_main.xml b/camera/integration-tests/camera2interopburst/src/main/res/layout/activity_main.xml
new file mode 100644
index 0000000..6c5b897
--- /dev/null
+++ b/camera/integration-tests/camera2interopburst/src/main/res/layout/activity_main.xml
@@ -0,0 +1,44 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2019 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.
+-->
+<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ app:layout_constraintHeight_min="640dp"
+ tools:context="androidx.camera.app.camera2interopburst.MainActivity">
+
+ <RelativeLayout
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical">
+
+ <TextureView
+ android:id="@+id/textureView"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:minHeight="177dp" />
+
+ <ImageView
+ android:id="@+id/imageView"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginLeft="0dp"
+ android:layout_marginTop="0dp"
+ android:elevation="2dp" />
+ </RelativeLayout>
+
+</android.support.constraint.ConstraintLayout>
diff --git a/camera/integration-tests/camera2interopburst/src/main/res/values/strings.xml b/camera/integration-tests/camera2interopburst/src/main/res/values/strings.xml
new file mode 100644
index 0000000..ff582fd
--- /dev/null
+++ b/camera/integration-tests/camera2interopburst/src/main/res/values/strings.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2019 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.
+-->
+<resources>
+ <string name="app_name">Camera2 Interop Burst</string>
+</resources>
diff --git a/camera/integration-tests/camera2interoperror/build.gradle b/camera/integration-tests/camera2interoperror/build.gradle
new file mode 100644
index 0000000..15c740d
--- /dev/null
+++ b/camera/integration-tests/camera2interoperror/build.gradle
@@ -0,0 +1,79 @@
+/*
+ * Copyright (C) 2019 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.
+ */
+
+apply plugin: 'com.android.application'
+
+android {
+ compileOptions {
+ sourceCompatibility project.ext.javaVersion
+ targetCompatibility project.ext.javaVersion
+ }
+
+ compileSdkVersion project.ext.compileSdk
+
+ defaultConfig {
+ applicationId "androidx.camera.testapp.camera2interoperror"
+ minSdkVersion project.ext.minSdk
+ targetSdkVersion project.ext.targetSdk
+ versionCode 1
+ versionName project.ext.version
+ multiDexEnabled true
+ }
+
+ sourceSets {
+ main.manifest.srcFile 'src/main/AndroidManifest.xml'
+ main.java.srcDirs = ['src/main/java']
+ main.java.excludes = ['**/build/**']
+ main.java.includes = ['**/*.java']
+ main.res.srcDirs = ['src/main/res']
+ }
+
+ buildTypes {
+ debug {
+ testCoverageEnabled true
+ }
+
+ release {
+ minifyEnabled true
+ proguardFiles getDefaultProguardFile('proguard-android.txt')
+ }
+ }
+
+ lintOptions {
+ abortOnError false
+ }
+}
+
+dependencies {
+ // Internal library
+ implementation project(':camera2')
+ implementation project(':core')
+
+ // Lifecycle and LiveData
+ implementation "android.arch.lifecycle:extensions:${project.ext.lifecycleVersion}"
+ implementation "android.arch.lifecycle:livedata:${project.ext.lifecycleVersion}"
+ implementation "android.arch.lifecycle:runtime:${project.ext.lifecycleVersion}"
+ implementation "android.arch.lifecycle:common-java8:${project.ext.lifecycleVersion}"
+
+ // Android Support Library
+ implementation "com.android.support:appcompat-v7:${project.ext.supportLibraryVersion}"
+ implementation "com.android.support.constraint:constraint-layout:1.0.2"
+ implementation "com.android.support:support-compat:${project.ext.supportLibraryVersion}"
+ implementation "com.android.support:support-annotations:${project.ext.supportLibraryVersion}"
+ implementation "com.android.support:support-v13:${project.ext.supportLibraryVersion}"
+}
+
diff --git a/camera/integration-tests/camera2interoperror/src/main/AndroidManifest.xml b/camera/integration-tests/camera2interoperror/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..94d0029
--- /dev/null
+++ b/camera/integration-tests/camera2interoperror/src/main/AndroidManifest.xml
@@ -0,0 +1,38 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2019 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.
+-->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="androidx.camera.testapp.camera2interoperror">
+
+ <uses-sdk
+ android:minSdkVersion="21"
+ android:targetSdkVersion="27" />
+
+ <uses-permission android:name="android.permission.CAMERA" />
+ <uses-feature android:name="android.hardware.camera" />
+
+ <application
+ android:name=".CameraXInteropErrorApplication"
+ android:theme="@style/AppTheme">
+ <activity
+ android:name=".CameraXInteropErrorActivity"
+ android:label="CameraX Camera2InteropError">
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN" />
+ <category android:name="android.intent.category.LAUNCHER" />
+ </intent-filter>
+ </activity>
+ </application>
+</manifest>
diff --git a/camera/integration-tests/camera2interoperror/src/main/java/androidx/camera/testapp/camera2interoperror/Camera2InteropErrorUseCase.java b/camera/integration-tests/camera2interoperror/src/main/java/androidx/camera/testapp/camera2interoperror/Camera2InteropErrorUseCase.java
new file mode 100644
index 0000000..c6374bb
--- /dev/null
+++ b/camera/integration-tests/camera2interoperror/src/main/java/androidx/camera/testapp/camera2interoperror/Camera2InteropErrorUseCase.java
@@ -0,0 +1,130 @@
+/*
+ * Copyright (C) 2019 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.testapp.camera2interoperror;
+
+import android.graphics.ImageFormat;
+import android.hardware.camera2.CameraAccessException;
+import android.hardware.camera2.CameraCaptureSession;
+import android.hardware.camera2.CameraDevice;
+import android.hardware.camera2.CameraDevice.StateCallback;
+import android.media.ImageReader;
+import android.support.annotation.NonNull;
+import android.util.Log;
+import android.util.Size;
+
+import androidx.camera.core.BaseUseCase;
+import androidx.camera.core.CameraX;
+import androidx.camera.core.SessionConfiguration;
+
+import java.util.Collections;
+import java.util.Map;
+
+/** A use case which attempts to use camera2 calls directly in an erroneous manner. */
+public class Camera2InteropErrorUseCase extends BaseUseCase {
+ private static final String TAG = "Camera2InteropErrorUseCase";
+ private final Camera2InteropErrorUseCaseConfiguration configuration;
+ private final CameraCaptureSession.StateCallback captureSessionStateCallback =
+ new CameraCaptureSession.StateCallback() {
+ @Override
+ public void onConfigured(@NonNull CameraCaptureSession session) {
+ Log.d(TAG, "CameraCaptureSession.StateCallback.onConfigured()");
+ }
+
+ @Override
+ public void onConfigureFailed(@NonNull CameraCaptureSession session) {
+ Log.d(TAG, "CameraCaptureSession.StateCallback.onConfigured()");
+ }
+ };
+ private CameraDevice cameraDevice;
+ private final CameraDevice.StateCallback stateCallback =
+ new StateCallback() {
+ @Override
+ public void onOpened(@NonNull CameraDevice camera) {
+ Log.d(TAG, "CameraDevice.StateCallback.onOpened()");
+ Camera2InteropErrorUseCase.this.cameraDevice = camera;
+ }
+
+ @Override
+ public void onDisconnected(@NonNull CameraDevice camera) {
+ Log.d(TAG, "CameraDevice.StateCallback.onDisconnected()");
+ }
+
+ @Override
+ public void onError(@NonNull CameraDevice camera, int error) {
+ Log.d(TAG, "CameraDevice.StateCallback.onError()");
+ }
+ };
+ private ImageReader imageReader;
+
+ public Camera2InteropErrorUseCase(Camera2InteropErrorUseCaseConfiguration configuration) {
+ super(configuration);
+ this.configuration = configuration;
+ }
+
+ /** Closes the {@link CameraDevice} obtained via callback. */
+ void closeCamera() {
+ if (cameraDevice != null) {
+ Log.d(TAG, "Closing CameraDevice.");
+ cameraDevice.close();
+ } else {
+ Log.d(TAG, "No CameraDevice to close.");
+ }
+ }
+
+ /**
+ * Opens a {@link CameraCaptureSession} using the {@link CameraDevice} obtained via callback.
+ */
+ void reopenCaptureSession() {
+ try {
+ Log.d(TAG, "Opening a CameraCaptureSession.");
+ cameraDevice.createCaptureSession(
+ Collections.singletonList(imageReader.getSurface()),
+ captureSessionStateCallback,
+ null);
+ } catch (CameraAccessException e) {
+ Log.e(TAG, "no permission to create capture session");
+ }
+ }
+
+ @Override
+ protected Map<String, Size> onSuggestedResolutionUpdated(
+ Map<String, Size> suggestedResolutionMap) {
+ imageReader = ImageReader.newInstance(640, 480, ImageFormat.YUV_420_888, 2);
+
+ imageReader.setOnImageAvailableListener(
+ imageReader -> {
+ imageReader.acquireNextImage().close();
+ },
+ null);
+
+ SessionConfiguration.Builder sessionConfigBuilder = new SessionConfiguration.Builder();
+ sessionConfigBuilder.clearSurfaces();
+ sessionConfigBuilder.setTemplateType(CameraDevice.TEMPLATE_PREVIEW);
+ sessionConfigBuilder.setDeviceStateCallback(stateCallback);
+
+ try {
+ String cameraId = CameraX.getCameraWithLensFacing(configuration.getLensFacing());
+ attachToCamera(cameraId, sessionConfigBuilder.build());
+ } catch (Exception e) {
+ throw new IllegalArgumentException(
+ "Unable to attach to camera with LensFacing " + configuration.getLensFacing(),
+ e);
+ }
+
+ return suggestedResolutionMap;
+ }
+}
diff --git a/camera/integration-tests/camera2interoperror/src/main/java/androidx/camera/testapp/camera2interoperror/Camera2InteropErrorUseCaseConfiguration.java b/camera/integration-tests/camera2interoperror/src/main/java/androidx/camera/testapp/camera2interoperror/Camera2InteropErrorUseCaseConfiguration.java
new file mode 100644
index 0000000..e816458
--- /dev/null
+++ b/camera/integration-tests/camera2interoperror/src/main/java/androidx/camera/testapp/camera2interoperror/Camera2InteropErrorUseCaseConfiguration.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright (C) 2019 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.testapp.camera2interoperror;
+
+import androidx.camera.core.CameraDeviceConfiguration;
+import androidx.camera.core.Configuration;
+import androidx.camera.core.ImageOutputConfiguration;
+import androidx.camera.core.MutableConfiguration;
+import androidx.camera.core.MutableOptionsBundle;
+import androidx.camera.core.OptionsBundle;
+import androidx.camera.core.UseCaseConfiguration;
+
+/** Configuration for the camera 2 interop case configuration */
+public class Camera2InteropErrorUseCaseConfiguration
+ implements UseCaseConfiguration<Camera2InteropErrorUseCase>,
+ CameraDeviceConfiguration,
+ ImageOutputConfiguration {
+
+ private final Configuration config;
+
+ private Camera2InteropErrorUseCaseConfiguration(Configuration config) {
+ this.config = config;
+ }
+
+ @Override
+ public Configuration getConfiguration() {
+ return config;
+ }
+
+ /** Builder for an empty Configuration */
+ public static final class Builder
+ implements UseCaseConfiguration.Builder<
+ Camera2InteropErrorUseCase,
+ Camera2InteropErrorUseCaseConfiguration,
+ Builder>,
+ CameraDeviceConfiguration.Builder<
+ Camera2InteropErrorUseCaseConfiguration, Builder>,
+ ImageOutputConfiguration.Builder<
+ Camera2InteropErrorUseCaseConfiguration, Builder> {
+
+ private final MutableOptionsBundle optionsBundle;
+
+ public Builder() {
+ optionsBundle = MutableOptionsBundle.create();
+ setTargetClass(Camera2InteropErrorUseCase.class);
+ }
+
+ @Override
+ public MutableConfiguration getMutableConfiguration() {
+ return optionsBundle;
+ }
+
+ @Override
+ public Builder builder() {
+ return this;
+ }
+
+ @Override
+ public Camera2InteropErrorUseCaseConfiguration build() {
+ return new Camera2InteropErrorUseCaseConfiguration(OptionsBundle.from(optionsBundle));
+ }
+ }
+}
diff --git a/camera/integration-tests/camera2interoperror/src/main/java/androidx/camera/testapp/camera2interoperror/CameraXInteropErrorActivity.java b/camera/integration-tests/camera2interoperror/src/main/java/androidx/camera/testapp/camera2interoperror/CameraXInteropErrorActivity.java
new file mode 100644
index 0000000..8f3cc40
--- /dev/null
+++ b/camera/integration-tests/camera2interoperror/src/main/java/androidx/camera/testapp/camera2interoperror/CameraXInteropErrorActivity.java
@@ -0,0 +1,299 @@
+/*
+ * Copyright (C) 2019 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.testapp.camera2interoperror;
+
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.graphics.SurfaceTexture;
+import android.hardware.camera2.CameraDevice;
+import android.hardware.camera2.CameraDevice.StateCallback;
+import android.hardware.camera2.CameraManager;
+import android.os.Bundle;
+import android.support.annotation.NonNull;
+import android.support.v4.content.ContextCompat;
+import android.support.v7.app.AppCompatActivity;
+import android.util.Log;
+import android.util.Rational;
+import android.view.TextureView;
+import android.view.ViewGroup;
+import android.widget.Button;
+import android.widget.TextView;
+
+import androidx.camera.core.CameraX;
+import androidx.camera.core.CameraX.LensFacing;
+import androidx.camera.core.ViewFinderUseCase;
+import androidx.camera.core.ViewFinderUseCaseConfiguration;
+import androidx.legacy.app.ActivityCompat;
+
+import java.util.concurrent.CompletableFuture;
+
+/** for testing interop */
+@SuppressWarnings("AndroidJdkLibsChecker") // CompletableFuture not generally available yet.
+public class CameraXInteropErrorActivity extends AppCompatActivity
+ implements ActivityCompat.OnRequestPermissionsResultCallback {
+ private static final String TAG = "CameraXInteropErrorActivity";
+ private static final int PERMISSIONS_REQUEST_CODE = 42;
+ private static final Rational ASPECT_RATIO = new Rational(4, 3);
+
+ private final CompletableFuture<Boolean> completableFuture = new CompletableFuture<>();
+
+ /** The LensFacing to use. */
+ private LensFacing currentCameraLensFacing = LensFacing.BACK;
+
+ private String currentCameraFacingString = "BACK";
+ private ErrorType currentError = ErrorType.REOPEN_CAMERA;
+
+ /**
+ * Creates a view finder use case.
+ *
+ * <p>This use case observes a {@link SurfaceTexture}. The texture is connected to a {@link
+ * TextureView} to display a camera preview.
+ */
+ private void createViewFinderUseCase() {
+ ViewFinderUseCaseConfiguration configuration =
+ new ViewFinderUseCaseConfiguration.Builder()
+ .setLensFacing(currentCameraLensFacing)
+ .setTargetName("ViewFinder")
+ .setTargetAspectRatio(ASPECT_RATIO)
+ .build();
+ ViewFinderUseCase viewFinderUseCase = new ViewFinderUseCase(configuration);
+
+ viewFinderUseCase.setOnViewFinderOutputUpdateListener(
+ viewFinderOutput -> {
+ // If TextureView was already created, need to re-add it to change the
+ // SurfaceTexture.
+ TextureView textureView = findViewById(R.id.textureView);
+ ViewGroup viewGroup = (ViewGroup) textureView.getParent();
+ viewGroup.removeView(textureView);
+ viewGroup.addView(textureView);
+ textureView.setSurfaceTexture(viewFinderOutput.getSurfaceTexture());
+ });
+
+ CameraX.bindToLifecycle(/* lifecycleOwner= */ this, viewFinderUseCase);
+ Log.i(TAG, "Got UseCase: " + viewFinderUseCase);
+ }
+
+ void createBadUseCase() {
+ Camera2InteropErrorUseCaseConfiguration configuration =
+ new Camera2InteropErrorUseCaseConfiguration.Builder()
+ .setLensFacing(currentCameraLensFacing)
+ .setTargetName("Camera2InteropErrorUseCase")
+ .setTargetAspectRatio(ASPECT_RATIO)
+ .build();
+ Camera2InteropErrorUseCase camera2InteropErrorUseCase =
+ new Camera2InteropErrorUseCase(configuration);
+ CameraX.bindToLifecycle(this, camera2InteropErrorUseCase);
+
+ Button button = this.findViewById(R.id.CauseError);
+
+ button.setOnClickListener(
+ view -> {
+ switch (currentError) {
+ case CLOSE_DEVICE:
+ camera2InteropErrorUseCase.closeCamera();
+ break;
+ case REOPEN_CAMERA:
+ CameraManager manager =
+ (CameraManager)
+ getApplicationContext()
+ .getSystemService(CAMERA_SERVICE);
+ Log.d(TAG, "Attempting to reopen camera");
+ try {
+ String cameraId =
+ CameraX.getCameraWithLensFacing(currentCameraLensFacing);
+ manager.openCamera(
+ cameraId,
+ new StateCallback() {
+ @Override
+ public void onOpened(@NonNull CameraDevice camera) {
+ }
+
+ @Override
+ public void onDisconnected(
+ @NonNull CameraDevice camera) {
+ }
+
+ @Override
+ public void onError(
+ @NonNull CameraDevice camera, int error) {
+ }
+ },
+ null);
+ Log.d(TAG, "Looks like nothing overtly bad occurred");
+ } catch (Exception e) {
+ Log.e(TAG, e.getMessage());
+ Log.e(TAG, "Should we do something here?");
+ }
+ break;
+ case OPEN_CAPTURE_SESSION:
+ camera2InteropErrorUseCase.reopenCaptureSession();
+ break;
+ }
+ });
+
+ TextView textView = this.findViewById(R.id.textView);
+ textView.setText(currentError.toString());
+
+ Button button1 = this.findViewById(R.id.SelectError);
+ button1.setOnClickListener(
+ view -> {
+ switch (currentError) {
+ case CLOSE_DEVICE:
+ currentError = ErrorType.REOPEN_CAMERA;
+ break;
+ case REOPEN_CAMERA:
+ currentError = ErrorType.OPEN_CAPTURE_SESSION;
+ break;
+ case OPEN_CAPTURE_SESSION:
+ currentError = ErrorType.CLOSE_DEVICE;
+ break;
+ }
+ textView.setText(currentError.toString());
+ });
+ }
+
+ /** Creates all the use cases. */
+ private void createUseCases() {
+ createViewFinderUseCase();
+ createBadUseCase();
+ }
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_camera_2main);
+
+ // Get params from adb extra string
+ Bundle bundle = this.getIntent().getExtras();
+ if (bundle != null) {
+ currentCameraFacingString = bundle.getString("cameraFacing");
+ }
+
+ new Thread(
+ () -> {
+ setupCamera();
+ })
+ .start();
+ setupPermissions();
+ }
+
+ private void setupCamera() {
+ try {
+ // Wait for permissions before proceeding.
+ if (!completableFuture.get()) {
+ Log.d(TAG, "Permissions denied.");
+ return;
+ }
+ } catch (Exception e) {
+ Log.e(TAG, "Exception occurred getting permission future: " + e);
+ }
+
+ Log.d(TAG, "Camera Facing: " + currentCameraFacingString);
+ if (currentCameraFacingString.equalsIgnoreCase("BACK")) {
+ currentCameraLensFacing = LensFacing.BACK;
+ } else if (currentCameraFacingString.equalsIgnoreCase("FRONT")) {
+ currentCameraLensFacing = LensFacing.FRONT;
+ } else {
+ throw new RuntimeException("Invalid lens facing: " + currentCameraFacingString);
+ }
+
+ Log.d(TAG, "Using camera lens facing: " + currentCameraLensFacing);
+
+ // Run this on the UI thread to manipulate the Textures & Views.
+ CameraXInteropErrorActivity.this.runOnUiThread(
+ () -> {
+ createUseCases();
+ });
+ }
+
+ private void setupPermissions() {
+ if (!allPermissionsGranted()) {
+ makePermissionRequest();
+ } else {
+ completableFuture.complete(true);
+ }
+ }
+
+ private void makePermissionRequest() {
+ ActivityCompat.requestPermissions(this, getRequiredPermissions(), PERMISSIONS_REQUEST_CODE);
+ }
+
+ /** Returns true if all the necessary permissions have been granted already. */
+ private boolean allPermissionsGranted() {
+ for (String permission : getRequiredPermissions()) {
+ if (ContextCompat.checkSelfPermission(this, permission)
+ != PackageManager.PERMISSION_GRANTED) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /** Tries to acquire all the necessary permissions through a dialog. */
+ private String[] getRequiredPermissions() {
+ PackageInfo info;
+ try {
+ info =
+ getPackageManager()
+ .getPackageInfo(getPackageName(), PackageManager.GET_PERMISSIONS);
+ } catch (NameNotFoundException exception) {
+ Log.e(TAG, "Failed to obtain all required permissions.", exception);
+ return new String[0];
+ }
+ String[] permissions = info.requestedPermissions;
+ if (permissions != null && permissions.length > 0) {
+ return permissions;
+ } else {
+ return new String[0];
+ }
+ }
+
+ @Override
+ public void onRequestPermissionsResult(
+ int requestCode, String[] permissions, int[] grantResults) {
+ switch (requestCode) {
+ case PERMISSIONS_REQUEST_CODE: {
+ // If request is cancelled, the result arrays are empty.
+ if (grantResults.length > 0
+ && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
+ Log.d(TAG, "Permissions Granted.");
+ completableFuture.complete(true);
+ } else {
+ Log.d(TAG, "Permissions Denied.");
+ completableFuture.complete(false);
+ }
+ return;
+ }
+ default:
+ // No-op
+ }
+ }
+
+ /** The types of errors that can be induced on CameraX. */
+ enum ErrorType {
+ /** Attempt to reopen the currently opened {@link CameraDevice}. */
+ REOPEN_CAMERA,
+ /** Close the {@link CameraDevice} without going through CameraX. */
+ CLOSE_DEVICE,
+ /**
+ * Open up a {@link android.hardware.camera2.CameraCaptureSession} using the {@link
+ * CameraDevice} obtained from {@link CameraDevice.StateCallback}.
+ */
+ OPEN_CAPTURE_SESSION
+ }
+}
diff --git a/camera/integration-tests/camera2interoperror/src/main/java/androidx/camera/testapp/camera2interoperror/CameraXInteropErrorApplication.java b/camera/integration-tests/camera2interoperror/src/main/java/androidx/camera/testapp/camera2interoperror/CameraXInteropErrorApplication.java
new file mode 100644
index 0000000..0c9a731
--- /dev/null
+++ b/camera/integration-tests/camera2interoperror/src/main/java/androidx/camera/testapp/camera2interoperror/CameraXInteropErrorApplication.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2019 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.testapp.camera2interoperror;
+
+import android.app.Application;
+
+/** An application for CameraX. */
+public class CameraXInteropErrorApplication extends Application {
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+ }
+}
diff --git a/camera/integration-tests/camera2interoperror/src/main/res/layout/activity_camera_2main.xml b/camera/integration-tests/camera2interoperror/src/main/res/layout/activity_camera_2main.xml
new file mode 100644
index 0000000..bcbe212
--- /dev/null
+++ b/camera/integration-tests/camera2interoperror/src/main/res/layout/activity_camera_2main.xml
@@ -0,0 +1,99 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2019 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.
+-->
+<android.support.constraint.ConstraintLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:id="@+id/constraintLayout"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ tools:context="androidx.camera.app.camera2interoperror.CameraXInteropErrorActivity">
+
+ <TextureView
+ android:id="@+id/textureView"
+ android:layout_width="0dp"
+ android:layout_height="0dp"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent" />
+
+ <TextView
+ android:id="@+id/textView"
+ android:layout_width="0dp"
+ android:layout_height="0dp"
+ android:background="#FFF"
+ android:elevation="2dp"
+ android:scaleType="fitXY"
+ android:src="@android:drawable/btn_radio"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintDimensionRatio="4:3"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintHorizontal_bias="1.0"
+ app:layout_constraintStart_toStartOf="@+id/guideline"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintVertical_bias="0.84000003" />
+
+ <android.support.constraint.Guideline
+ android:id="@+id/guideline"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ app:layout_constraintGuide_begin="0dp"
+ app:layout_constraintGuide_percent="0.7" />
+
+ <android.support.constraint.Guideline
+ android:id="@+id/selecterror"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ app:layout_constraintGuide_begin="0dp"
+ app:layout_constraintGuide_percent="0.1" />
+
+ <android.support.constraint.Guideline
+ android:id="@+id/causeerror"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ app:layout_constraintGuide_begin="0dp"
+ app:layout_constraintGuide_percent="0.4" />
+
+ <Button
+ android:id="@+id/SelectError"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:scaleType="fitXY"
+ android:text="Select Error"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintHorizontal_bias="0.0"
+ app:layout_constraintStart_toStartOf="@+id/selecterror"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintVertical_bias="0.84000003" />
+
+ <Button
+ android:id="@+id/CauseError"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:scaleType="fitXY"
+ android:text="Cause Error"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintHorizontal_bias="0.0"
+ app:layout_constraintStart_toStartOf="@+id/causeerror"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintVertical_bias="0.84000003" />
+</android.support.constraint.ConstraintLayout>
diff --git a/camera/integration-tests/camera2interoperror/src/main/res/values/strings.xml b/camera/integration-tests/camera2interoperror/src/main/res/values/strings.xml
new file mode 100644
index 0000000..2df0565
--- /dev/null
+++ b/camera/integration-tests/camera2interoperror/src/main/res/values/strings.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2019 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.
+-->
+<resources>
+</resources>
diff --git a/camera/integration-tests/camera2interoperror/src/main/res/values/style.xml b/camera/integration-tests/camera2interoperror/src/main/res/values/style.xml
new file mode 100644
index 0000000..7503cc0
--- /dev/null
+++ b/camera/integration-tests/camera2interoperror/src/main/res/values/style.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2019 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.
+-->
+<resources>
+ <!-- Base application theme. -->
+ <style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
+ <!-- Customize your theme here. -->
+ </style>
+</resources>
diff --git a/camera/integration-tests/cameraview/src/build.gradle b/camera/integration-tests/cameraview/src/build.gradle
new file mode 100644
index 0000000..8b0d003
--- /dev/null
+++ b/camera/integration-tests/cameraview/src/build.gradle
@@ -0,0 +1,85 @@
+/*
+ * Copyright (C) 2019 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.
+ */
+
+apply plugin: 'com.android.application'
+
+android {
+ compileOptions {
+ sourceCompatibility project.ext.javaVersion
+ targetCompatibility project.ext.javaVersion
+ }
+
+ compileSdkVersion project.ext.compileSdk
+
+ defaultConfig {
+ applicationId "androidx.camera.testapp.cameraview"
+ minSdkVersion project.ext.minSdk
+ targetSdkVersion project.ext.targetSdk
+ versionCode 1
+ versionName project.ext.version
+ multiDexEnabled true
+ }
+
+ sourceSets {
+ main.manifest.srcFile 'src/main/AndroidManifest.xml'
+ main.java.srcDirs = ['src/main/java']
+ main.java.excludes = ['**/build/**']
+ main.java.includes = ['**/*.java']
+ main.res.srcDirs = ['src/main/res']
+ }
+
+ buildTypes {
+ debug {
+ testCoverageEnabled true
+ }
+
+ release {
+ minifyEnabled true
+ proguardFiles getDefaultProguardFile('proguard-android.txt')
+ }
+ }
+
+ lintOptions {
+ abortOnError false
+ }
+}
+
+dependencies {
+ implementation fileTree(include: ['*.jar'], dir: 'libs')
+
+ // Internal libraries
+ implementation project(':camera2')
+ implementation project(':core')
+ implementation project(':view')
+
+ // Lifecycle and LiveData
+ implementation "android.arch.lifecycle:extensions:${project.ext.lifecycleVersion}"
+ implementation "android.arch.lifecycle:livedata:${project.ext.lifecycleVersion}"
+ implementation "android.arch.lifecycle:runtime:${project.ext.lifecycleVersion}"
+ implementation "android.arch.lifecycle:common-java8:${project.ext.lifecycleVersion}"
+
+ // Android Support Library
+ implementation "com.android.support:appcompat-v7:${project.ext.supportLibraryVersion}"
+ implementation "com.android.support.constraint:constraint-layout:1.0.2"
+ implementation "com.android.support:support-compat:${project.ext.supportLibraryVersion}"
+ implementation "com.android.support:support-annotations:${project.ext.supportLibraryVersion}"
+ implementation "com.android.support:support-v13:${project.ext.supportLibraryVersion}"
+
+ // EXIF
+ api 'com.android.support:exifinterface:27.+'
+}
+
diff --git a/camera/integration-tests/cameraview/src/main/AndroidManifest.xml b/camera/integration-tests/cameraview/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..a27776d
--- /dev/null
+++ b/camera/integration-tests/cameraview/src/main/AndroidManifest.xml
@@ -0,0 +1,48 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2019 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.
+-->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="androidx.camera.testapp.cameraview">
+
+ <uses-sdk
+ android:minSdkVersion="21"
+ android:targetSdkVersion="27" />
+
+ <!-- For using the camera -->
+ <uses-permission android:name="android.permission.CAMERA" />
+ <uses-permission android:name="android.permission.RECORD_AUDIO" />
+ <!-- For saving to the SD Card -->
+ <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
+
+ <application
+ android:label="@string/app_name"
+ android:largeHeap="true"
+ android:theme="@style/AppTheme">
+
+ <activity
+ android:name=".MainActivity"
+ android:label="@string/app_name"
+ android:screenOrientation="fullUser">
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN" />
+
+ <category android:name="android.intent.category.LAUNCHER" />
+ <category android:name="android.intent.category.DEFAULT" />
+ </intent-filter>
+ </activity>
+
+ </application>
+
+</manifest>
diff --git a/camera/integration-tests/cameraview/src/main/java/androidx/camera/testapp/cameraview/CaptureViewOnTouchListener.java b/camera/integration-tests/cameraview/src/main/java/androidx/camera/testapp/cameraview/CaptureViewOnTouchListener.java
new file mode 100644
index 0000000..a917a66
--- /dev/null
+++ b/camera/integration-tests/cameraview/src/main/java/androidx/camera/testapp/cameraview/CaptureViewOnTouchListener.java
@@ -0,0 +1,241 @@
+/*
+ * Copyright (C) 2019 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.app.cameraview;
+
+import android.content.Intent;
+import android.graphics.Rect;
+import android.hardware.Camera;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Environment;
+import android.os.Handler;
+import android.os.Message;
+import android.util.Log;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewConfiguration;
+import android.widget.Toast;
+
+import androidx.annotation.Nullable;
+import androidx.camera.core.ImageCaptureUseCase.OnImageSavedListener;
+import androidx.camera.core.ImageCaptureUseCase.UseCaseError;
+import androidx.camera.core.VideoCaptureUseCase;
+import androidx.camera.core.VideoCaptureUseCase.OnVideoSavedListener;
+import androidx.camera.view.CameraView;
+import androidx.camera.view.CameraView.CaptureMode;
+
+import java.io.File;
+import java.text.SimpleDateFormat;
+import java.util.Locale;
+
+/**
+ * A {@link View.OnTouchListener} which converts a view's touches into camera actions.
+ *
+ * <p>The listener converts touches on a {@link View}, such as a button, into appropriate photo
+ * taking or video recording actions through a {@link CameraView}. A click is interpreted as a
+ * take-photo signal, while a long-press is interpreted as a record-video signal.
+ */
+class CaptureViewOnTouchListener
+ implements View.OnTouchListener, OnImageSavedListener, OnVideoSavedListener {
+ private static final String TAG = "CaptureViewOnTouchListener";
+
+ private static final String FILENAME = "yyyy-MM-dd-HH-mm-ss-SSS";
+ private static final String PHOTO_EXTENSION = ".jpg";
+ private static final String VIDEO_EXTENSION = ".mp4";
+
+ private static final int TAP = 1;
+ private static final int HOLD = 2;
+ private static final int RELEASE = 3;
+
+ private final long longPress = ViewConfiguration.getLongPressTimeout();
+ private final CameraView cameraView;
+
+ // TODO: Use a Handler for a background thread, rather than running on the current (main)
+ // thread.
+ private final Handler handler =
+ new Handler() {
+ @Override
+ public void handleMessage(Message msg) {
+ switch (msg.what) {
+ case TAP:
+ onTap();
+ break;
+ case HOLD:
+ onHold();
+ if (cameraView.getMaxVideoDuration() > 0) {
+ sendEmptyMessageDelayed(RELEASE, cameraView.getMaxVideoDuration());
+ }
+ break;
+ case RELEASE:
+ onRelease();
+ break;
+ default:
+ // No op
+ }
+ }
+ };
+
+ private long downEventTimestamp;
+ private Rect viewBoundsRect;
+
+ /** Creates a new listener which links to the given {@link CameraView}. */
+ CaptureViewOnTouchListener(CameraView cameraView) {
+ this.cameraView = cameraView;
+ }
+
+ /** Called when the user taps. */
+ void onTap() {
+ if (cameraView.getCaptureMode() == CaptureMode.IMAGE
+ || cameraView.getCaptureMode() == CaptureMode.MIXED) {
+ cameraView.takePicture(createNewFile(PHOTO_EXTENSION), this);
+ }
+ }
+
+ /** Called when the user holds (long presses). */
+ void onHold() {
+ if (cameraView.getCaptureMode() == CaptureMode.VIDEO
+ || cameraView.getCaptureMode() == CaptureMode.MIXED) {
+ cameraView.startRecording(createNewFile(VIDEO_EXTENSION), this);
+ }
+ }
+
+ /** Called when the user releases. */
+ void onRelease() {
+ if (cameraView.getCaptureMode() == CaptureMode.VIDEO
+ || cameraView.getCaptureMode() == CaptureMode.MIXED) {
+ cameraView.stopRecording();
+ }
+ }
+
+ @Override
+ public boolean onTouch(View view, MotionEvent event) {
+ switch (event.getAction()) {
+ case MotionEvent.ACTION_DOWN:
+ downEventTimestamp = System.currentTimeMillis();
+ viewBoundsRect =
+ new Rect(view.getLeft(), view.getTop(), view.getRight(), view.getBottom());
+ handler.sendEmptyMessageDelayed(HOLD, longPress);
+ view.setPressed(true);
+ break;
+ case MotionEvent.ACTION_MOVE:
+ // If the user moves their finger off the button, trigger RELEASE
+ if (viewBoundsRect.contains(
+ view.getLeft() + (int) event.getX(), view.getTop() + (int) event.getY())) {
+ break;
+ }
+ // Fall-through
+ case MotionEvent.ACTION_CANCEL:
+ clearHandler();
+ if (deltaSinceDownEvent() > longPress
+ && (cameraView.getMaxVideoDuration() <= 0
+ || deltaSinceDownEvent() < cameraView.getMaxVideoDuration())) {
+ handler.sendEmptyMessage(RELEASE);
+ }
+ view.setPressed(false);
+ break;
+ case MotionEvent.ACTION_UP:
+ clearHandler();
+ if (deltaSinceDownEvent() < longPress) {
+ handler.sendEmptyMessage(TAP);
+ } else if ((cameraView.getMaxVideoDuration() <= 0
+ || deltaSinceDownEvent() < cameraView.getMaxVideoDuration())) {
+ handler.sendEmptyMessage(RELEASE);
+ }
+ view.setPressed(false);
+ break;
+ default:
+ // No op
+ }
+ return true;
+ }
+
+ private long deltaSinceDownEvent() {
+ return System.currentTimeMillis() - downEventTimestamp;
+ }
+
+ private void clearHandler() {
+ handler.removeMessages(TAP);
+ handler.removeMessages(HOLD);
+ handler.removeMessages(RELEASE);
+ }
+
+ private File createNewFile(String extension) {
+ // Use Locale.US to ensure we get ASCII digits
+ return new File(
+ Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES),
+ new SimpleDateFormat(FILENAME, Locale.US).format(System.currentTimeMillis())
+ + extension);
+ }
+
+ @Override
+ public void onImageSaved(File file) {
+ report("Picture saved to " + file.getAbsolutePath());
+
+ // Print out metadata about the picture
+ // TODO: Print out metadata to log once metadata is implemented
+
+ broadcastPicture(file);
+ }
+
+ @Override
+ public void onVideoSaved(File file) {
+ report("Video saved to " + file.getAbsolutePath());
+ broadcastVideo(file);
+ }
+
+ @Override
+ public void onError(UseCaseError useCaseError, String message, @Nullable Throwable cause) {
+ report("Failure");
+ }
+
+ @Override
+ public void onError(
+ VideoCaptureUseCase.UseCaseError useCaseError,
+ String message,
+ @Nullable Throwable cause) {
+ report("Failure");
+ }
+
+ private void report(String msg) {
+ Log.d(TAG, msg);
+ Toast.makeText(cameraView.getContext(), msg, Toast.LENGTH_SHORT).show();
+ }
+
+ private void broadcastPicture(File file) {
+ if (Build.VERSION.SDK_INT < 24) {
+ Intent intent = new Intent(Camera.ACTION_NEW_PICTURE);
+ intent.setData(Uri.fromFile(file));
+ cameraView.getContext().sendBroadcast(intent);
+ } else {
+ Intent intent = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE);
+ intent.setData(Uri.fromFile(file));
+ cameraView.getContext().sendBroadcast(intent);
+ }
+ }
+
+ private void broadcastVideo(File file) {
+ if (Build.VERSION.SDK_INT < 24) {
+ Intent intent = new Intent(Camera.ACTION_NEW_VIDEO);
+ intent.setData(Uri.fromFile(file));
+ cameraView.getContext().sendBroadcast(intent);
+ } else {
+ Intent intent = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE);
+ intent.setData(Uri.fromFile(file));
+ cameraView.getContext().sendBroadcast(intent);
+ }
+ }
+}
diff --git a/camera/integration-tests/cameraview/src/main/java/androidx/camera/testapp/cameraview/MainActivity.java b/camera/integration-tests/cameraview/src/main/java/androidx/camera/testapp/cameraview/MainActivity.java
new file mode 100644
index 0000000..3f88be9
--- /dev/null
+++ b/camera/integration-tests/cameraview/src/main/java/androidx/camera/testapp/cameraview/MainActivity.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright (C) 2019 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.app.cameraview;
+
+import android.Manifest;
+import android.content.pm.PackageManager;
+import android.os.Bundle;
+import android.support.v4.content.ContextCompat;
+import android.support.v7.app.AppCompatActivity;
+import android.util.Log;
+import android.widget.Toast;
+
+import androidx.annotation.NonNull;
+
+/** The main activity. */
+public class MainActivity extends AppCompatActivity {
+ private static final String TAG = "MainActivity";
+ private static final String[] REQUIRED_PERMISSIONS =
+ new String[]{
+ Manifest.permission.CAMERA,
+ Manifest.permission.RECORD_AUDIO,
+ Manifest.permission.WRITE_EXTERNAL_STORAGE
+ };
+ private static final int REQUEST_CODE_PERMISSIONS = 10;
+
+ private boolean checkedPermissions = false;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_main);
+ if (null == savedInstanceState) {
+ if (allPermissionsGranted()) {
+ startCamera();
+ } else if (!checkedPermissions) {
+ requestPermissions(REQUIRED_PERMISSIONS, REQUEST_CODE_PERMISSIONS);
+ checkedPermissions = true;
+ }
+ }
+ }
+
+ @Override
+ public void onRequestPermissionsResult(
+ int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
+ if (requestCode == REQUEST_CODE_PERMISSIONS) {
+ if (allPermissionsGranted()) {
+ startCamera();
+ } else {
+ report("Permissions not granted by the user.");
+ }
+ }
+ }
+
+ private boolean allPermissionsGranted() {
+ for (String permission : REQUIRED_PERMISSIONS) {
+ if (ContextCompat.checkSelfPermission(this, permission)
+ != PackageManager.PERMISSION_GRANTED) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ private void startCamera() {
+ getSupportFragmentManager()
+ .beginTransaction()
+ .replace(R.id.content, new MainFragment())
+ .commit();
+ }
+
+ private void report(String msg) {
+ Log.d(TAG, msg);
+ Toast.makeText(this, msg, Toast.LENGTH_SHORT).show();
+ }
+}
diff --git a/camera/integration-tests/cameraview/src/main/java/androidx/camera/testapp/cameraview/MainFragment.java b/camera/integration-tests/cameraview/src/main/java/androidx/camera/testapp/cameraview/MainFragment.java
new file mode 100644
index 0000000..2325ab1
--- /dev/null
+++ b/camera/integration-tests/cameraview/src/main/java/androidx/camera/testapp/cameraview/MainFragment.java
@@ -0,0 +1,221 @@
+/*
+ * Copyright (C) 2019 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.app.cameraview;
+
+import android.graphics.Rect;
+import android.os.Bundle;
+import android.support.v4.app.Fragment;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewTreeObserver;
+import android.widget.CompoundButton;
+import android.widget.Toast;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.camera.core.CameraX.LensFacing;
+import androidx.camera.view.CameraView;
+import androidx.camera.view.CameraView.CaptureMode;
+import androidx.camera.view.CameraView.ScaleType;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.PrintStream;
+
+/** The main camera fragment. */
+public class MainFragment extends Fragment {
+ private static final String TAG = "MainFragment";
+
+ // Possible values for this intent key are the name values of CameraX.LensFacing encoded as
+ // strings (case-insensitive): "back", "front".
+ private static final String INTENT_EXTRA_CAMERA_DIRECTION = "camera_direction";
+
+ // Possible values for this intent key are the name values of CameraView.CaptureMode encoded as
+ // strings (case-insensitive): "image", "video", "mixed"
+ private static final String INTENT_EXTRA_CAPTURE_MODE = "captureMode";
+
+ private View cameraHolder;
+ private CameraView cameraView;
+ private View captureView;
+ private CompoundButton modeButton;
+ @Nullable
+ private CompoundButton toggleCameraButton;
+ private CompoundButton toggleCropButton;
+
+ @Override
+ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
+ cameraHolder = view.findViewById(R.id.layout_camera);
+ cameraView = view.findViewById(R.id.camera);
+ toggleCameraButton = view.findViewById(R.id.toggle);
+ toggleCropButton = view.findViewById(R.id.toggle_crop);
+ captureView = cameraHolder.findViewById(R.id.capture);
+ if (cameraHolder == null) {
+ throw new IllegalStateException("No View found with id R.id.layout_camera");
+ }
+ if (cameraView == null) {
+ throw new IllegalStateException("No CameraView found with id R.id.camera");
+ }
+ if (captureView == null) {
+ throw new IllegalStateException("No CameraView found with id R.id.capture");
+ }
+
+ modeButton = cameraHolder.findViewById(R.id.mode);
+
+ if (modeButton == null) {
+ throw new IllegalStateException("No View found with id R.id.mode");
+ }
+
+ // Log the location of some views, so their locations can be used to perform some automated
+ // clicks in tests.
+ logCenterCoordinates(cameraView, "camera_view");
+ logCenterCoordinates(captureView, "capture");
+ logCenterCoordinates(toggleCameraButton, "toggle_camera");
+ logCenterCoordinates(toggleCropButton, "toggle_crop");
+ logCenterCoordinates(modeButton, "mode");
+
+ // Get extra option for setting initial camera direction
+ Bundle bundle = getActivity().getIntent().getExtras();
+ if (bundle != null) {
+ String cameraDirectionString = bundle.getString(INTENT_EXTRA_CAMERA_DIRECTION);
+ if (cameraDirectionString != null) {
+ LensFacing lensFacing = LensFacing.valueOf(cameraDirectionString.toUpperCase());
+ cameraView.setCameraByLensFacing(lensFacing);
+ }
+
+ String captureModeString = bundle.getString(INTENT_EXTRA_CAPTURE_MODE);
+ if (captureModeString != null) {
+ CaptureMode captureMode = CaptureMode.valueOf(captureModeString.toUpperCase());
+ cameraView.setCaptureMode(captureMode);
+ }
+ }
+ }
+
+ @Override
+ public void onViewStateRestored(@Nullable Bundle savedInstanceState) {
+ super.onViewStateRestored(savedInstanceState);
+
+ // Set the lifecycle that will be used to control the camera
+ cameraView.bindToLifecycle(getActivity());
+
+ cameraView.setPinchToZoomEnabled(true);
+ captureView.setOnTouchListener(new CaptureViewOnTouchListener(cameraView));
+
+ // Set clickable, Let the cameraView can be interacted by Voice Access
+ cameraView.setClickable(true);
+
+ if (toggleCameraButton != null) {
+ toggleCameraButton.setVisibility(
+ (cameraView.hasCameraWithLensFacing(LensFacing.BACK)
+ && cameraView.hasCameraWithLensFacing(LensFacing.FRONT))
+ ? View.VISIBLE
+ : View.INVISIBLE);
+ toggleCameraButton.setChecked(cameraView.getCameraLensFacing() == LensFacing.FRONT);
+ }
+
+ // Set listeners here, or else restoring state will trigger them.
+ if (toggleCameraButton != null) {
+ toggleCameraButton.setOnCheckedChangeListener(
+ (b, checked) ->
+ cameraView.setCameraByLensFacing(
+ checked ? LensFacing.FRONT : LensFacing.BACK));
+ }
+
+ toggleCropButton.setChecked(cameraView.getScaleType() == ScaleType.CENTER_CROP);
+ toggleCropButton.setOnCheckedChangeListener(
+ (b, checked) -> {
+ if (checked) {
+ cameraView.setScaleType(ScaleType.CENTER_CROP);
+ } else {
+ cameraView.setScaleType(ScaleType.CENTER_INSIDE);
+ }
+ });
+
+ if (modeButton != null) {
+ updateModeButtonIcon();
+
+ modeButton.setOnClickListener(
+ view -> {
+ if (cameraView.isRecording()) {
+ Toast.makeText(
+ getContext(),
+ "Can not switch mode during video recording.",
+ Toast.LENGTH_SHORT)
+ .show();
+ return;
+ }
+
+ if (cameraView.getCaptureMode() == CaptureMode.MIXED) {
+ cameraView.setCaptureMode(CaptureMode.IMAGE);
+ } else if (cameraView.getCaptureMode() == CaptureMode.IMAGE) {
+ cameraView.setCaptureMode(CaptureMode.VIDEO);
+ } else {
+ cameraView.setCaptureMode(CaptureMode.MIXED);
+ }
+
+ updateModeButtonIcon();
+ });
+ }
+ }
+
+ private void updateModeButtonIcon() {
+ if (cameraView.getCaptureMode() == CaptureMode.MIXED) {
+ modeButton.setButtonDrawable(R.drawable.ic_photo_camera);
+ } else if (cameraView.getCaptureMode() == CaptureMode.IMAGE) {
+ modeButton.setButtonDrawable(R.drawable.ic_camera);
+ } else {
+ modeButton.setButtonDrawable(R.drawable.ic_videocam);
+ }
+ }
+
+ @Override
+ public View onCreateView(
+ @NonNull LayoutInflater inflater,
+ @Nullable ViewGroup container,
+ @Nullable Bundle savedInstanceState) {
+ return inflater.inflate(R.layout.fragment_main, container, false);
+ }
+
+ private void logCenterCoordinates(View view, String name) {
+ view.getViewTreeObserver()
+ .addOnGlobalLayoutListener(
+ new ViewTreeObserver.OnGlobalLayoutListener() {
+ @Override
+ public void onGlobalLayout() {
+ Rect rect = new Rect();
+ view.getGlobalVisibleRect(rect);
+ Log.d(
+ TAG,
+ "View "
+ + name
+ + " Center "
+ + rect.centerX()
+ + " "
+ + rect.centerY());
+ File externalDir = getActivity().getExternalFilesDir(null);
+ File logFile =
+ new File(externalDir, name + "_button_coordinates.txt");
+ try (PrintStream stream = new PrintStream(logFile)) {
+ stream.print(rect.centerX() + " " + rect.centerY());
+ } catch (IOException e) {
+ Log.e(TAG, "Could not save to " + logFile, e);
+ }
+ }
+ });
+ }
+}
diff --git a/camera/integration-tests/cameraview/src/main/res/drawable/fullscreen_selector.xml b/camera/integration-tests/cameraview/src/main/res/drawable/fullscreen_selector.xml
new file mode 100644
index 0000000..ff39715
--- /dev/null
+++ b/camera/integration-tests/cameraview/src/main/res/drawable/fullscreen_selector.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2019 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.
+-->
+<selector
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:constantSize="true">
+ <item android:drawable="@drawable/ic_fullscreen_exit" android:state_checked="true" android:state_enabled="true" />
+ <item android:drawable="@drawable/ic_fullscreen" android:state_checked="false" android:state_enabled="true" />
+</selector>
diff --git a/camera/integration-tests/cameraview/src/main/res/drawable/ic_camera.xml b/camera/integration-tests/cameraview/src/main/res/drawable/ic_camera.xml
new file mode 100644
index 0000000..a35682e
--- /dev/null
+++ b/camera/integration-tests/cameraview/src/main/res/drawable/ic_camera.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2019 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.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportHeight="24.0"
+ android:viewportWidth="24.0">
+ <path
+
+ android:fillColor="?android:attr/colorControlNormal"
+ android:pathData="M9.4,10.5l4.77,-8.26C13.47,2.09 12.75,2 12,2c-2.4,0 -4.6,0.85 -6.32,2.25l3.66,6.35 0.06,-0.1zM21.54,9c-0.92,-2.92 -3.15,-5.26 -6,-6.34L11.88,9h9.66zM21.8,10h-7.49l0.29,0.5 4.76,8.25C21,16.97 22,14.61 22,12c0,-0.69 -0.07,-1.35 -0.2,-2zM8.54,12l-3.9,-6.75C3.01,7.03 2,9.39 2,12c0,0.69 0.07,1.35 0.2,2h7.49l-1.15,-2zM2.46,15c0.92,2.92 3.15,5.26 6,6.34L12.12,15L2.46,15zM13.73,15l-3.9,6.76c0.7,0.15 1.42,0.24 2.17,0.24 2.4,0 4.6,-0.85 6.32,-2.25l-3.66,-6.35 -0.93,1.6z" />
+</vector>
diff --git a/camera/integration-tests/cameraview/src/main/res/drawable/ic_fullscreen.xml b/camera/integration-tests/cameraview/src/main/res/drawable/ic_fullscreen.xml
new file mode 100644
index 0000000..467ce90
--- /dev/null
+++ b/camera/integration-tests/cameraview/src/main/res/drawable/ic_fullscreen.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2019 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.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportHeight="24.0"
+ android:viewportWidth="24.0">
+ <path
+ android:fillColor="?android:attr/colorControlNormal"
+ android:pathData="M7,14L5,14v5h5v-2L7,17v-3zM5,10h2L7,7h3L10,5L5,5v5zM17,17h-3v2h5v-5h-2v3zM14,5v2h3v3h2L19,5h-5z" />
+</vector>
diff --git a/camera/integration-tests/cameraview/src/main/res/drawable/ic_fullscreen_exit.xml b/camera/integration-tests/cameraview/src/main/res/drawable/ic_fullscreen_exit.xml
new file mode 100644
index 0000000..8ee9dfc
--- /dev/null
+++ b/camera/integration-tests/cameraview/src/main/res/drawable/ic_fullscreen_exit.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2019 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.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportHeight="24.0"
+ android:viewportWidth="24.0">
+ <path
+ android:fillColor="?android:attr/colorControlNormal"
+ android:pathData="M5,16h3v3h2v-5L5,14v2zM8,8L5,8v2h5L10,5L8,5v3zM14,19h2v-3h3v-2h-5v5zM16,8L16,5h-2v5h5L19,8h-3z" />
+</vector>
diff --git a/camera/integration-tests/cameraview/src/main/res/drawable/ic_photo_camera.xml b/camera/integration-tests/cameraview/src/main/res/drawable/ic_photo_camera.xml
new file mode 100644
index 0000000..625e155
--- /dev/null
+++ b/camera/integration-tests/cameraview/src/main/res/drawable/ic_photo_camera.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2019 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.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:tint="?attr/colorControlNormal"
+ android:viewportHeight="24.0"
+ android:viewportWidth="24.0">
+ <path
+ android:fillColor="@android:color/white"
+ android:pathData="M12,12m-3.2,0a3.2,3.2 0,1 1,6.4 0a3.2,3.2 0,1 1,-6.4 0" />
+ <path
+ android:fillColor="@android:color/white"
+ android:pathData="M9,2L7.17,4L4,4c-1.1,0 -2,0.9 -2,2v12c0,1.1 0.9,2 2,2h16c1.1,0 2,-0.9 2,-2L22,6c0,-1.1 -0.9,-2 -2,-2h-3.17L15,2L9,2zM12,17c-2.76,0 -5,-2.24 -5,-5s2.24,-5 5,-5 5,2.24 5,5 -2.24,5 -5,5z" />
+</vector>
diff --git a/camera/integration-tests/cameraview/src/main/res/drawable/ic_videocam.xml b/camera/integration-tests/cameraview/src/main/res/drawable/ic_videocam.xml
new file mode 100644
index 0000000..f009891
--- /dev/null
+++ b/camera/integration-tests/cameraview/src/main/res/drawable/ic_videocam.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2019 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.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:tint="?attr/colorControlNormal"
+ android:viewportHeight="24.0"
+ android:viewportWidth="24.0">
+ <path
+ android:fillColor="@android:color/white"
+ android:pathData="M18,10.48V6c0,-1.1 -0.9,-2 -2,-2H4C2.9,4 2,4.9 2,6v12c0,1.1 0.9,2 2,2h12c1.1,0 2,-0.9 2,-2v-4.48l4,3.98v-11L18,10.48zM16,9.69V18H4V6h12V9.69zM11.67,11l-2.5,3.72L7.5,12L5,16h10L11.67,11z" />
+</vector>
diff --git a/camera/integration-tests/cameraview/src/main/res/layout-land/fragment_main.xml b/camera/integration-tests/cameraview/src/main/res/layout-land/fragment_main.xml
new file mode 100644
index 0000000..ee0b902
--- /dev/null
+++ b/camera/integration-tests/cameraview/src/main/res/layout-land/fragment_main.xml
@@ -0,0 +1,73 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2019 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.
+-->
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical">
+
+ <LinearLayout
+ android:id="@+id/layout_camera"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="horizontal">
+
+ <androidx.camera.view.CameraView
+ android:id="@+id/camera"
+ android:layout_width="0dp"
+ android:layout_height="match_parent"
+ android:layout_gravity="center_vertical"
+ android:layout_weight="1" />
+
+ <LinearLayout
+ android:layout_width="125dp"
+ android:layout_height="match_parent"
+ android:layout_marginBottom="10dp"
+ android:layout_marginTop="10dp"
+ android:gravity="center"
+ android:orientation="vertical">
+
+ <CheckBox
+ android:id="@+id/toggle_crop"
+ android:layout_width="wrap_content"
+ android:layout_height="50dp"
+ android:button="@drawable/fullscreen_selector" />
+
+ <CheckBox
+ android:id="@+id/mode"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:button="@drawable/ic_photo_camera" />
+
+ <Button
+ android:id="@+id/capture"
+ style="?android:buttonBarButtonStyle"
+ android:layout_width="match_parent"
+ android:layout_height="0dp"
+ android:layout_marginBottom="50dp"
+ android:layout_marginTop="50dp"
+ android:layout_weight="1"
+ android:text="@string/btn_capture" />
+
+ <CheckBox
+ android:id="@+id/toggle"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content" />
+
+ </LinearLayout>
+
+ </LinearLayout>
+
+</FrameLayout>
diff --git a/camera/integration-tests/cameraview/src/main/res/layout/activity_main.xml b/camera/integration-tests/cameraview/src/main/res/layout/activity_main.xml
new file mode 100644
index 0000000..a88437d0
--- /dev/null
+++ b/camera/integration-tests/cameraview/src/main/res/layout/activity_main.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2019 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.
+-->
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:id="@+id/content"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ tools:context=".MainActivity" />
diff --git a/camera/integration-tests/cameraview/src/main/res/layout/fragment_main.xml b/camera/integration-tests/cameraview/src/main/res/layout/fragment_main.xml
new file mode 100644
index 0000000..72f8ecb
--- /dev/null
+++ b/camera/integration-tests/cameraview/src/main/res/layout/fragment_main.xml
@@ -0,0 +1,73 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2019 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.
+-->
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical">
+
+ <LinearLayout
+ android:id="@+id/layout_camera"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical">
+
+ <androidx.camera.view.CameraView
+ android:id="@+id/camera"
+ android:layout_width="match_parent"
+ android:layout_height="0dp"
+ android:layout_gravity="center_horizontal"
+ android:layout_weight="1" />
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="58dp"
+ android:layout_marginLeft="10dp"
+ android:layout_marginRight="10dp"
+ android:gravity="center">
+
+ <CheckBox
+ android:id="@+id/toggle"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content" />
+
+ <Button
+ android:id="@+id/capture"
+ style="?android:buttonBarButtonStyle"
+ android:layout_width="0dp"
+ android:layout_height="match_parent"
+ android:layout_marginLeft="50dp"
+ android:layout_marginRight="50dp"
+ android:layout_weight="1"
+ android:text="@string/btn_capture" />
+
+ <CheckBox
+ android:id="@+id/mode"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginRight="15dp"
+ android:button="@drawable/ic_photo_camera" />
+
+ <CheckBox
+ android:id="@+id/toggle_crop"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:button="@drawable/fullscreen_selector" />
+
+ </LinearLayout>
+
+ </LinearLayout>
+
+</FrameLayout>
diff --git a/camera/integration-tests/cameraview/src/main/res/values/ids.xml b/camera/integration-tests/cameraview/src/main/res/values/ids.xml
new file mode 100644
index 0000000..91326df
--- /dev/null
+++ b/camera/integration-tests/cameraview/src/main/res/values/ids.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2019 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.
+-->
+<resources>
+ <item name="layout_camera" type="id" />
+ <item name="layout_permissions" type="id" />
+ <item name="request_permissions" type="id" />
+ <item name="camera" type="id" />
+ <item name="capture" type="id" />
+ <item name="duration" type="id" />
+ <item name="progress" type="id" />
+ <item name="toggle" type="id" />
+ <item name="cancel" type="id" />
+ <item name="confirm" type="id" />
+</resources>
diff --git a/camera/integration-tests/cameraview/src/main/res/values/strings.xml b/camera/integration-tests/cameraview/src/main/res/values/strings.xml
new file mode 100644
index 0000000..ca95f1a
--- /dev/null
+++ b/camera/integration-tests/cameraview/src/main/res/values/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2019 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.
+-->
+<resources>
+
+ <string name="app_name">CameraView Demo</string>
+
+ <string name="view_finder">Viewfinder</string>
+ <string name="btn_capture">Capture</string>
+ <string name="btn_confirm">Confirm</string>
+ <string name="btn_cancel">Cancel</string>
+
+</resources>
diff --git a/camera/integration-tests/cameraview/src/main/res/values/styles.xml b/camera/integration-tests/cameraview/src/main/res/values/styles.xml
new file mode 100644
index 0000000..2d26a15
--- /dev/null
+++ b/camera/integration-tests/cameraview/src/main/res/values/styles.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2019 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.
+-->
+<resources>
+
+ <!-- Base application theme. -->
+ <style name="AppTheme" parent="Theme.AppCompat.NoActionBar" />
+
+</resources>
diff --git a/camera/integration-tests/hellocamerax/build.gradle b/camera/integration-tests/hellocamerax/build.gradle
new file mode 100644
index 0000000..72f4a6b
--- /dev/null
+++ b/camera/integration-tests/hellocamerax/build.gradle
@@ -0,0 +1,79 @@
+/*
+ * Copyright (C) 2019 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.
+ */
+
+apply plugin: 'com.android.application'
+
+android {
+ compileOptions {
+ sourceCompatibility project.ext.javaVersion
+ targetCompatibility project.ext.javaVersion
+ }
+
+ compileSdkVersion project.ext.compileSdk
+
+ defaultConfig {
+ applicationId "androidx.camera.testapp.hellocamerax"
+ minSdkVersion project.ext.minSdk
+ targetSdkVersion project.ext.targetSdk
+ versionCode 1
+ versionName project.ext.version
+ multiDexEnabled true
+ }
+
+ sourceSets {
+ main.manifest.srcFile 'src/main/AndroidManifest.xml'
+ main.java.srcDirs = ['src/main/java']
+ main.java.excludes = ['**/build/**']
+ main.java.includes = ['**/*.java']
+ main.res.srcDirs = ['src/main/res']
+ }
+
+ buildTypes {
+ debug {
+ testCoverageEnabled true
+ }
+
+ release {
+ minifyEnabled true
+ proguardFiles getDefaultProguardFile('proguard-android.txt')
+ }
+ }
+
+ lintOptions {
+ abortOnError false
+ }
+}
+
+dependencies {
+ // Internal library
+ implementation project(':camera2')
+ implementation project(':core')
+
+ // Lifecycle and LiveData
+ implementation "android.arch.lifecycle:extensions:${project.ext.lifecycleVersion}"
+ implementation "android.arch.lifecycle:livedata:${project.ext.lifecycleVersion}"
+ implementation "android.arch.lifecycle:runtime:${project.ext.lifecycleVersion}"
+ implementation "android.arch.lifecycle:common-java8:${project.ext.lifecycleVersion}"
+
+ // Android Support Library
+ implementation "com.android.support:appcompat-v7:${project.ext.supportLibraryVersion}"
+ implementation "com.android.support.constraint:constraint-layout:1.0.2"
+ implementation "com.android.support:support-compat:${project.ext.supportLibraryVersion}"
+ implementation "com.android.support:support-annotations:${project.ext.supportLibraryVersion}"
+ implementation "com.android.support:support-v13:${project.ext.supportLibraryVersion}"
+}
+
diff --git a/camera/integration-tests/hellocamerax/src/main/AndroidManifest.xml b/camera/integration-tests/hellocamerax/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..c1178c1
--- /dev/null
+++ b/camera/integration-tests/hellocamerax/src/main/AndroidManifest.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2019 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.
+-->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="androidx.camera.testapp.hellocamerax">
+
+ <uses-sdk
+ android:minSdkVersion="21"
+ android:targetSdkVersion="27" />
+
+ <uses-permission android:name="android.permission.CAMERA" />
+ <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
+ <uses-permission android:name="android.permission.RECORD_AUDIO" />
+
+ <uses-feature android:name="android.hardware.camera" />
+
+ <application
+ android:name=".CameraXApplication"
+ android:theme="@style/AppTheme">
+ <activity
+ android:name=".CameraXActivity"
+ android:label="Hello CameraX">
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN" />
+ <category android:name="android.intent.category.LAUNCHER" />
+ </intent-filter>
+ </activity>
+ </application>
+</manifest>
diff --git a/camera/integration-tests/hellocamerax/src/main/java/androidx/camera/testapp/hellocamerax/CameraXActivity.java b/camera/integration-tests/hellocamerax/src/main/java/androidx/camera/testapp/hellocamerax/CameraXActivity.java
new file mode 100644
index 0000000..42d095e
--- /dev/null
+++ b/camera/integration-tests/hellocamerax/src/main/java/androidx/camera/testapp/hellocamerax/CameraXActivity.java
@@ -0,0 +1,691 @@
+/*
+ * Copyright (C) 2019 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.testapp.hellocamerax;
+
+import android.arch.lifecycle.MutableLiveData;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.graphics.Color;
+import android.graphics.Matrix;
+import android.graphics.SurfaceTexture;
+import android.os.Bundle;
+import android.os.Environment;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.StrictMode;
+import android.support.v4.content.ContextCompat;
+import android.support.v7.app.AppCompatActivity;
+import android.util.Log;
+import android.util.Size;
+import android.view.Surface;
+import android.view.TextureView;
+import android.view.TextureView.SurfaceTextureListener;
+import android.view.ViewGroup;
+import android.widget.Button;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import androidx.camera.core.BaseUseCase;
+import androidx.camera.core.CameraDeviceConfiguration;
+import androidx.camera.core.CameraX;
+import androidx.camera.core.CameraX.LensFacing;
+import androidx.camera.core.ImageAnalysisUseCase;
+import androidx.camera.core.ImageAnalysisUseCaseConfiguration;
+import androidx.camera.core.ImageCaptureUseCase;
+import androidx.camera.core.ImageCaptureUseCaseConfiguration;
+import androidx.camera.core.VideoCaptureUseCase;
+import androidx.camera.core.VideoCaptureUseCaseConfiguration;
+import androidx.camera.core.ViewFinderUseCase;
+import androidx.camera.core.ViewFinderUseCaseConfiguration;
+import androidx.legacy.app.ActivityCompat;
+
+import java.io.File;
+import java.math.BigDecimal;
+import java.text.Format;
+import java.text.SimpleDateFormat;
+import java.util.Calendar;
+import java.util.concurrent.Callable;
+import java.util.concurrent.FutureTask;
+import java.util.concurrent.atomic.AtomicLong;
+import java.util.concurrent.atomic.AtomicReference;
+
+/**
+ * An activity with four use cases: (1) view finder, (2) image capture, (3) image analysis, (4)
+ * video capture.
+ *
+ * <p>All four use cases are created with CameraX and tied to the activity's lifecycle. CameraX
+ * automatically connects and disconnects the use cases from the camera in response to changes in
+ * the activity's lifecycle. Therefore, the use cases function properly when the app is paused and
+ * resumed and when the device is rotated. The complex interactions between the camera and these
+ * lifecycle events are handled internally by CameraX.
+ */
+public class CameraXActivity extends AppCompatActivity
+ implements ActivityCompat.OnRequestPermissionsResultCallback {
+ private static final String TAG = "CameraXActivity";
+ private static final int PERMISSIONS_REQUEST_CODE = 42;
+ // Possible values for this intent key: "backward" or "forward".
+ private static final String INTENT_EXTRA_CAMERA_DIRECTION = "camera_direction";
+
+ private final SettableCallable<Boolean> settableResult = new SettableCallable<>();
+ private final FutureTask<Boolean> completableFuture = new FutureTask<>(settableResult);
+ private final AtomicLong imageAnalysisFrameCount = new AtomicLong(0);
+ private final MutableLiveData<String> imageAnalysisResult = new MutableLiveData<>();
+ private VideoFileSaver videoFileSaver;
+ /** The cameraId to use. Assume that 0 is the typical back facing camera. */
+ private LensFacing currentCameraLensFacing = LensFacing.BACK;
+
+ // TODO: Move the analysis processing, capture processing to separate threads, so
+ // there is smaller impact on the preview.
+ private String currentCameraDirection = "BACKWARD";
+ private ViewFinderUseCase viewFinderUseCase;
+ private ImageAnalysisUseCase imageAnalysisUseCase;
+ private ImageCaptureUseCase imageCaptureUseCase;
+ private VideoCaptureUseCase videoCaptureUseCase;
+
+ /**
+ * Creates a view finder use case.
+ *
+ * <p>This use case observes a {@link SurfaceTexture}. The texture is connected to a {@link
+ * TextureView} to display a camera preview.
+ */
+ private void createViewFinderUseCase() {
+ Button button = this.findViewById(R.id.PreviewToggle);
+ button.setBackgroundColor(Color.LTGRAY);
+ enableViewFinderUseCase();
+
+ button.setOnClickListener(
+ view -> {
+ Button buttonView = (Button) view;
+ if (viewFinderUseCase != null) {
+ // Remove the use case
+ buttonView.setBackgroundColor(Color.RED);
+ CameraX.unbind(viewFinderUseCase);
+ viewFinderUseCase = null;
+ } else {
+ // Add the use case
+ buttonView.setBackgroundColor(Color.LTGRAY);
+
+ enableViewFinderUseCase();
+ }
+ });
+
+ Log.i(TAG, "Got UseCase: " + viewFinderUseCase);
+ }
+
+ void enableViewFinderUseCase() {
+ ViewFinderUseCaseConfiguration configuration =
+ new ViewFinderUseCaseConfiguration.Builder()
+ .setLensFacing(currentCameraLensFacing)
+ .setTargetName("ViewFinder")
+ .build();
+
+ viewFinderUseCase = new ViewFinderUseCase(configuration);
+ TextureView textureView = findViewById(R.id.textureView);
+ viewFinderUseCase.setOnViewFinderOutputUpdateListener(
+ viewFinderOutput -> {
+ // If TextureView was already created, need to re-add it to change the
+ // SurfaceTexture.
+ ViewGroup viewGroup = (ViewGroup) textureView.getParent();
+ viewGroup.removeView(textureView);
+ viewGroup.addView(textureView);
+ textureView.setSurfaceTexture(viewFinderOutput.getSurfaceTexture());
+ });
+
+ if (!bindToLifecycleSafely(viewFinderUseCase, R.id.PreviewToggle)) {
+ viewFinderUseCase = null;
+ return;
+ }
+
+ transformPreview();
+
+ textureView.setSurfaceTextureListener(
+ new SurfaceTextureListener() {
+ @Override
+ public void onSurfaceTextureAvailable(
+ SurfaceTexture surfaceTexture, int i, int i1) {
+ }
+
+ @Override
+ public void onSurfaceTextureSizeChanged(
+ SurfaceTexture surfaceTexture, int i, int i1) {
+ transformPreview();
+ }
+
+ @Override
+ public boolean onSurfaceTextureDestroyed(SurfaceTexture surfaceTexture) {
+ return false;
+ }
+
+ @Override
+ public void onSurfaceTextureUpdated(SurfaceTexture surfaceTexture) {
+ }
+ });
+ }
+
+ void transformPreview() {
+ String cameraId = null;
+ LensFacing viewFinderLensFacing =
+ ((CameraDeviceConfiguration) viewFinderUseCase.getUseCaseConfiguration())
+ .getLensFacing(/*valueIfMissing=*/ null);
+ if (viewFinderLensFacing != currentCameraLensFacing) {
+ throw new IllegalStateException(
+ "Invalid view finder lens facing: "
+ + viewFinderLensFacing
+ + " Should be: "
+ + currentCameraLensFacing);
+ }
+ try {
+ cameraId = CameraX.getCameraWithLensFacing(viewFinderLensFacing);
+ } catch (Exception e) {
+ throw new IllegalArgumentException(
+ "Unable to get camera id for lens facing " + viewFinderLensFacing, e);
+ }
+ Size srcResolution = viewFinderUseCase.getAttachedSurfaceResolution(cameraId);
+
+ if (srcResolution.getWidth() == 0 || srcResolution.getHeight() == 0) {
+ return;
+ }
+
+ TextureView textureView = this.findViewById(R.id.textureView);
+
+ if (textureView.getWidth() == 0 || textureView.getHeight() == 0) {
+ return;
+ }
+
+ Matrix matrix = new Matrix();
+
+ int left = textureView.getLeft();
+ int right = textureView.getRight();
+ int top = textureView.getTop();
+ int bottom = textureView.getBottom();
+
+ // Compute the viewfinder ui size based on the available width, height, and ui orientation.
+ int viewWidth = (right - left);
+ int viewHeight = (bottom - top);
+
+ int displayRotation = getDisplayRotation();
+ Size scaled =
+ calculateViewfinderViewDimens(
+ srcResolution, viewWidth, viewHeight, displayRotation);
+
+ // Compute the center of the view.
+ int centerX = viewWidth / 2;
+ int centerY = viewHeight / 2;
+
+ // Do corresponding rotation to correct the preview direction
+ matrix.postRotate(-getDisplayRotation(), centerX, centerY);
+
+ // Compute the scale value for center crop mode
+ float xScale = scaled.getWidth() / (float) viewWidth;
+ float yScale = scaled.getHeight() / (float) viewHeight;
+
+ if (getDisplayRotation() == 90 || getDisplayRotation() == 270) {
+ xScale = scaled.getWidth() / (float) viewHeight;
+ yScale = scaled.getHeight() / (float) viewWidth;
+ }
+
+ // Only two digits after the decimal point are valid for postScale. Need to get ceiling of
+ // two
+ // digits floating value to do the scale operation. Otherwise, the result may be scaled not
+ // large enough and will have some blank lines on the screen.
+ xScale = new BigDecimal(xScale).setScale(2, BigDecimal.ROUND_CEILING).floatValue();
+ yScale = new BigDecimal(yScale).setScale(2, BigDecimal.ROUND_CEILING).floatValue();
+
+ // Do corresponding scale to resolve the deformation problem
+ matrix.postScale(xScale, yScale, centerX, centerY);
+
+ // Compute the new left/top positions to do translate
+ int layoutL = centerX - (scaled.getWidth() / 2);
+ int layoutT = centerY - (scaled.getHeight() / 2);
+
+ // Do corresponding translation to be center crop
+ matrix.postTranslate(layoutL, layoutT);
+
+ textureView.setTransform(matrix);
+ }
+
+ /** @return One of 0, 90, 180, 270. */
+ private int getDisplayRotation() {
+ int displayRotation = getWindowManager().getDefaultDisplay().getRotation();
+
+ switch (displayRotation) {
+ case Surface.ROTATION_0:
+ displayRotation = 0;
+ break;
+ case Surface.ROTATION_90:
+ displayRotation = 90;
+ break;
+ case Surface.ROTATION_180:
+ displayRotation = 180;
+ break;
+ case Surface.ROTATION_270:
+ displayRotation = 270;
+ break;
+ default:
+ throw new UnsupportedOperationException(
+ "Unsupported display rotation: " + displayRotation);
+ }
+
+ return displayRotation;
+ }
+
+ private Size calculateViewfinderViewDimens(
+ Size srcSize, int parentWidth, int parentHeight, int displayRotation) {
+ int inWidth = srcSize.getWidth();
+ int inHeight = srcSize.getHeight();
+ if (displayRotation == 0 || displayRotation == 180) {
+ // Need to reverse the width and height since we're in landscape orientation.
+ inWidth = srcSize.getHeight();
+ inHeight = srcSize.getWidth();
+ }
+
+ int outWidth = parentWidth;
+ int outHeight = parentHeight;
+ if (inWidth != 0 && inHeight != 0) {
+ float vfRatio = inWidth / (float) inHeight;
+ float parentRatio = parentWidth / (float) parentHeight;
+
+ // Match shortest sides together.
+ if (vfRatio < parentRatio) {
+ outWidth = parentWidth;
+ outHeight = Math.round(parentWidth / vfRatio);
+ } else {
+ outWidth = Math.round(parentHeight * vfRatio);
+ outHeight = parentHeight;
+ }
+ }
+
+ return new Size(outWidth, outHeight);
+ }
+
+ /**
+ * Creates an image analysis use case.
+ *
+ * <p>This use case observes a stream of analysis results computed from the frames.
+ */
+ private void createImageAnalysisUseCase() {
+ Button button = this.findViewById(R.id.AnalysisToggle);
+ button.setBackgroundColor(Color.LTGRAY);
+ enableImageAnalysisUseCase();
+
+ button.setOnClickListener(
+ view -> {
+ Button buttonView = (Button) view;
+ if (imageAnalysisUseCase != null) {
+ // Remove the use case
+ buttonView.setBackgroundColor(Color.RED);
+ CameraX.unbind(imageAnalysisUseCase);
+ imageAnalysisUseCase = null;
+ } else {
+ // Add the use case
+ buttonView.setBackgroundColor(Color.LTGRAY);
+ enableImageAnalysisUseCase();
+ }
+ });
+
+ Log.i(TAG, "Got UseCase: " + imageAnalysisUseCase);
+ }
+
+ void enableImageAnalysisUseCase() {
+ ImageAnalysisUseCaseConfiguration configuration =
+ new ImageAnalysisUseCaseConfiguration.Builder()
+ .setLensFacing(currentCameraLensFacing)
+ .setTargetName("ImageAnalysis")
+ .setCallbackHandler(new Handler(Looper.getMainLooper()))
+ .build();
+
+ imageAnalysisUseCase = new ImageAnalysisUseCase(configuration);
+
+ TextView textView = this.findViewById(R.id.textView);
+
+ if (!bindToLifecycleSafely(imageAnalysisUseCase, R.id.AnalysisToggle)) {
+ imageAnalysisUseCase = null;
+ return;
+ }
+
+ imageAnalysisUseCase.setAnalyzer(
+ (image, rotationDegrees) -> {
+ // Since we set the callback handler to a main thread handler, we can call
+ // setValue()
+ // here. If we weren't on the main thread, we would have to call postValue()
+ // instead.
+ imageAnalysisResult.setValue(Long.toString(image.getTimestamp()));
+ });
+ imageAnalysisResult.observe(
+ this,
+ text -> {
+ if (imageAnalysisFrameCount.getAndIncrement() % 30 == 0) {
+ textView.setText(
+ "ImgCount: " + imageAnalysisFrameCount.get() + " @ts: " + text);
+ }
+ });
+ }
+
+ /**
+ * Creates an image capture use case.
+ *
+ * <p>This use case takes a picture and saves it to a file, whenever the user clicks a button.
+ */
+ private void createImageCaptureUseCase() {
+
+ Button button = this.findViewById(R.id.PhotoToggle);
+ button.setBackgroundColor(Color.LTGRAY);
+ enableImageCaptureUseCase();
+
+ button.setOnClickListener(
+ view -> {
+ Button buttonView = (Button) view;
+ if (imageCaptureUseCase != null) {
+ // Remove the use case
+ buttonView.setBackgroundColor(Color.RED);
+ disableImageCaptureUseCase();
+ } else {
+ // Add the use case
+ buttonView.setBackgroundColor(Color.LTGRAY);
+ enableImageCaptureUseCase();
+ }
+ });
+
+ Log.i(TAG, "Got UseCase: " + imageCaptureUseCase);
+ }
+
+ void enableImageCaptureUseCase() {
+ ImageCaptureUseCaseConfiguration configuration =
+ new ImageCaptureUseCaseConfiguration.Builder()
+ .setLensFacing(currentCameraLensFacing)
+ .setTargetName("ImageCapture")
+ .build();
+
+ imageCaptureUseCase = new ImageCaptureUseCase(configuration);
+
+ if (!bindToLifecycleSafely(imageCaptureUseCase, R.id.PhotoToggle)) {
+ Button button = this.findViewById(R.id.Picture);
+ button.setOnClickListener(null);
+ imageCaptureUseCase = null;
+ return;
+ }
+
+ Button button = this.findViewById(R.id.Picture);
+ final Format formatter = new SimpleDateFormat("yyyy-MM-dd-HH-mm-ss-SSS");
+ final File dir = this.getExternalFilesDir(null);
+ button.setOnClickListener(
+ view -> {
+ imageCaptureUseCase.takePicture(
+ new File(
+ dir,
+ formatter.format(Calendar.getInstance().getTime()) + ".jpg"),
+ new ImageCaptureUseCase.OnImageSavedListener() {
+ @Override
+ public void onImageSaved(File file) {
+ Log.d(TAG, "Saved image to " + file);
+ }
+
+ @Override
+ public void onError(
+ ImageCaptureUseCase.UseCaseError useCaseError,
+ String message,
+ Throwable cause) {
+ Log.e(TAG, "Failed to save image.", cause);
+ }
+ });
+ });
+ }
+
+ void disableImageCaptureUseCase() {
+ CameraX.unbind(imageCaptureUseCase);
+
+ imageCaptureUseCase = null;
+ Button button = this.findViewById(R.id.Picture);
+ button.setOnClickListener(null);
+ }
+
+ /**
+ * Creates a video capture use case.
+ *
+ * <p>This use case records a video segment and saves it to a file, in response to user button
+ * clicks.
+ */
+ private void createVideoCaptureUseCase() {
+ Button button = this.findViewById(R.id.VideoToggle);
+ button.setBackgroundColor(Color.LTGRAY);
+ enableVideoCaptureUseCase();
+
+ videoFileSaver = new VideoFileSaver();
+ videoFileSaver.setRootDirectory(
+ Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM));
+
+ button.setOnClickListener(
+ view -> {
+ Button buttonView = (Button) view;
+ if (videoCaptureUseCase != null) {
+ // Remove the use case
+ buttonView.setBackgroundColor(Color.RED);
+ disableVideoCaptureUseCase();
+ } else {
+ // Add the use case
+ buttonView.setBackgroundColor(Color.LTGRAY);
+ enableVideoCaptureUseCase();
+ }
+ });
+
+ Log.i(TAG, "Got UseCase: " + videoCaptureUseCase);
+ }
+
+ void enableVideoCaptureUseCase() {
+ VideoCaptureUseCaseConfiguration configuration =
+ new VideoCaptureUseCaseConfiguration.Builder()
+ .setLensFacing(currentCameraLensFacing)
+ .setTargetName("VideoCapture")
+ .build();
+
+ videoCaptureUseCase = new VideoCaptureUseCase(configuration);
+
+ if (!bindToLifecycleSafely(videoCaptureUseCase, R.id.VideoToggle)) {
+ Button button = this.findViewById(R.id.Video);
+ button.setOnClickListener(null);
+ videoCaptureUseCase = null;
+ return;
+ }
+
+ Button button = this.findViewById(R.id.Video);
+ button.setOnClickListener(
+ view -> {
+ Button buttonView = (Button) view;
+ String text = button.getText().toString();
+ if (text.equals("Record") && !videoFileSaver.isSaving()) {
+ videoCaptureUseCase.startRecording(
+ videoFileSaver.getNewVideoFile(), videoFileSaver);
+ videoFileSaver.setSaving();
+ buttonView.setText("Stop");
+ } else if (text.equals("Stop") && videoFileSaver.isSaving()) {
+ buttonView.setText("Record");
+ videoCaptureUseCase.stopRecording();
+ } else if (text.equals("Record") && videoFileSaver.isSaving()) {
+ buttonView.setText("Stop");
+ videoFileSaver.setSaving();
+ } else if (text.equals("Stop") && !videoFileSaver.isSaving()) {
+ buttonView.setText("Record");
+ }
+ });
+ }
+
+ void disableVideoCaptureUseCase() {
+ Button button = this.findViewById(R.id.Video);
+ button.setOnClickListener(null);
+ CameraX.unbind(videoCaptureUseCase);
+
+ videoCaptureUseCase = null;
+ }
+
+ /** Creates all the use cases. */
+ private void createUseCases() {
+ createImageCaptureUseCase();
+ createViewFinderUseCase();
+ createImageAnalysisUseCase();
+ createVideoCaptureUseCase();
+ }
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_camera_xmain);
+
+ StrictMode.VmPolicy policy =
+ new StrictMode.VmPolicy.Builder().detectAll().penaltyLog().build();
+ StrictMode.setVmPolicy(policy);
+
+ // 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) {
+ currentCameraDirection = newCameraDirection;
+ }
+ }
+
+ new Thread(
+ () -> {
+ setupCamera();
+ })
+ .start();
+ setupPermissions();
+ }
+
+ private void setupCamera() {
+ try {
+ // Wait for permissions before proceeding.
+ if (!completableFuture.get()) {
+ Log.d(TAG, "Permissions denied.");
+ return;
+ }
+ } catch (Exception e) {
+ Log.e(TAG, "Exception occurred getting permission future: " + e);
+ }
+
+ Log.d(TAG, "Camera direction: " + currentCameraDirection);
+ if (currentCameraDirection.equalsIgnoreCase("BACKWARD")) {
+ currentCameraLensFacing = LensFacing.BACK;
+ } else if (currentCameraDirection.equalsIgnoreCase("FORWARD")) {
+ currentCameraLensFacing = LensFacing.FRONT;
+ } else {
+ throw new RuntimeException("Invalid camera direction: " + currentCameraDirection);
+ }
+ Log.d(TAG, "Using camera lens facing: " + currentCameraLensFacing);
+
+ // Run this on the UI thread to manipulate the Textures & Views.
+ CameraXActivity.this.runOnUiThread(
+ () -> {
+ createUseCases();
+ });
+ }
+
+ private void setupPermissions() {
+ if (!allPermissionsGranted()) {
+ makePermissionRequest();
+ } else {
+ settableResult.set(true);
+ completableFuture.run();
+ }
+ }
+
+ private void makePermissionRequest() {
+ ActivityCompat.requestPermissions(this, getRequiredPermissions(), PERMISSIONS_REQUEST_CODE);
+ }
+
+ /** Returns true if all the necessary permissions have been granted already. */
+ private boolean allPermissionsGranted() {
+ for (String permission : getRequiredPermissions()) {
+ if (ContextCompat.checkSelfPermission(this, permission)
+ != PackageManager.PERMISSION_GRANTED) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /** Tries to acquire all the necessary permissions through a dialog. */
+ private String[] getRequiredPermissions() {
+ PackageInfo info;
+ try {
+ info =
+ getPackageManager()
+ .getPackageInfo(getPackageName(), PackageManager.GET_PERMISSIONS);
+ } catch (NameNotFoundException exception) {
+ Log.e(TAG, "Failed to obtain all required permissions.", exception);
+ return new String[0];
+ }
+ String[] permissions = info.requestedPermissions;
+ if (permissions != null && permissions.length > 0) {
+ return permissions;
+ } else {
+ return new String[0];
+ }
+ }
+
+ @Override
+ public void onRequestPermissionsResult(
+ int requestCode, String[] permissions, int[] grantResults) {
+ switch (requestCode) {
+ case PERMISSIONS_REQUEST_CODE: {
+ // If request is cancelled, the result arrays are empty.
+ if (grantResults.length > 0
+ && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
+ Log.d(TAG, "Permissions Granted.");
+ settableResult.set(true);
+ completableFuture.run();
+ } else {
+ Log.d(TAG, "Permissions Denied.");
+ settableResult.set(false);
+ completableFuture.run();
+ }
+ return;
+ }
+ default:
+ // No-op
+ }
+ }
+
+ private boolean bindToLifecycleSafely(BaseUseCase useCase, int buttonViewId) {
+ try {
+ CameraX.bindToLifecycle(this, useCase);
+ } catch (IllegalArgumentException e) {
+ Log.e(TAG, e.getMessage());
+ Toast.makeText(getApplicationContext(), "Bind too many use cases.", Toast.LENGTH_SHORT)
+ .show();
+ Button button = this.findViewById(buttonViewId);
+ button.setBackgroundColor(Color.RED);
+ return false;
+ }
+
+ return true;
+ }
+
+ /** A {@link Callable} whose return value can be set. */
+ private static final class SettableCallable<V> implements Callable<V> {
+ private final AtomicReference<V> value = new AtomicReference<>();
+
+ public void set(V value) {
+ this.value.set(value);
+ }
+
+ @Override
+ public V call() {
+ return value.get();
+ }
+ }
+}
diff --git a/camera/integration-tests/hellocamerax/src/main/java/androidx/camera/testapp/hellocamerax/CameraXApplication.java b/camera/integration-tests/hellocamerax/src/main/java/androidx/camera/testapp/hellocamerax/CameraXApplication.java
new file mode 100644
index 0000000..0ff2227
--- /dev/null
+++ b/camera/integration-tests/hellocamerax/src/main/java/androidx/camera/testapp/hellocamerax/CameraXApplication.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2019 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.testapp.hellocamerax;
+
+import android.app.Application;
+
+/** An application for CameraX. */
+public class CameraXApplication extends Application {
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+ }
+}
diff --git a/camera/integration-tests/hellocamerax/src/main/java/androidx/camera/testapp/hellocamerax/VideoFileSaver.java b/camera/integration-tests/hellocamerax/src/main/java/androidx/camera/testapp/hellocamerax/VideoFileSaver.java
new file mode 100644
index 0000000..c95858b
--- /dev/null
+++ b/camera/integration-tests/hellocamerax/src/main/java/androidx/camera/testapp/hellocamerax/VideoFileSaver.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright (C) 2019 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.testapp.hellocamerax;
+
+import android.support.annotation.GuardedBy;
+import android.support.annotation.Nullable;
+import android.util.Log;
+
+import androidx.camera.core.VideoCaptureUseCase.OnVideoSavedListener;
+import androidx.camera.core.VideoCaptureUseCase.UseCaseError;
+
+import java.io.File;
+import java.text.Format;
+import java.text.SimpleDateFormat;
+import java.util.Calendar;
+import java.util.Date;
+import java.util.Locale;
+
+/**
+ * Basic functionality required for interfacing the {@link
+ * androidx.camera.core.VideoCaptureUseCase}.
+ */
+public class VideoFileSaver implements OnVideoSavedListener {
+ private static final String TAG = "VideoFileSaver";
+ private final Format formatter = new SimpleDateFormat("yyyy-MM-dd-HH-mm-ss", Locale.ENGLISH);
+ private final Object lock = new Object();
+ private File rootDirectory;
+ @GuardedBy("lock")
+ private boolean isSaving = false;
+
+ @Override
+ public void onVideoSaved(File file) {
+
+ Log.d(TAG, "Saved file: " + file.getPath());
+ synchronized (lock) {
+ isSaving = false;
+ }
+ }
+
+ @Override
+ public void onError(UseCaseError useCaseError, String message, @Nullable Throwable cause) {
+
+ Log.e(TAG, "Error: " + useCaseError + ", " + message);
+ if (cause != null) {
+ Log.e(TAG, "Error cause: " + cause.getCause());
+ }
+
+ synchronized (lock) {
+ isSaving = false;
+ }
+ }
+
+ /** Returns a new {@link File} where to save a video. */
+ public File getNewVideoFile() {
+ Date date = Calendar.getInstance().getTime();
+ File file = new File(rootDirectory + "/" + formatter.format(date) + ".mp4");
+ return file;
+ }
+
+ /** Sets the directory for saving files. */
+ public void setRootDirectory(File rootDirectory) {
+ this.rootDirectory = rootDirectory;
+ }
+
+ boolean isSaving() {
+ synchronized (lock) {
+ return isSaving;
+ }
+ }
+
+ /** Sets saving state after video startRecording */
+ void setSaving() {
+ synchronized (lock) {
+ isSaving = true;
+ }
+ }
+}
diff --git a/camera/integration-tests/hellocamerax/src/main/res/layout/activity_camera_xmain.xml b/camera/integration-tests/hellocamerax/src/main/res/layout/activity_camera_xmain.xml
new file mode 100644
index 0000000..eddc73d
--- /dev/null
+++ b/camera/integration-tests/hellocamerax/src/main/res/layout/activity_camera_xmain.xml
@@ -0,0 +1,155 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2019 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.
+-->
+<android.support.constraint.ConstraintLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:id="@+id/constraintLayout"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ tools:context="androidx.camera.app.CameraXActivity">
+
+ <TextureView
+ android:id="@+id/textureView"
+ android:layout_width="0dp"
+ android:layout_height="0dp"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent" />
+
+ <TextView
+ android:id="@+id/textView"
+ android:layout_width="0dp"
+ android:layout_height="0dp"
+ android:background="#FFF"
+ android:elevation="2dp"
+ android:scaleType="fitXY"
+ android:src="@android:drawable/btn_radio"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintDimensionRatio="4:3"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintHorizontal_bias="1.0"
+ app:layout_constraintStart_toStartOf="@+id/guideline"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintVertical_bias="1.0" />
+
+ <android.support.constraint.Guideline
+ android:id="@+id/guideline"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ app:layout_constraintGuide_begin="0dp"
+ app:layout_constraintGuide_percent="0.7" />
+
+ <android.support.constraint.Guideline
+ android:id="@+id/takepicture"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ app:layout_constraintGuide_begin="0dp"
+ app:layout_constraintGuide_percent="0.1" />
+
+ <android.support.constraint.Guideline
+ android:id="@+id/takevideo"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ app:layout_constraintGuide_begin="0dp"
+ app:layout_constraintGuide_percent="0.4" />
+
+ <Button
+ android:id="@+id/Picture"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:scaleType="fitXY"
+ android:text="Picture"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintHorizontal_bias="0.0"
+ app:layout_constraintStart_toStartOf="@+id/takepicture"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintVertical_bias="1.0" />
+
+ <Button
+ android:id="@+id/Video"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:scaleType="fitXY"
+ android:text="Record"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintHorizontal_bias="0.0"
+ app:layout_constraintStart_toStartOf="@+id/takevideo"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintVertical_bias="1.0" />
+
+ <Button
+ android:id="@+id/VideoToggle"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:scaleType="fitXY"
+ android:text="Video"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintHorizontal_bias="0"
+ app:layout_constraintStart_toStartOf="@+id/constraintLayout"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintVertical_bias="0"
+ />
+
+ <Button
+ android:id="@+id/PhotoToggle"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:scaleType="fitXY"
+ android:text="Photo"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintHorizontal_bias="0.333"
+ app:layout_constraintStart_toStartOf="@+id/constraintLayout"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintVertical_bias="0"
+ />
+
+ <Button
+ android:id="@+id/AnalysisToggle"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:scaleType="fitXY"
+ android:text="Analysis"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintHorizontal_bias="0.666"
+ app:layout_constraintStart_toStartOf="@+id/constraintLayout"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintVertical_bias="0"
+ />
+
+ <Button
+ android:id="@+id/PreviewToggle"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:scaleType="fitXY"
+ android:text="Preview"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintHorizontal_bias="1.0"
+ app:layout_constraintStart_toStartOf="@+id/constraintLayout"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintVertical_bias="0"
+ />
+</android.support.constraint.ConstraintLayout>
diff --git a/camera/integration-tests/hellocamerax/src/main/res/values/strings.xml b/camera/integration-tests/hellocamerax/src/main/res/values/strings.xml
new file mode 100644
index 0000000..5fdbbbf
--- /dev/null
+++ b/camera/integration-tests/hellocamerax/src/main/res/values/strings.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2019 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.
+-->
+<resources />
+ </resources>
diff --git a/camera/integration-tests/hellocamerax/src/main/res/values/style.xml b/camera/integration-tests/hellocamerax/src/main/res/values/style.xml
new file mode 100644
index 0000000..7503cc0
--- /dev/null
+++ b/camera/integration-tests/hellocamerax/src/main/res/values/style.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2019 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.
+-->
+<resources>
+ <!-- Base application theme. -->
+ <style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
+ <!-- Customize your theme here. -->
+ </style>
+</resources>
diff --git a/camera/integration-tests/javacameraxpermissions/build.gradle b/camera/integration-tests/javacameraxpermissions/build.gradle
new file mode 100644
index 0000000..e59bb5e
--- /dev/null
+++ b/camera/integration-tests/javacameraxpermissions/build.gradle
@@ -0,0 +1,79 @@
+/*
+ * Copyright (C) 2019 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.
+ */
+
+apply plugin: 'com.android.application'
+
+android {
+ compileOptions {
+ sourceCompatibility project.ext.javaVersion
+ targetCompatibility project.ext.javaVersion
+ }
+
+ compileSdkVersion project.ext.compileSdk
+
+ defaultConfig {
+ applicationId "androidx.camera.testapp.javacameraxpermissions"
+ minSdkVersion project.ext.minSdk
+ targetSdkVersion project.ext.targetSdk
+ versionCode 1
+ versionName project.ext.version
+ multiDexEnabled true
+ }
+
+ sourceSets {
+ main.manifest.srcFile 'src/main/AndroidManifest.xml'
+ main.java.srcDirs = ['src/main/java']
+ main.java.excludes = ['**/build/**']
+ main.java.includes = ['**/*.java']
+ main.res.srcDirs = ['src/main/res']
+ }
+
+ buildTypes {
+ debug {
+ testCoverageEnabled true
+ }
+
+ release {
+ minifyEnabled true
+ proguardFiles getDefaultProguardFile('proguard-android.txt')
+ }
+ }
+
+ lintOptions {
+ abortOnError false
+ }
+}
+
+dependencies {
+ // Internal library
+ implementation project(':camera2')
+ implementation project(':core')
+
+ // Lifecycle and LiveData
+ implementation "android.arch.lifecycle:extensions:${project.ext.lifecycleVersion}"
+ implementation "android.arch.lifecycle:livedata:${project.ext.lifecycleVersion}"
+ implementation "android.arch.lifecycle:runtime:${project.ext.lifecycleVersion}"
+ implementation "android.arch.lifecycle:common-java8:${project.ext.lifecycleVersion}"
+
+ // Android Support Library
+ implementation "com.android.support:appcompat-v7:${project.ext.supportLibraryVersion}"
+ implementation "com.android.support.constraint:constraint-layout:1.0.2"
+ implementation "com.android.support:support-compat:${project.ext.supportLibraryVersion}"
+ implementation "com.android.support:support-annotations:${project.ext.supportLibraryVersion}"
+ implementation "com.android.support:support-v13:${project.ext.supportLibraryVersion}"
+}
+
diff --git a/camera/integration-tests/javacameraxpermissions/src/main/AndroidManifest.xml b/camera/integration-tests/javacameraxpermissions/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..c9c6a7b
--- /dev/null
+++ b/camera/integration-tests/javacameraxpermissions/src/main/AndroidManifest.xml
@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2019 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.
+-->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="androidx.camera.testapp.javacameraxpermissions">
+
+ <uses-sdk
+ android:minSdkVersion="21"
+ android:targetSdkVersion="27" />
+
+ <uses-permission android:name="android.permission.CAMERA" />
+
+ <uses-feature android:name="android.hardware.camera" />
+
+ <application
+ android:allowBackup="true"
+ android:label="@string/app_name"
+ android:theme="@style/Theme.AppCompat">
+
+ <activity
+ android:name=".MainActivity"
+ android:label="@string/app_name">
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN" />
+ <category android:name="android.intent.category.LAUNCHER" />
+ </intent-filter>
+ </activity>
+ </application>
+
+</manifest>
diff --git a/camera/integration-tests/javacameraxpermissions/src/main/java/androidx/camera/testapp/javacameraxpermissions/MainActivity.java b/camera/integration-tests/javacameraxpermissions/src/main/java/androidx/camera/testapp/javacameraxpermissions/MainActivity.java
new file mode 100644
index 0000000..3eeb1b2
--- /dev/null
+++ b/camera/integration-tests/javacameraxpermissions/src/main/java/androidx/camera/testapp/javacameraxpermissions/MainActivity.java
@@ -0,0 +1,122 @@
+/*
+ * Copyright (C) 2019 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.testapp.javacameraxpermissions;
+
+import android.Manifest;
+import android.app.Activity;
+import android.content.pm.PackageManager;
+import android.os.Bundle;
+import android.support.annotation.Nullable;
+import android.support.v4.app.ActivityCompat;
+import android.support.v4.content.ContextCompat;
+import android.support.v7.app.AppCompatActivity;
+import android.util.Log;
+import android.view.TextureView;
+import android.view.ViewGroup;
+
+import androidx.camera.core.CameraX;
+import androidx.camera.core.ViewFinderUseCase;
+import androidx.camera.core.ViewFinderUseCaseConfiguration;
+
+import java.util.concurrent.CompletableFuture;
+
+@SuppressWarnings("AndroidJdkLibsChecker")
+public class MainActivity extends AppCompatActivity {
+ private static final String TAG = MainActivity.class.getName();
+ private static final int CAMERA_REQUEST_CODE = 101;
+ private final CompletableFuture<Integer> cf = new CompletableFuture<>();
+
+ private static void makePermissionRequest(Activity context) {
+ ActivityCompat.requestPermissions(
+ context, new String[]{Manifest.permission.CAMERA}, CAMERA_REQUEST_CODE);
+ }
+
+ @Override
+ protected void onCreate(@Nullable Bundle bundle) {
+ super.onCreate(bundle);
+ setContentView(R.layout.activity_main);
+ new Thread(
+ () -> {
+ setupCamera();
+ })
+ .start();
+ setupPermissions(this);
+ }
+
+ private void setupCamera() {
+ try {
+ // Wait for permissions before proceeding.
+ if (cf.get() == PackageManager.PERMISSION_DENIED) {
+ Log.i(TAG, "Permission to open camera denied.");
+ return;
+ }
+ } catch (Exception e) {
+ Log.i(TAG, "Exception occurred getting permission future: " + e);
+ }
+
+ // Run this on the UI thread to manipulate the Textures & Views.
+ MainActivity.this.runOnUiThread(
+ () -> {
+ ViewFinderUseCaseConfiguration vfConfig =
+ new ViewFinderUseCaseConfiguration.Builder()
+ .setTargetName("vf0")
+ .build();
+ TextureView textureView = findViewById(R.id.textureView);
+ ViewFinderUseCase vfUseCase = new ViewFinderUseCase(vfConfig);
+
+ vfUseCase.setOnViewFinderOutputUpdateListener(
+ viewFinderOutput -> {
+ // If TextureView was already created, need to re-add it to change
+ // the SurfaceTexture.
+ ViewGroup v = (ViewGroup) textureView.getParent();
+ v.removeView(textureView);
+ v.addView(textureView);
+ textureView.setSurfaceTexture(viewFinderOutput.getSurfaceTexture());
+ });
+ CameraX.bindToLifecycle(/* lifecycleOwner= */ this, vfUseCase);
+ });
+ }
+
+ private void setupPermissions(Activity context) {
+ int permission = ContextCompat.checkSelfPermission(context, Manifest.permission.CAMERA);
+ if (permission != PackageManager.PERMISSION_GRANTED) {
+ makePermissionRequest(context);
+ } else {
+ cf.complete(permission);
+ }
+ }
+
+ @Override
+ public void onRequestPermissionsResult(
+ int requestCode, String[] permissions, int[] grantResults) {
+ switch (requestCode) {
+ case CAMERA_REQUEST_CODE: {
+ // If request is cancelled, the result arrays are empty.
+ if (grantResults.length > 0
+ && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
+ Log.i(TAG, "Camera Permission Granted.");
+ } else {
+ Log.i(TAG, "Camera Permission Denied.");
+ }
+ cf.complete(grantResults[0]);
+ return;
+ }
+ default: {
+ }
+ }
+ }
+}
diff --git a/camera/integration-tests/javacameraxpermissions/src/main/res/layout/activity_main.xml b/camera/integration-tests/javacameraxpermissions/src/main/res/layout/activity_main.xml
new file mode 100644
index 0000000..aeb586e
--- /dev/null
+++ b/camera/integration-tests/javacameraxpermissions/src/main/res/layout/activity_main.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2019 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.
+-->
+<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ app:layout_constraintHeight_min="640dp"
+ tools:context="androidx.camera.app.javacameraxpermissions.MainActivity">
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical">
+
+ <TextureView
+ android:id="@+id/textureView"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:minHeight="177dp" />
+ </LinearLayout>
+
+</android.support.constraint.ConstraintLayout>
diff --git a/camera/integration-tests/javacameraxpermissions/src/main/res/values/strings.xml b/camera/integration-tests/javacameraxpermissions/src/main/res/values/strings.xml
new file mode 100644
index 0000000..3ae3d90
--- /dev/null
+++ b/camera/integration-tests/javacameraxpermissions/src/main/res/values/strings.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2019 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.
+-->
+<resources>
+ <string name="app_name">CameraXPermissions</string>
+</resources>
diff --git a/camera/integration-tests/timingapp/build.gradle b/camera/integration-tests/timingapp/build.gradle
new file mode 100644
index 0000000..b3939a4
--- /dev/null
+++ b/camera/integration-tests/timingapp/build.gradle
@@ -0,0 +1,79 @@
+/*
+ * Copyright (C) 2019 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.
+ */
+
+apply plugin: 'com.android.application'
+
+android {
+ compileOptions {
+ sourceCompatibility project.ext.javaVersion
+ targetCompatibility project.ext.javaVersion
+ }
+
+ compileSdkVersion project.ext.compileSdk
+
+ defaultConfig {
+ applicationId "androidx.camera.testapp.timingapp"
+ minSdkVersion project.ext.minSdk
+ targetSdkVersion project.ext.targetSdk
+ versionCode 1
+ versionName project.ext.version
+ multiDexEnabled true
+ }
+
+ sourceSets {
+ main.manifest.srcFile 'src/main/AndroidManifest.xml'
+ main.java.srcDirs = ['src/main/java']
+ main.java.excludes = ['**/build/**']
+ main.java.includes = ['**/*.java']
+ main.res.srcDirs = ['src/main/res']
+ }
+
+ buildTypes {
+ debug {
+ testCoverageEnabled true
+ }
+
+ release {
+ minifyEnabled true
+ proguardFiles getDefaultProguardFile('proguard-android.txt')
+ }
+ }
+
+ lintOptions {
+ abortOnError false
+ }
+}
+
+dependencies {
+ // Internal library
+ implementation project(':camera2')
+ implementation project(':core')
+
+ // Lifecycle and LiveData
+ implementation "android.arch.lifecycle:extensions:${project.ext.lifecycleVersion}"
+ implementation "android.arch.lifecycle:livedata:${project.ext.lifecycleVersion}"
+ implementation "android.arch.lifecycle:runtime:${project.ext.lifecycleVersion}"
+ implementation "android.arch.lifecycle:common-java8:${project.ext.lifecycleVersion}"
+
+ // Android Support Library
+ implementation "com.android.support:appcompat-v7:${project.ext.supportLibraryVersion}"
+ implementation "com.android.support.constraint:constraint-layout:1.0.2"
+ implementation "com.android.support:support-compat:${project.ext.supportLibraryVersion}"
+ implementation "com.android.support:support-annotations:${project.ext.supportLibraryVersion}"
+ implementation "com.android.support:support-v13:${project.ext.supportLibraryVersion}"
+}
+
diff --git a/camera/integration-tests/timingapp/src/main/AndroidManifest.xml b/camera/integration-tests/timingapp/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..5f47d8c
--- /dev/null
+++ b/camera/integration-tests/timingapp/src/main/AndroidManifest.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2019 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.
+-->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="androidx.camera.testapp.timingapp">
+
+ <uses-sdk
+ android:minSdkVersion="21"
+ android:targetSdkVersion="27" />
+
+ <uses-permission android:name="android.permission.CAMERA" />
+ <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
+ <uses-permission android:name="android.permission.RECORD_AUDIO" />
+
+ <uses-feature android:name="android.hardware.camera" />
+
+ <application android:theme="@style/AppTheme">
+ <activity
+ android:name=".TakePhotoActivity"
+ android:label="Taking Photo">
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN" />
+ <category android:name="android.intent.category.LAUNCHER" />
+ </intent-filter>
+ </activity>
+ </application>
+</manifest>
diff --git a/camera/integration-tests/timingapp/src/main/java/androidx/camera/testapp/timingapp/BaseActivity.java b/camera/integration-tests/timingapp/src/main/java/androidx/camera/testapp/timingapp/BaseActivity.java
new file mode 100644
index 0000000..e5cacda
--- /dev/null
+++ b/camera/integration-tests/timingapp/src/main/java/androidx/camera/testapp/timingapp/BaseActivity.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2019 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.testapp.timingapp;
+
+import android.os.Bundle;
+import android.support.v7.app.AppCompatActivity;
+
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * An activity used to run performance test case.
+ *
+ * <p>To run performance test case, please implement this Activity. Camerax Use Case can be
+ * implement in prepareUseCase and runUseCase. For performance result, you can set currentTimeMillis
+ * to startTime and store the execution time into totalTime. At the end of test case, please call
+ * onUseCaseFinish() to notify the lock.
+ */
+public abstract class BaseActivity extends AppCompatActivity {
+ public static final long MICROS_IN_SECOND = TimeUnit.SECONDS.toMillis(1);
+ public static final long PREVIEW_FILL_BUFFER_TIME = 1500;
+ private static final String TAG = "BaseActivity";
+ public long startTime;
+ public long totalTime;
+ public long openCameraStartTime;
+ public long openCameraTotalTime;
+ public long startRreviewTime;
+ public long startPreviewTotalTime;
+ public long previewFrameRate;
+ public long closeCameraStartTime;
+ public long closeCameraTotalTime;
+ public String imageResolution;
+ public CountDownLatch latch;
+
+ public abstract void prepareUseCase();
+
+ public abstract void runUseCase() throws InterruptedException;
+
+ public void onUseCaseFinish() {
+ latch.countDown();
+ }
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ latch = new CountDownLatch(1);
+ }
+}
diff --git a/camera/integration-tests/timingapp/src/main/java/androidx/camera/testapp/timingapp/CustomLifecycle.java b/camera/integration-tests/timingapp/src/main/java/androidx/camera/testapp/timingapp/CustomLifecycle.java
new file mode 100644
index 0000000..926d84f
--- /dev/null
+++ b/camera/integration-tests/timingapp/src/main/java/androidx/camera/testapp/timingapp/CustomLifecycle.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2019 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.testapp.timingapp;
+
+import android.arch.lifecycle.Lifecycle;
+import android.arch.lifecycle.Lifecycle.State;
+import android.arch.lifecycle.LifecycleOwner;
+import android.arch.lifecycle.LifecycleRegistry;
+import android.os.Handler;
+import android.os.Looper;
+import android.support.annotation.NonNull;
+
+/** A customized lifecycle owner which obeys the lifecycle transition rules. */
+public final class CustomLifecycle implements LifecycleOwner {
+ private final LifecycleRegistry lifecycleRegistry;
+ private final Handler mainHandler = new Handler(Looper.getMainLooper());
+
+ public CustomLifecycle() {
+ lifecycleRegistry = new LifecycleRegistry(this);
+ lifecycleRegistry.markState(Lifecycle.State.INITIALIZED);
+ lifecycleRegistry.markState(Lifecycle.State.CREATED);
+ }
+
+ @NonNull
+ @Override
+ public Lifecycle getLifecycle() {
+ return lifecycleRegistry;
+ }
+
+ public void doOnResume() {
+ if (Looper.getMainLooper() != Looper.myLooper()) {
+ mainHandler.post(() -> doOnResume());
+ return;
+ }
+ lifecycleRegistry.markState(State.RESUMED);
+ }
+
+ public void doDestroyed() {
+ if (Looper.getMainLooper() != Looper.myLooper()) {
+ mainHandler.post(() -> doDestroyed());
+ return;
+ }
+ lifecycleRegistry.markState(State.DESTROYED);
+ }
+}
diff --git a/camera/integration-tests/timingapp/src/main/java/androidx/camera/testapp/timingapp/TakePhotoActivity.java b/camera/integration-tests/timingapp/src/main/java/androidx/camera/testapp/timingapp/TakePhotoActivity.java
new file mode 100644
index 0000000..93adba9
--- /dev/null
+++ b/camera/integration-tests/timingapp/src/main/java/androidx/camera/testapp/timingapp/TakePhotoActivity.java
@@ -0,0 +1,272 @@
+/*
+ * Copyright (C) 2019 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.testapp.timingapp;
+
+import android.graphics.SurfaceTexture;
+import android.hardware.camera2.CameraCaptureSession;
+import android.hardware.camera2.CameraDevice;
+import android.os.Bundle;
+import android.util.Log;
+import android.view.TextureView;
+import android.view.TextureView.SurfaceTextureListener;
+import android.view.ViewGroup;
+import android.widget.Button;
+
+import androidx.camera.camera2.Camera2Configuration;
+import androidx.camera.core.CameraX;
+import androidx.camera.core.CameraX.LensFacing;
+import androidx.camera.core.ImageCaptureUseCase;
+import androidx.camera.core.ImageCaptureUseCase.CaptureMode;
+import androidx.camera.core.ImageCaptureUseCaseConfiguration;
+import androidx.camera.core.ImageProxy;
+import androidx.camera.core.ViewFinderUseCase;
+import androidx.camera.core.ViewFinderUseCaseConfiguration;
+
+import com.google.common.base.Ascii;
+
+/** This Activity is used to run image capture performance test in mobileharness. */
+public class TakePhotoActivity extends BaseActivity {
+
+ private static final String TAG = "TakePhotoActivity";
+ // How many sample frames we should use to calculate framerate.
+ private static final int FRAMERATE_SAMPLE_WINDOW = 5;
+ private static final String EXTRA_CAPTURE_MODE = "capture_mode";
+ private static final String EXTRA_CAMERA_FACING = "camera_facing";
+ private static final String CAMERA_FACING_FRONT = "FRONT";
+ private static final String CAMERA_FACING_BACK = "BACK";
+ private final String defaultCameraFacing = CAMERA_FACING_BACK;
+ private final CameraDevice.StateCallback deviceStateCallback =
+ new CameraDevice.StateCallback() {
+
+ @Override
+ public void onOpened(CameraDevice cameraDevice) {
+ openCameraTotalTime = System.currentTimeMillis() - openCameraStartTime;
+ Log.d(TAG, "[onOpened] openCameraTotalTime: " + openCameraTotalTime);
+ startRreviewTime = System.currentTimeMillis();
+ }
+
+ @Override
+ public void onClosed(CameraDevice camera) {
+ super.onClosed(camera);
+ closeCameraTotalTime = System.currentTimeMillis() - closeCameraStartTime;
+ Log.d(TAG, "[onClosed] closeCameraTotalTime: " + closeCameraTotalTime);
+ onUseCaseFinish();
+ }
+
+ @Override
+ public void onDisconnected(CameraDevice cameraDevice) {
+ }
+
+ @Override
+ public void onError(CameraDevice cameraDevice, int i) {
+ Log.e(TAG, "[onError] open camera failed, error code: " + i);
+ }
+ };
+ private final CameraCaptureSession.StateCallback captureSessionStateCallback =
+ new CameraCaptureSession.StateCallback() {
+
+ @Override
+ public void onActive(CameraCaptureSession session) {
+ super.onActive(session);
+ startPreviewTotalTime = System.currentTimeMillis() - startRreviewTime;
+ Log.d(TAG, "[onActive] previewStartTotalTime: " + startPreviewTotalTime);
+ }
+
+ @Override
+ public void onConfigured(CameraCaptureSession cameraCaptureSession) {
+ Log.d(TAG, "[onConfigured] CaptureSession configured!");
+ }
+
+ @Override
+ public void onConfigureFailed(CameraCaptureSession cameraCaptureSession) {
+ Log.e(TAG, "[onConfigureFailed] CameraX preview initialization failed.");
+ }
+ };
+ /** The default cameraId to use. */
+ private LensFacing currentCameraLensFacing = LensFacing.BACK;
+ private ImageCaptureUseCase imageCaptureUseCase;
+ private ViewFinderUseCase viewFinderUseCase;
+ private int frameCount;
+ private long previewSampleStartTime;
+ private CaptureMode captureMode = CaptureMode.MIN_LATENCY;
+ private CustomLifecycle customLifecycle;
+
+ @Override
+ public void runUseCase() throws InterruptedException {
+
+ // Length of time to let the preview stream run before capturing the first image.
+ // This can help ensure capture latency is real latency and not merely the device
+ // filling the buffer.
+ Thread.sleep(PREVIEW_FILL_BUFFER_TIME);
+
+ startTime = System.currentTimeMillis();
+ imageCaptureUseCase.takePicture(
+ new ImageCaptureUseCase.OnImageCapturedListener() {
+ @Override
+ public void onCaptureSuccess(ImageProxy image, int rotationDegrees) {
+ totalTime = System.currentTimeMillis() - startTime;
+ if (image != null) {
+ imageResolution = image.getWidth() + "x" + image.getHeight();
+ } else {
+ Log.e(TAG, "[onCaptureSuccess] image is null");
+ }
+ }
+ });
+ }
+
+ @Override
+ public void prepareUseCase() {
+ createViewFinderUseCase();
+ createImageCaptureUseCase();
+ }
+
+ void createViewFinderUseCase() {
+ ViewFinderUseCaseConfiguration.Builder configurationBuilder =
+ new ViewFinderUseCaseConfiguration.Builder()
+ .setLensFacing(currentCameraLensFacing)
+ .setTargetName("ViewFinder");
+
+ new Camera2Configuration.Extender(configurationBuilder)
+ .setDeviceStateCallback(deviceStateCallback)
+ .setSessionStateCallback(captureSessionStateCallback);
+
+ viewFinderUseCase = new ViewFinderUseCase(configurationBuilder.build());
+ openCameraStartTime = System.currentTimeMillis();
+
+ viewFinderUseCase.setOnViewFinderOutputUpdateListener(
+ viewFinderOutput -> {
+ TextureView textureView = this.findViewById(R.id.textureView);
+ ViewGroup viewGroup = (ViewGroup) textureView.getParent();
+ viewGroup.removeView(textureView);
+ viewGroup.addView(textureView);
+ textureView.setSurfaceTexture(viewFinderOutput.getSurfaceTexture());
+ textureView.setSurfaceTextureListener(
+ new SurfaceTextureListener() {
+ @Override
+ public void onSurfaceTextureAvailable(
+ SurfaceTexture surfaceTexture, int i, int i1) {
+ }
+
+ @Override
+ public void onSurfaceTextureSizeChanged(
+ SurfaceTexture surfaceTexture, int i, int i1) {
+ }
+
+ @Override
+ public boolean onSurfaceTextureDestroyed(
+ SurfaceTexture surfaceTexture) {
+ return false;
+ }
+
+ @Override
+ public void onSurfaceTextureUpdated(SurfaceTexture surfaceTexture) {
+ Log.d(TAG, "[onSurfaceTextureUpdated]");
+ if (0 == totalTime) {
+ return;
+ }
+
+ if (0 == frameCount) {
+ previewSampleStartTime = System.currentTimeMillis();
+ } else if (FRAMERATE_SAMPLE_WINDOW == frameCount) {
+ final long duration =
+ System.currentTimeMillis() - previewSampleStartTime;
+ previewFrameRate =
+ (MICROS_IN_SECOND
+ * FRAMERATE_SAMPLE_WINDOW
+ / duration);
+ closeCameraStartTime = System.currentTimeMillis();
+ customLifecycle.doDestroyed();
+ }
+ frameCount++;
+ }
+ });
+ });
+
+ CameraX.bindToLifecycle(customLifecycle, viewFinderUseCase);
+ }
+
+ void createImageCaptureUseCase() {
+ ImageCaptureUseCaseConfiguration configuration =
+ new ImageCaptureUseCaseConfiguration.Builder()
+ .setTargetName("ImageCapture")
+ .setLensFacing(currentCameraLensFacing)
+ .setCaptureMode(captureMode)
+ .build();
+
+ imageCaptureUseCase = new ImageCaptureUseCase(configuration);
+ CameraX.bindToLifecycle(customLifecycle, imageCaptureUseCase);
+
+ final Button button = this.findViewById(R.id.Picture);
+ button.setOnClickListener(
+ view -> {
+ startTime = System.currentTimeMillis();
+ imageCaptureUseCase.takePicture(
+ new ImageCaptureUseCase.OnImageCapturedListener() {
+ @Override
+ public void onCaptureSuccess(
+ ImageProxy image, int rotationDegrees) {
+ totalTime = System.currentTimeMillis() - startTime;
+ if (image != null) {
+ imageResolution =
+ image.getWidth() + "x" + image.getHeight();
+ } else {
+ Log.e(TAG, "[onCaptureSuccess] image is null");
+ }
+ }
+ });
+ });
+ }
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_main);
+
+ final Bundle bundle = getIntent().getExtras();
+ if (bundle != null) {
+ final String captureModeString = bundle.getString(EXTRA_CAPTURE_MODE);
+ if (captureModeString != null) {
+ captureMode = CaptureMode.valueOf(Ascii.toUpperCase(captureModeString));
+ }
+ final String cameraLensFacing = bundle.getString(EXTRA_CAMERA_FACING);
+ if (cameraLensFacing != null) {
+ setupCamera(cameraLensFacing);
+ } else {
+ setupCamera(defaultCameraFacing);
+ }
+ }
+ customLifecycle = new CustomLifecycle();
+ prepareUseCase();
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ customLifecycle.doOnResume();
+ }
+
+ void setupCamera(String cameraFacing) {
+ Log.d(TAG, "Camera Facing: " + cameraFacing);
+ if (Ascii.equalsIgnoreCase(cameraFacing, CAMERA_FACING_BACK)) {
+ currentCameraLensFacing = LensFacing.BACK;
+ } else if (Ascii.equalsIgnoreCase(cameraFacing, CAMERA_FACING_FRONT)) {
+ currentCameraLensFacing = LensFacing.FRONT;
+ } else {
+ throw new RuntimeException("Invalid lens facing: " + cameraFacing);
+ }
+ }
+}
diff --git a/camera/integration-tests/timingapp/src/main/res/layout/activity_main.xml b/camera/integration-tests/timingapp/src/main/res/layout/activity_main.xml
new file mode 100644
index 0000000..76ee350
--- /dev/null
+++ b/camera/integration-tests/timingapp/src/main/res/layout/activity_main.xml
@@ -0,0 +1,52 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2019 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.
+-->
+<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ tools:context="androidx.camera.app.timingapp">
+
+ <TextureView
+ android:id="@+id/textureView"
+ android:layout_width="0dp"
+ android:layout_height="0dp"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent" />
+
+ <android.support.constraint.Guideline
+ android:id="@+id/takepicture"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ app:layout_constraintGuide_begin="0dp"
+ app:layout_constraintGuide_percent="0.1" />
+
+ <Button
+ android:id="@+id/Picture"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:scaleType="fitXY"
+ android:text="Picture"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintHorizontal_bias="0.0"
+ app:layout_constraintStart_toStartOf="@+id/takepicture"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintVertical_bias="1.0" />
+</android.support.constraint.ConstraintLayout>
diff --git a/camera/integration-tests/timingapp/src/main/res/values/strings.xml b/camera/integration-tests/timingapp/src/main/res/values/strings.xml
new file mode 100644
index 0000000..2df0565
--- /dev/null
+++ b/camera/integration-tests/timingapp/src/main/res/values/strings.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2019 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.
+-->
+<resources>
+</resources>
diff --git a/camera/integration-tests/timingapp/src/main/res/values/style.xml b/camera/integration-tests/timingapp/src/main/res/values/style.xml
new file mode 100644
index 0000000..7503cc0
--- /dev/null
+++ b/camera/integration-tests/timingapp/src/main/res/values/style.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2019 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.
+-->
+<resources>
+ <!-- Base application theme. -->
+ <style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
+ <!-- Customize your theme here. -->
+ </style>
+</resources>
diff --git a/camera/testing/src/main/AndroidManifest.xml b/camera/testing/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..abd2784
--- /dev/null
+++ b/camera/testing/src/main/AndroidManifest.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright 2019 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.
+ -->
+<manifest package="androidx.camera.testing" />
diff --git a/camera/testing/src/main/java/androidx/camera/testing/fakes/FakeAppConfiguration.java b/camera/testing/src/main/java/androidx/camera/testing/fakes/FakeAppConfiguration.java
new file mode 100644
index 0000000..afd2f8b
--- /dev/null
+++ b/camera/testing/src/main/java/androidx/camera/testing/fakes/FakeAppConfiguration.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2019 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.testing.fakes;
+
+import androidx.camera.core.AppConfiguration;
+import androidx.camera.core.CameraDeviceSurfaceManager;
+import androidx.camera.core.CameraFactory;
+import androidx.camera.core.ExtendableUseCaseConfigFactory;
+import androidx.camera.core.UseCaseConfigurationFactory;
+
+/**
+ * Convenience class for generating a fake {@link androidx.camera.core.AppConfiguration}.
+ *
+ * <p>This {@link AppConfiguration} contains all fake CameraX implementation components.
+ */
+public final class FakeAppConfiguration {
+
+ /** Generates a fake {@link androidx.camera.core.AppConfiguration}. */
+ public static final AppConfiguration create() {
+ CameraFactory cameraFactory = new FakeCameraFactory();
+ CameraDeviceSurfaceManager surfaceManager = new FakeCameraDeviceSurfaceManager();
+ UseCaseConfigurationFactory defaultConfigFactory = new ExtendableUseCaseConfigFactory();
+
+ AppConfiguration.Builder appConfigBuilder =
+ new AppConfiguration.Builder()
+ .setCameraFactory(cameraFactory)
+ .setDeviceSurfaceManager(surfaceManager)
+ .setUseCaseConfigFactory(defaultConfigFactory);
+
+ return appConfigBuilder.build();
+ }
+}
diff --git a/camera/testing/src/main/java/androidx/camera/testing/fakes/FakeCamera.java b/camera/testing/src/main/java/androidx/camera/testing/fakes/FakeCamera.java
new file mode 100644
index 0000000..cc45b5d
--- /dev/null
+++ b/camera/testing/src/main/java/androidx/camera/testing/fakes/FakeCamera.java
@@ -0,0 +1,92 @@
+/*
+ * Copyright (C) 2019 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.testing.fakes;
+
+import androidx.camera.core.BaseCamera;
+import androidx.camera.core.BaseUseCase;
+import androidx.camera.core.CameraControl;
+import androidx.camera.core.CameraInfo;
+import androidx.camera.core.CaptureRequestConfiguration;
+
+import java.util.Collection;
+
+/** A fake camera which will not produce any data. */
+public class FakeCamera implements BaseCamera {
+ private final CameraControl cameraControl = CameraControl.defaultEmptyInstance();
+
+ private final CameraInfo cameraInfo;
+
+ FakeCamera() {
+ this(new FakeCameraInfo());
+ }
+
+ FakeCamera(FakeCameraInfo cameraInfo) {
+ this.cameraInfo = cameraInfo;
+ }
+
+ @Override
+ public void open() {
+ }
+
+ @Override
+ public void close() {
+ }
+
+ @Override
+ public void release() {
+ }
+
+ @Override
+ public void addOnlineUseCase(Collection<BaseUseCase> baseUseCases) {
+ }
+
+ @Override
+ public void removeOnlineUseCase(Collection<BaseUseCase> baseUseCases) {
+ }
+
+ @Override
+ public void onUseCaseActive(BaseUseCase useCase) {
+ }
+
+ @Override
+ public void onUseCaseInactive(BaseUseCase useCase) {
+ }
+
+ @Override
+ public void onUseCaseUpdated(BaseUseCase useCase) {
+ }
+
+ @Override
+ public void onUseCaseReset(BaseUseCase useCase) {
+ }
+
+ @Override
+ public void onUseCaseSingleRequest(
+ BaseUseCase useCase, CaptureRequestConfiguration captureRequestConfiguration) {
+ }
+
+ // Returns fixed CameraControl instance in order to verify the instance is correctly attached.
+ @Override
+ public CameraControl getCameraControl() {
+ return cameraControl;
+ }
+
+ @Override
+ public CameraInfo getCameraInfo() {
+ return cameraInfo;
+ }
+}
diff --git a/camera/testing/src/main/java/androidx/camera/testing/fakes/FakeCameraDeviceSurfaceManager.java b/camera/testing/src/main/java/androidx/camera/testing/fakes/FakeCameraDeviceSurfaceManager.java
new file mode 100644
index 0000000..c3f7473
--- /dev/null
+++ b/camera/testing/src/main/java/androidx/camera/testing/fakes/FakeCameraDeviceSurfaceManager.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright (C) 2019 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.testing.fakes;
+
+import android.util.Size;
+
+import androidx.annotation.Nullable;
+import androidx.camera.core.BaseUseCase;
+import androidx.camera.core.CameraDeviceSurfaceManager;
+import androidx.camera.core.SurfaceConfiguration;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/** A CameraDeviceSurfaceManager which has no supported SurfaceConfigurations. */
+public class FakeCameraDeviceSurfaceManager implements CameraDeviceSurfaceManager {
+
+ private static final Size MAX_OUTPUT_SIZE = new Size(0, 0);
+ private static final Size PREVIEW_SIZE = new Size(1920, 1080);
+
+ @Override
+ public boolean checkSupported(
+ String cameraId, List<SurfaceConfiguration> surfaceConfigurationList) {
+ return false;
+ }
+
+ @Override
+ public SurfaceConfiguration transformSurfaceConfiguration(
+ String cameraId, int imageFormat, Size size) {
+ return null;
+ }
+
+ @Nullable
+ @Override
+ public Size getMaxOutputSize(String cameraId, int imageFormat) {
+ return MAX_OUTPUT_SIZE;
+ }
+
+ @Override
+ public Map<BaseUseCase, Size> getSuggestedResolutions(
+ String cameraId, List<BaseUseCase> originalUseCases, List<BaseUseCase> newUseCases) {
+ Map<BaseUseCase, Size> suggestedSizes = new HashMap<>();
+ for (BaseUseCase useCase : newUseCases) {
+ suggestedSizes.put(useCase, MAX_OUTPUT_SIZE);
+ }
+
+ return suggestedSizes;
+ }
+
+ @Override
+ public Size getPreviewSize() {
+ return PREVIEW_SIZE;
+ }
+}
diff --git a/camera/testing/src/main/java/androidx/camera/testing/fakes/FakeCameraFactory.java b/camera/testing/src/main/java/androidx/camera/testing/fakes/FakeCameraFactory.java
new file mode 100644
index 0000000..2618741
--- /dev/null
+++ b/camera/testing/src/main/java/androidx/camera/testing/fakes/FakeCameraFactory.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright (C) 2019 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.testing.fakes;
+
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.annotation.RestrictTo.Scope;
+import androidx.camera.core.BaseCamera;
+import androidx.camera.core.CameraFactory;
+import androidx.camera.core.CameraX.LensFacing;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * A {@link CameraFactory} implementation that contains and produces fake cameras.
+ *
+ * @hide
+ */
+@RestrictTo(Scope.LIBRARY_GROUP)
+public final class FakeCameraFactory implements CameraFactory {
+
+ private static final String BACK_ID = "0";
+ private static final String FRONT_ID = "1";
+
+ private final Set<String> cameraIds;
+
+ private final Map<String, BaseCamera> cameraMap = new HashMap<>();
+
+ public FakeCameraFactory() {
+ HashSet<String> camIds = new HashSet<>();
+ camIds.add(BACK_ID);
+ camIds.add(FRONT_ID);
+
+ cameraIds = Collections.unmodifiableSet(camIds);
+ }
+
+ @Override
+ public BaseCamera getCamera(String cameraId) {
+ if (cameraIds.contains(cameraId)) {
+ BaseCamera camera = cameraMap.get(cameraId);
+ if (camera == null) {
+ camera = new FakeCamera();
+ cameraMap.put(cameraId, camera);
+ }
+ return camera;
+ }
+ throw new IllegalArgumentException("Unknown camera: " + cameraId);
+ }
+
+ @Override
+ public Set<String> getAvailableCameraIds() {
+ return cameraIds;
+ }
+
+ @Nullable
+ @Override
+ public String cameraIdForLensFacing(LensFacing lensFacing) {
+ switch (lensFacing) {
+ case FRONT:
+ return FRONT_ID;
+ case BACK:
+ return BACK_ID;
+ }
+
+ throw new IllegalArgumentException("Unknown lensFacing: " + lensFacing);
+ }
+}
diff --git a/camera/testing/src/main/java/androidx/camera/testing/fakes/FakeCameraInfo.java b/camera/testing/src/main/java/androidx/camera/testing/fakes/FakeCameraInfo.java
new file mode 100644
index 0000000..8d201db
--- /dev/null
+++ b/camera/testing/src/main/java/androidx/camera/testing/fakes/FakeCameraInfo.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2019 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.testing.fakes;
+
+import androidx.annotation.Nullable;
+import androidx.camera.core.CameraInfo;
+import androidx.camera.core.CameraX.LensFacing;
+import androidx.camera.core.ImageOutputConfiguration.RotationValue;
+
+/**
+ * Information for a fake camera.
+ *
+ * <p>This camera info can be constructed with fake values.
+ */
+class FakeCameraInfo implements CameraInfo {
+
+ private final int sensorRotation;
+ private final LensFacing lensFacing;
+
+ FakeCameraInfo() {
+ this(/*sensorRotation=*/ 0, /*lensFacing=*/ LensFacing.BACK);
+ }
+
+ FakeCameraInfo(int sensorRotation, LensFacing lensFacing) {
+ this.sensorRotation = sensorRotation;
+ this.lensFacing = lensFacing;
+ }
+
+ @Nullable
+ @Override
+ public LensFacing getLensFacing() {
+ return lensFacing;
+ }
+
+ @Override
+ public int getSensorRotationDegrees(@RotationValue int relativeRotation) {
+ return sensorRotation;
+ }
+}
diff --git a/camera/testing/src/main/java/androidx/camera/testing/fakes/FakeConfiguration.java b/camera/testing/src/main/java/androidx/camera/testing/fakes/FakeConfiguration.java
new file mode 100644
index 0000000..d7e7791
--- /dev/null
+++ b/camera/testing/src/main/java/androidx/camera/testing/fakes/FakeConfiguration.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2019 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.testing.fakes;
+
+import androidx.annotation.RestrictTo;
+import androidx.annotation.RestrictTo.Scope;
+import androidx.camera.core.Configuration;
+import androidx.camera.core.MutableConfiguration;
+import androidx.camera.core.MutableOptionsBundle;
+import androidx.camera.core.OptionsBundle;
+
+/** Wrapper for an empty Configuration */
+@RestrictTo(Scope.LIBRARY_GROUP)
+public final class FakeConfiguration implements Configuration.Reader {
+
+ private final Configuration config;
+
+ FakeConfiguration(Configuration config) {
+ this.config = config;
+ }
+
+ @Override
+ public Configuration getConfiguration() {
+ return config;
+ }
+
+ /** Builder for an empty Configuration */
+ public static final class Builder implements Configuration.Builder<FakeConfiguration, Builder> {
+
+ private final MutableOptionsBundle optionsBundle;
+
+ public Builder() {
+ optionsBundle = MutableOptionsBundle.create();
+ }
+
+ @Override
+ public MutableConfiguration getMutableConfiguration() {
+ return optionsBundle;
+ }
+
+ @Override
+ public Builder builder() {
+ return this;
+ }
+
+ @Override
+ public FakeConfiguration build() {
+ return new FakeConfiguration(OptionsBundle.from(optionsBundle));
+ }
+ }
+}
diff --git a/camera/testing/src/main/java/androidx/camera/testing/fakes/FakeLifecycleOwner.java b/camera/testing/src/main/java/androidx/camera/testing/fakes/FakeLifecycleOwner.java
new file mode 100644
index 0000000..a255142
--- /dev/null
+++ b/camera/testing/src/main/java/androidx/camera/testing/fakes/FakeLifecycleOwner.java
@@ -0,0 +1,122 @@
+/*
+ * Copyright (C) 2019 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.testing.fakes;
+
+import androidx.annotation.RestrictTo;
+import androidx.annotation.RestrictTo.Scope;
+import androidx.lifecycle.Lifecycle;
+import androidx.lifecycle.LifecycleOwner;
+import androidx.lifecycle.LifecycleRegistry;
+
+/**
+ * A fake lifecycle owner which obeys the lifecycle transition rules.
+ *
+ * @hide
+ * @see <a href="https://developer.android.com/topic/libraries/architecture/lifecycle">lifecycle</a>
+ */
+@RestrictTo(Scope.LIBRARY_GROUP)
+public final class FakeLifecycleOwner implements LifecycleOwner {
+ private final LifecycleRegistry lifecycleRegistry;
+
+ /**
+ * Creates a new lifecycle owner.
+ *
+ * <p>The lifecycle is initial put into the INITIALIZED and CREATED states.
+ */
+ public FakeLifecycleOwner() {
+ lifecycleRegistry = new LifecycleRegistry(this);
+ lifecycleRegistry.markState(Lifecycle.State.INITIALIZED);
+ lifecycleRegistry.markState(Lifecycle.State.CREATED);
+ }
+
+ /**
+ * Starts and resumes the lifecycle.
+ *
+ * <p>The lifecycle is put into the STARTED and RESUMED states. The lifecycle must already be in
+ * the CREATED state or an exception is thrown.
+ */
+ public void startAndResume() {
+ if (lifecycleRegistry.getCurrentState() != Lifecycle.State.CREATED) {
+ throw new IllegalStateException("Invalid state transition.");
+ }
+ lifecycleRegistry.markState(Lifecycle.State.STARTED);
+ lifecycleRegistry.markState(Lifecycle.State.RESUMED);
+ }
+
+ /**
+ * Starts the lifecycle.
+ *
+ * <p>The lifecycle is put into the START state. The lifecycle must already be in the CREATED
+ * state or an exception is thrown.
+ */
+ public void start() {
+ if (lifecycleRegistry.getCurrentState() != Lifecycle.State.CREATED) {
+ throw new IllegalStateException("Invalid state transition.");
+ }
+ lifecycleRegistry.markState(Lifecycle.State.STARTED);
+ }
+
+ /**
+ * Pauses and stops the lifecycle.
+ *
+ * <p>The lifecycle is put into the STARTED and CREATED states. The lifecycle must already be in
+ * the RESUMED state or an exception is thrown.
+ */
+ public void pauseAndStop() {
+ if (lifecycleRegistry.getCurrentState() != Lifecycle.State.RESUMED) {
+ throw new IllegalStateException("Invalid state transition.");
+ }
+ lifecycleRegistry.markState(Lifecycle.State.STARTED);
+ lifecycleRegistry.markState(Lifecycle.State.CREATED);
+ }
+
+ /**
+ * Stops the lifecycle.
+ *
+ * <p>The lifecycle is put into the CREATED state. The lifecycle must already be in the STARTED
+ * state or an exception is thrown.
+ */
+ public void stop() {
+ if (lifecycleRegistry.getCurrentState() != Lifecycle.State.STARTED) {
+ throw new IllegalStateException("Invalid state transition.");
+ }
+ lifecycleRegistry.markState(Lifecycle.State.CREATED);
+ }
+
+ /**
+ * Destroys the lifecycle.
+ *
+ * <p>The lifecycle is put into the DESTROYED state. The lifecycle must already be in the
+ * CREATED state or an exception is thrown.
+ */
+ public void destroy() {
+ if (lifecycleRegistry.getCurrentState() != Lifecycle.State.CREATED) {
+ throw new IllegalStateException("Invalid state transition.");
+ }
+ lifecycleRegistry.markState(Lifecycle.State.DESTROYED);
+ }
+
+ /** Returns the number of observers of this lifecycle. */
+ public int getObserverCount() {
+ return lifecycleRegistry.getObserverCount();
+ }
+
+ @Override
+ public Lifecycle getLifecycle() {
+ return lifecycleRegistry;
+ }
+}
diff --git a/camera/view/proguard.flags b/camera/view/proguard.flags
new file mode 100644
index 0000000..d8edd2e
--- /dev/null
+++ b/camera/view/proguard.flags
@@ -0,0 +1,66 @@
+# Copyright (C) 2019 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.
+
+# You can comment this out if you're not interested in stack traces.
+
+-keepparameternames
+-keepattributes Exceptions,InnerClasses,Signature,Deprecated,
+ SourceFile,LineNumberTable,EnclosingMethod
+
+# Preserve all annotations.
+
+-keepattributes *Annotation*
+
+# Preserve all public classes, and their public and protected fields and
+# methods.
+
+-keep public class * {
+ public protected *;
+}
+
+# Preserve all .class method names.
+
+-keepclassmembernames class * {
+ java.lang.Class class$(java.lang.String);
+ java.lang.Class class$(java.lang.String, boolean);
+}
+
+# Preserve all native method names and the names of their classes.
+
+-keepclasseswithmembernames class * {
+ native <methods>;
+}
+
+# Preserve the special static methods that are required in all enumeration
+# classes.
+
+-keepclassmembers class * extends java.lang.Enum {
+ public static **[] values();
+ public static ** valueOf(java.lang.String);
+}
+
+# Explicitly preserve all serialization members. The Serializable interface
+# is only a marker interface, so it wouldn't save them.
+# You can comment this out if your library doesn't use serialization.
+# If your code contains serializable classes that have to be backward
+# compatible, please refer to the manual.
+
+-keepclassmembers class * implements java.io.Serializable {
+ static final long serialVersionUID;
+ static final java.io.ObjectStreamField[] serialPersistentFields;
+ private void writeObject(java.io.ObjectOutputStream);
+ private void readObject(java.io.ObjectInputStream);
+ java.lang.Object writeReplace();
+ java.lang.Object readResolve();
+}
diff --git a/camera/view/src/main/AndroidManifest.xml b/camera/view/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..ae10d71
--- /dev/null
+++ b/camera/view/src/main/AndroidManifest.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2019 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.
+-->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="androidx.camera.view">
+
+ <uses-sdk
+ android:minSdkVersion="21"
+ android:targetSdkVersion="27" />
+
+</manifest>
diff --git a/camera/view/src/main/java/androidx/camera/view/CameraView.java b/camera/view/src/main/java/androidx/camera/view/CameraView.java
new file mode 100644
index 0000000..7f434e1
--- /dev/null
+++ b/camera/view/src/main/java/androidx/camera/view/CameraView.java
@@ -0,0 +1,1160 @@
+/*
+ * Copyright (C) 2019 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.view;
+
+import android.Manifest.permission;
+import android.annotation.SuppressLint;
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.Matrix;
+import android.graphics.Paint;
+import android.graphics.Rect;
+import android.graphics.SurfaceTexture;
+import android.hardware.display.DisplayManager;
+import android.hardware.display.DisplayManager.DisplayListener;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Parcelable;
+import android.text.TextUtils;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.util.Size;
+import android.view.Display;
+import android.view.MotionEvent;
+import android.view.ScaleGestureDetector;
+import android.view.Surface;
+import android.view.TextureView;
+import android.view.View;
+import android.view.ViewConfiguration;
+import android.view.ViewGroup;
+import android.view.animation.BaseInterpolator;
+import android.view.animation.DecelerateInterpolator;
+
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresPermission;
+import androidx.annotation.RestrictTo;
+import androidx.annotation.RestrictTo.Scope;
+import androidx.annotation.UiThread;
+import androidx.camera.core.CameraX.LensFacing;
+import androidx.camera.core.FlashMode;
+import androidx.camera.core.ImageCaptureUseCase.OnImageCapturedListener;
+import androidx.camera.core.ImageCaptureUseCase.OnImageSavedListener;
+import androidx.camera.core.ImageProxy;
+import androidx.camera.core.VideoCaptureUseCase.OnVideoSavedListener;
+import androidx.lifecycle.LifecycleOwner;
+
+import java.io.File;
+
+/**
+ * A {@link View} that displays a preview of the camera with methods {@link
+ * #takePicture(OnImageCapturedListener)}, {@link #takePicture(File, OnImageSavedListener)}, {@link
+ * #startRecording(File, OnVideoSavedListener)} and {@link #stopRecording()}.
+ *
+ * <p>Because the Camera is a limited resource and consumes a high amount of power, CameraView must
+ * be opened/closed. CameraView will handle opening/closing automatically through use of a {@link
+ * LifecycleOwner}. Use {@link #bindToLifecycle(LifecycleOwner)} to start the camera.
+ */
+public final class CameraView extends ViewGroup {
+ static final String TAG = androidx.camera.view.CameraView.class.getSimpleName();
+ static final boolean DEBUG = false;
+
+ static final int INDEFINITE_VIDEO_DURATION = -1;
+ static final int INDEFINITE_VIDEO_SIZE = -1;
+
+ private static final String EXTRA_SUPER = "super";
+ private static final String EXTRA_QUALITY = "quality";
+ private static final String EXTRA_ZOOM_LEVEL = "zoom_level";
+ private static final String EXTRA_PINCH_TO_ZOOM_ENABLED = "pinch_to_zoom_enabled";
+ private static final String EXTRA_FLASH = "flash";
+ private static final String EXTRA_MAX_VIDEO_DURATION = "max_video_duration";
+ private static final String EXTRA_MAX_VIDEO_SIZE = "max_video_size";
+ private static final String EXTRA_SCALE_TYPE = "scale_type";
+ private static final String EXTRA_CAMERA_DIRECTION = "camera_direction";
+ private static final String EXTRA_CAPTURE_MODE = "captureMode";
+
+ private static final int LENS_FACING_NONE = 0;
+ private static final int LENS_FACING_FRONT = 1;
+ private static final int LENS_FACING_BACK = 2;
+ private static final int FLASH_MODE_AUTO = 1;
+ private static final int FLASH_MODE_ON = 2;
+ private static final int FLASH_MODE_OFF = 4;
+ private final Rect focusingRect = new Rect();
+ private final Rect meteringRect = new Rect();
+ // For tap-to-focus
+ private long downEventTimestamp;
+ // For pinch-to-zoom
+ private PinchToZoomGestureDetector pinchToZoomGestureDetector;
+ private boolean isPinchToZoomEnabled = true;
+ CameraXModule cameraModule;
+ private final DisplayManager.DisplayListener displayListener =
+ new DisplayListener() {
+ @Override
+ public void onDisplayAdded(int displayId) {
+ }
+
+ @Override
+ public void onDisplayRemoved(int displayId) {
+ }
+
+ @Override
+ public void onDisplayChanged(int displayId) {
+ cameraModule.invalidateView();
+ }
+ };
+ private TextureView cameraTextureView;
+ private Size viewFinderSrcSize = new Size(0, 0);
+ private ScaleType scaleType = ScaleType.CENTER_CROP;
+ // For accessibility event
+ private MotionEvent upEvent;
+ private @Nullable
+ Paint layerPaint;
+
+ public CameraView(Context context) {
+ this(context, null);
+ }
+
+ public CameraView(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public CameraView(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ init(context, attrs);
+ }
+
+ @TargetApi(21)
+ public CameraView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+ init(context, attrs);
+ }
+
+ /** Debug logging that can be enabled. */
+ private static void log(String msg) {
+ if (DEBUG) {
+ Log.i(TAG, msg);
+ }
+ }
+
+ /** Utility method for converting an displayRotation int into a human readable string. */
+ private static String displayRotationToString(int displayRotation) {
+ if (displayRotation == Surface.ROTATION_0 || displayRotation == Surface.ROTATION_180) {
+ return "Portrait-" + (displayRotation * 90);
+ } else if (displayRotation == Surface.ROTATION_90
+ || displayRotation == Surface.ROTATION_270) {
+ return "Landscape-" + (displayRotation * 90);
+ } else {
+ return "Unknown";
+ }
+ }
+
+ /**
+ * Binds control of the camera used by this view to the given lifecycle.
+ *
+ * <p>This links opening/closing the camera to the given lifecycle. The camera will not operate
+ * unless this method is called with a valid {@link LifecycleOwner} that is not in the {@link
+ * android.arch.lifecycle.Lifecycle.State#DESTROYED} state. Call this method only once camera
+ * permissions have been obtained.
+ *
+ * <p>Once the provided lifecycle has transitioned to a {@link
+ * android.arch.lifecycle.Lifecycle.State#DESTROYED} state, CameraView must be bound to a new
+ * lifecycle through this method in order to operate the camera.
+ *
+ * @param lifecycleOwner The lifecycle that will control this view's camera
+ * @throws IllegalArgumentException if provided lifecycle is in a {@link
+ * android.arch.lifecycle.Lifecycle.State#DESTROYED} state.
+ * @throws IllegalStateException if camera permissions are not granted.
+ */
+ @RequiresPermission(permission.CAMERA)
+ public void bindToLifecycle(LifecycleOwner lifecycleOwner) {
+ cameraModule.bindToLifecycle(lifecycleOwner);
+ }
+
+ private void init(Context context, @Nullable AttributeSet attrs) {
+ addView(cameraTextureView = new TextureView(getContext()), 0 /* view position */);
+ cameraTextureView.setLayerPaint(layerPaint);
+ cameraModule = new CameraXModule(this);
+
+ if (isInEditMode()) {
+ onViewfinderSourceDimensUpdated(640, 480);
+ }
+
+ if (attrs != null) {
+ TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CameraView);
+ setScaleType(
+ ScaleType.fromId(
+ a.getInteger(R.styleable.CameraView_scaleType, getScaleType().id)));
+ setQuality(
+ Quality.fromId(a.getInteger(R.styleable.CameraView_quality, getQuality().id)));
+ setPinchToZoomEnabled(
+ a.getBoolean(
+ R.styleable.CameraView_pinchToZoomEnabled, isPinchToZoomEnabled()));
+ setCaptureMode(
+ CaptureMode.fromId(
+ a.getInteger(R.styleable.CameraView_captureMode, getCaptureMode().id)));
+
+ int lensFacing = a.getInt(R.styleable.CameraView_lensFacing, LENS_FACING_BACK);
+ switch (lensFacing) {
+ case LENS_FACING_NONE:
+ setCameraByLensFacing(null);
+ break;
+ case LENS_FACING_FRONT:
+ setCameraByLensFacing(LensFacing.FRONT);
+ break;
+ case LENS_FACING_BACK:
+ setCameraByLensFacing(LensFacing.BACK);
+ break;
+ default:
+ // Unhandled event.
+ }
+
+ int flashMode = a.getInt(R.styleable.CameraView_flash, 0);
+ switch (flashMode) {
+ case FLASH_MODE_AUTO:
+ setFlash(FlashMode.AUTO);
+ break;
+ case FLASH_MODE_ON:
+ setFlash(FlashMode.ON);
+ break;
+ case FLASH_MODE_OFF:
+ setFlash(FlashMode.OFF);
+ break;
+ default:
+ // Unhandled event.
+ }
+
+ a.recycle();
+ }
+
+ if (getBackground() == null) {
+ setBackgroundColor(0xFF111111);
+ }
+
+ pinchToZoomGestureDetector = new PinchToZoomGestureDetector(context);
+ }
+
+ @Override
+ protected LayoutParams generateDefaultLayoutParams() {
+ return new LayoutParams(
+ ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
+ }
+
+ @Override
+ protected Parcelable onSaveInstanceState() {
+ // TODO(b/113884082): Decide what belongs here or what should be invalidated on
+ // configuration
+ // change
+ Bundle state = new Bundle();
+ state.putParcelable(EXTRA_SUPER, super.onSaveInstanceState());
+ state.putInt(EXTRA_SCALE_TYPE, getScaleType().id);
+ state.putInt(EXTRA_QUALITY, getQuality().id);
+ state.putFloat(EXTRA_ZOOM_LEVEL, getZoomLevel());
+ state.putBoolean(EXTRA_PINCH_TO_ZOOM_ENABLED, isPinchToZoomEnabled());
+ state.putString(EXTRA_FLASH, getFlash().name());
+ state.putLong(EXTRA_MAX_VIDEO_DURATION, getMaxVideoDuration());
+ state.putLong(EXTRA_MAX_VIDEO_SIZE, getMaxVideoSize());
+ if (getCameraLensFacing() != null) {
+ state.putString(EXTRA_CAMERA_DIRECTION, getCameraLensFacing().name());
+ }
+ state.putInt(EXTRA_CAPTURE_MODE, getCaptureMode().id);
+ return state;
+ }
+
+ @Override
+ protected void onRestoreInstanceState(Parcelable savedState) {
+ // TODO(b/113884082): Decide what belongs here or what should be invalidated on
+ // configuration
+ // change
+ if (savedState instanceof Bundle) {
+ Bundle state = (Bundle) savedState;
+ super.onRestoreInstanceState(state.getParcelable(EXTRA_SUPER));
+ setScaleType(ScaleType.fromId(state.getInt(EXTRA_SCALE_TYPE)));
+ setQuality(Quality.fromId(state.getInt(EXTRA_QUALITY)));
+ setZoomLevel(state.getFloat(EXTRA_ZOOM_LEVEL));
+ setPinchToZoomEnabled(state.getBoolean(EXTRA_PINCH_TO_ZOOM_ENABLED));
+ setFlash(FlashMode.valueOf(state.getString(EXTRA_FLASH)));
+ setMaxVideoDuration(state.getLong(EXTRA_MAX_VIDEO_DURATION));
+ setMaxVideoSize(state.getLong(EXTRA_MAX_VIDEO_SIZE));
+ String lensFacingString = state.getString(EXTRA_CAMERA_DIRECTION);
+ setCameraByLensFacing(
+ TextUtils.isEmpty(lensFacingString)
+ ? null
+ : LensFacing.valueOf(lensFacingString));
+ setCaptureMode(CaptureMode.fromId(state.getInt(EXTRA_CAPTURE_MODE)));
+ } else {
+ super.onRestoreInstanceState(savedState);
+ }
+ }
+
+ /**
+ * Sets the paint on the viewfinder.
+ *
+ * <p>This only affects the viewfinder, and does not affect captured images/video.
+ *
+ * @param paint The paint object to apply to the viewfinder.
+ * @hide This may not work once {@link android.view.SurfaceView} is supported along with {@link
+ * TextureView}.
+ */
+ @Override
+ public void setLayerPaint(@Nullable Paint paint) {
+ super.setLayerPaint(paint);
+ layerPaint = paint;
+ cameraTextureView.setLayerPaint(paint);
+ }
+
+ @Override
+ protected void onAttachedToWindow() {
+ super.onAttachedToWindow();
+ DisplayManager dpyMgr =
+ (DisplayManager) getContext().getSystemService(Context.DISPLAY_SERVICE);
+ dpyMgr.registerDisplayListener(displayListener, new Handler(Looper.getMainLooper()));
+ }
+
+ @Override
+ protected void onDetachedFromWindow() {
+ super.onDetachedFromWindow();
+ DisplayManager dpyMgr =
+ (DisplayManager) getContext().getSystemService(Context.DISPLAY_SERVICE);
+ dpyMgr.unregisterDisplayListener(displayListener);
+ }
+
+ // TODO(b/124269166): Rethink how we can handle permissions here.
+ @SuppressLint("MissingPermission")
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ int viewWidth = MeasureSpec.getSize(widthMeasureSpec);
+ int viewHeight = MeasureSpec.getSize(heightMeasureSpec);
+
+ int displayRotation = getDisplay().getRotation();
+
+ if (viewFinderSrcSize.getHeight() == 0 || viewFinderSrcSize.getWidth() == 0) {
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+ cameraTextureView.measure(viewWidth, viewHeight);
+ } else {
+ Size scaled =
+ calculateViewfinderViewDimens(
+ viewFinderSrcSize, viewWidth, viewHeight, displayRotation, scaleType);
+ super.setMeasuredDimension(
+ Math.min(scaled.getWidth(), viewWidth),
+ Math.min(scaled.getHeight(), viewHeight));
+ cameraTextureView.measure(scaled.getWidth(), scaled.getHeight());
+ }
+
+ // Since bindToLifecycle will depend on the measured dimension, only call it when measured
+ // dimension is not 0x0
+ if (getMeasuredWidth() > 0 && getMeasuredHeight() > 0) {
+ cameraModule.bindToLifecycleAfterViewMeasured();
+ }
+ }
+
+ // TODO(b/124269166): Rethink how we can handle permissions here.
+ @SuppressLint("MissingPermission")
+ @Override
+ protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+ // In case that the CameraView size is always set as 0x0, we still need to trigger to force
+ // binding to lifecycle
+ cameraModule.bindToLifecycleAfterViewMeasured();
+
+ // If we don't know the src buffer size yet, set the viewfinder to be the parent size
+ if (viewFinderSrcSize.getWidth() == 0 || viewFinderSrcSize.getHeight() == 0) {
+ cameraTextureView.layout(left, top, right, bottom);
+ return;
+ }
+
+ // Compute the viewfinder ui size based on the available width, height, and ui orientation.
+ int viewWidth = (right - left);
+ int viewHeight = (bottom - top);
+ int displayRotation = getDisplay().getRotation();
+ Size scaled =
+ calculateViewfinderViewDimens(
+ viewFinderSrcSize, viewWidth, viewHeight, displayRotation, scaleType);
+
+ // Compute the center of the view.
+ int centerX = viewWidth / 2;
+ int centerY = viewHeight / 2;
+
+ // Compute the left / top / right / bottom values such that viewfinder is centered.
+ int layoutL = centerX - (scaled.getWidth() / 2);
+ int layoutT = centerY - (scaled.getHeight() / 2);
+ int layoutR = layoutL + scaled.getWidth();
+ int layoutB = layoutT + scaled.getHeight();
+
+ // Layout debugging
+ log("layout: viewWidth: " + viewWidth);
+ log("layout: viewHeight: " + viewHeight);
+ log("layout: viewRatio: " + (viewWidth / (float) viewHeight));
+ log("layout: sizeWidth: " + viewFinderSrcSize.getWidth());
+ log("layout: sizeHeight: " + viewFinderSrcSize.getHeight());
+ log(
+ "layout: sizeRatio: "
+ + (viewFinderSrcSize.getWidth() / (float) viewFinderSrcSize.getHeight()));
+ log("layout: scaledWidth: " + scaled.getWidth());
+ log("layout: scaledHeight: " + scaled.getHeight());
+ log("layout: scaledRatio: " + (scaled.getWidth() / (float) scaled.getHeight()));
+ log(
+ "layout: size: "
+ + scaled
+ + " ("
+ + (scaled.getWidth() / (float) scaled.getHeight())
+ + " - "
+ + scaleType
+ + "-"
+ + displayRotationToString(displayRotation)
+ + ")");
+ log("layout: final " + layoutL + ", " + layoutT + ", " + layoutR + ", " + layoutB);
+
+ cameraTextureView.layout(layoutL, layoutT, layoutR, layoutB);
+
+ cameraModule.invalidateView();
+ }
+
+ /** Records the size of the viewfinder's buffers. */
+ @UiThread
+ void onViewfinderSourceDimensUpdated(int srcWidth, int srcHeight) {
+ if (srcWidth != viewFinderSrcSize.getWidth()
+ || srcHeight != viewFinderSrcSize.getHeight()) {
+ viewFinderSrcSize = new Size(srcWidth, srcHeight);
+ requestLayout();
+ }
+ }
+
+ private Size calculateViewfinderViewDimens(
+ Size srcSize,
+ int parentWidth,
+ int parentHeight,
+ int displayRotation,
+ ScaleType scaleType) {
+ int inWidth = srcSize.getWidth();
+ int inHeight = srcSize.getHeight();
+ if (displayRotation == Surface.ROTATION_90 || displayRotation == Surface.ROTATION_270) {
+ // Need to reverse the width and height since we're in landscape orientation.
+ inWidth = srcSize.getHeight();
+ inHeight = srcSize.getWidth();
+ }
+
+ int outWidth = parentWidth;
+ int outHeight = parentHeight;
+ if (inWidth != 0 && inHeight != 0) {
+ float vfRatio = inWidth / (float) inHeight;
+ float parentRatio = parentWidth / (float) parentHeight;
+
+ switch (scaleType) {
+ case CENTER_INSIDE:
+ // Match longest sides together.
+ if (vfRatio > parentRatio) {
+ outWidth = parentWidth;
+ outHeight = Math.round(parentWidth / vfRatio);
+ } else {
+ outWidth = Math.round(parentHeight * vfRatio);
+ outHeight = parentHeight;
+ }
+ break;
+ case CENTER_CROP:
+ // Match shortest sides together.
+ if (vfRatio < parentRatio) {
+ outWidth = parentWidth;
+ outHeight = Math.round(parentWidth / vfRatio);
+ } else {
+ outWidth = Math.round(parentHeight * vfRatio);
+ outHeight = parentHeight;
+ }
+ break;
+ }
+ }
+
+ return new Size(outWidth, outHeight);
+ }
+
+ /**
+ * @return One of {@link Surface#ROTATION_0}, {@link Surface#ROTATION_90}, {@link
+ * Surface#ROTATION_180}, {@link Surface#ROTATION_270}.
+ */
+ int getDisplaySurfaceRotation() {
+ Display display = getDisplay();
+
+ // Null when the View is detached. If we were in the middle of a background operation,
+ // better to not NPE. When the background operation finishes, it'll realize that the camera
+ // was closed.
+ if (display == null) {
+ return 0;
+ }
+
+ return display.getRotation();
+ }
+
+ @UiThread
+ SurfaceTexture getSurfaceTexture() {
+ if (cameraTextureView != null) {
+ return cameraTextureView.getSurfaceTexture();
+ }
+
+ return null;
+ }
+
+ @UiThread
+ void setSurfaceTexture(SurfaceTexture surfaceTexture) {
+ if (cameraTextureView.getSurfaceTexture() != surfaceTexture) {
+ if (cameraTextureView.isAvailable()) {
+ // Remove the old TextureView to properly detach the old SurfaceTexture from the GL
+ // Context.
+ removeView(cameraTextureView);
+ addView(cameraTextureView = new TextureView(getContext()), 0);
+ cameraTextureView.setLayerPaint(layerPaint);
+ requestLayout();
+ }
+
+ cameraTextureView.setSurfaceTexture(surfaceTexture);
+ }
+ }
+
+ @UiThread
+ Matrix getTransform(Matrix matrix) {
+ return cameraTextureView.getTransform(matrix);
+ }
+
+ @UiThread
+ int getViewFinderWidth() {
+ return cameraTextureView.getWidth();
+ }
+
+ @UiThread
+ int getViewFinderHeight() {
+ return cameraTextureView.getHeight();
+ }
+
+ @UiThread
+ void setTransform(final Matrix matrix) {
+ if (cameraTextureView != null) {
+ cameraTextureView.setTransform(matrix);
+ }
+ }
+
+ /**
+ * Returns the scale type used to scale the viewfinder.
+ *
+ * @return The current {@link ScaleType}.
+ */
+ public ScaleType getScaleType() {
+ return scaleType;
+ }
+
+ /**
+ * Sets the view finder scale type.
+ *
+ * <p>This controls how the view finder should be scaled and positioned within the view.
+ *
+ * @param scaleType The desired {@link ScaleType}.
+ */
+ public void setScaleType(ScaleType scaleType) {
+ if (scaleType != this.scaleType) {
+ this.scaleType = scaleType;
+ requestLayout();
+ }
+ }
+
+ /**
+ * Gets the current quality for image and video outputs.
+ *
+ * @return The current {@link Quality}. Currently only {@link Quality#HIGH} is supported.
+ * @hide Not currently connected to use cases.
+ */
+ public Quality getQuality() {
+ return cameraModule.getQuality();
+ }
+
+ /**
+ * Sets the quality for image and video outputs.
+ *
+ * @param quality The {@link Quality} used for image and video. Currently only {@link
+ * Quality#HIGH} is supported.
+ * @throws UnsupportedOperationException if any quality other than HIGH is set.
+ * @hide Not currently connected to use cases.
+ */
+ public void setQuality(Quality quality) {
+ cameraModule.setQuality(quality);
+ }
+
+ /**
+ * Returns the scale type used to scale the viewfinder.
+ *
+ * @return The current {@link CaptureMode}.
+ */
+ public CaptureMode getCaptureMode() {
+ return cameraModule.getCaptureMode();
+ }
+
+ /**
+ * Sets the CameraView capture mode
+ *
+ * <p>This controls only image or video capture function is enabled or both are enabled.
+ *
+ * @param captureMode The desired {@link CaptureMode}.
+ */
+ public void setCaptureMode(CaptureMode captureMode) {
+ cameraModule.setCaptureMode(captureMode);
+ }
+
+ /**
+ * Returns the maximum duration of videos, or {@link #INDEFINITE_VIDEO_DURATION} if there is no
+ * timeout.
+ *
+ * @hide Not currently implemented.
+ */
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ public long getMaxVideoDuration() {
+ return cameraModule.getMaxVideoDuration();
+ }
+
+ /**
+ * Sets the maximum video duration before {@link OnVideoSavedListener#onVideoSaved(File)} is
+ * called automatically. Use {@link #INDEFINITE_VIDEO_DURATION} to disable the timeout.
+ */
+ private void setMaxVideoDuration(long duration) {
+ cameraModule.setMaxVideoDuration(duration);
+ }
+
+ /**
+ * Returns the maximum size of videos in bytes, or {@link #INDEFINITE_VIDEO_SIZE} if there is no
+ * timeout.
+ */
+ private long getMaxVideoSize() {
+ return cameraModule.getMaxVideoSize();
+ }
+
+ /**
+ * Sets the maximum video size in bytes before {@link OnVideoSavedListener#onVideoSaved(File)}
+ * is called automatically. Use {@link #INDEFINITE_VIDEO_SIZE} to disable the size restriction.
+ */
+ private void setMaxVideoSize(long size) {
+ cameraModule.setMaxVideoSize(size);
+ }
+
+ /**
+ * Takes a picture, and calls {@link OnImageCapturedListener#onCaptureSuccess(ImageProxy, int)}
+ * once when done.
+ *
+ * @param listener Listener which will receive success or failure callbacks.
+ */
+ public void takePicture(OnImageCapturedListener listener) {
+ cameraModule.takePicture(listener);
+ }
+
+ /**
+ * Takes a picture and calls {@link OnImageSavedListener#onImageSaved(File)} when done.
+ *
+ * @param file The destination.
+ * @param listener Listener which will receive success or failure callbacks.
+ */
+ public void takePicture(File file, OnImageSavedListener listener) {
+ cameraModule.takePicture(file, listener);
+ }
+
+ /**
+ * Takes a video and calls {@link OnVideoSavedListener#onVideoSaved(File)} when done.
+ *
+ * @param file The destination.
+ */
+ public void startRecording(File file, OnVideoSavedListener listener) {
+ cameraModule.startRecording(file, listener);
+ }
+
+ /** Stops an in progress video. */
+ public void stopRecording() {
+ cameraModule.stopRecording();
+ }
+
+ /** @return True if currently recording. */
+ public boolean isRecording() {
+ return cameraModule.isRecording();
+ }
+
+ /**
+ * Queries whether the current device has a camera with the specified direction.
+ *
+ * @return True if the device supports the direction.
+ * @throws IllegalStateException if the CAMERA permission is not currently granted.
+ */
+ @RequiresPermission(permission.CAMERA)
+ public boolean hasCameraWithLensFacing(LensFacing lensFacing) {
+ return cameraModule.hasCameraWithLensFacing(lensFacing);
+ }
+
+ /**
+ * Toggles between the primary front facing camera and the primary back facing camera.
+ *
+ * <p>This will have no effect if not already bound to a lifecycle via {@link
+ * #bindToLifecycle(LifecycleOwner)}.
+ */
+ public void toggleCamera() {
+ cameraModule.toggleCamera();
+ }
+
+ /**
+ * Sets the desired camera lensFacing.
+ *
+ * <p>This will choose the primary camera with the specified camera lensFacing.
+ *
+ * <p>If called before {@link #bindToLifecycle(LifecycleOwner)}, this will set the camera to be
+ * used when first bound to the lifecycle. If the specified lensFacing is not supported by the
+ * device, as determined by {@link #hasCameraWithLensFacing(LensFacing)}, the first supported
+ * lensFacing will be chosen when {@link #bindToLifecycle(LifecycleOwner)} is called.
+ *
+ * <p>If called with {@code null} AFTER binding to the lifecycle, the behavior would be
+ * equivalent to unbind the use cases without the lifecycle having to be destroyed.
+ *
+ * @param lensFacing The desired camera lensFacing.
+ */
+ public void setCameraByLensFacing(@Nullable LensFacing lensFacing) {
+ cameraModule.setCameraByLensFacing(lensFacing);
+ }
+
+ /** Returns the currently selected {@link LensFacing}. */
+ @Nullable
+ public LensFacing getCameraLensFacing() {
+ return cameraModule.getLensFacing();
+ }
+
+ /**
+ * Focuses the camera on the given area.
+ *
+ * <p>Sets the focus and exposure metering rectangles. Coordinates for both X and Y dimensions
+ * are Limited from -1000 to 1000, where (0, 0) is the center of the image and the width/height
+ * represent the values from -1000 to 1000.
+ *
+ * @param focus Area used to focus the camera.
+ * @param metering Area used for exposure metering.
+ */
+ public void focus(Rect focus, Rect metering) {
+ cameraModule.focus(focus, metering);
+ }
+
+ /** Gets the active flash strategy. */
+ public FlashMode getFlash() {
+ return cameraModule.getFlash();
+ }
+
+ /** Sets the active flash strategy. */
+ public void setFlash(FlashMode flashMode) {
+ cameraModule.setFlash(flashMode);
+ }
+
+ private int getRelativeCameraOrientation(boolean compensateForMirroring) {
+ return cameraModule.getRelativeCameraOrientation(compensateForMirroring);
+ }
+
+ private long delta() {
+ return System.currentTimeMillis() - downEventTimestamp;
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent event) {
+ // Disable pinch-to-zoom and tap-to-focus while the camera module is paused.
+ if (cameraModule.isPaused()) {
+ return false;
+ }
+ // Only forward the event to the pinch-to-zoom gesture detector when pinch-to-zoom is
+ // enabled.
+ if (isPinchToZoomEnabled()) {
+ pinchToZoomGestureDetector.onTouchEvent(event);
+ }
+ if (event.getPointerCount() == 2 && isPinchToZoomEnabled() && isZoomSupported()) {
+ return true;
+ }
+
+ // Camera focus
+ switch (event.getAction()) {
+ case MotionEvent.ACTION_DOWN:
+ downEventTimestamp = System.currentTimeMillis();
+ break;
+ case MotionEvent.ACTION_UP:
+ if (delta() < ViewConfiguration.getLongPressTimeout()) {
+ upEvent = event;
+ performClick();
+ }
+ break;
+ default:
+ // Unhandled event.
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Focus the position of the touch event, or focus the center of the viewfinder for
+ * accessibility events
+ */
+ @Override
+ public boolean performClick() {
+ super.performClick();
+
+ final float x = (upEvent != null) ? upEvent.getX() : getX() + getWidth() / 2f;
+ final float y = (upEvent != null) ? upEvent.getY() : getY() + getHeight() / 2f;
+ upEvent = null;
+ calculateTapArea(focusingRect, x, y, 1f);
+ calculateTapArea(meteringRect, x, y, 1.5f);
+ if (area(focusingRect) > 0 && area(meteringRect) > 0) {
+ focus(focusingRect, meteringRect);
+ }
+
+ return true;
+ }
+
+ /** Returns the width * height of the given rect */
+ private int area(Rect rect) {
+ return rect.width() * rect.height();
+ }
+
+ /** The area must be between -1000,-1000 and 1000,1000 */
+ private void calculateTapArea(Rect rect, float x, float y, float coefficient) {
+ int max = 1000;
+ int min = -1000;
+
+ // Default to 300 (1/6th the total area) and scale by the coefficient
+ int areaSize = (int) (300 * coefficient);
+
+ // Rotate the coordinates if the camera orientation is different
+ int width = getWidth();
+ int height = getHeight();
+
+ // Compensate orientation as it's mirrored on preview for forward facing cameras
+ boolean compensateForMirroring = (getCameraLensFacing() == LensFacing.FRONT);
+ int relativeCameraOrientation = getRelativeCameraOrientation(compensateForMirroring);
+ int temp;
+ float tempf;
+ switch (relativeCameraOrientation) {
+ case 90:
+ // Fall-through
+ case 270:
+ // We're horizontal. Swap width/height. Swap x/y.
+ temp = width;
+ //noinspection SuspiciousNameCombination
+ width = height;
+ height = temp;
+
+ tempf = x;
+ //noinspection SuspiciousNameCombination
+ x = y;
+ y = tempf;
+ break;
+ default:
+ break;
+ }
+
+ switch (relativeCameraOrientation) {
+ // Map to correct coordinates according to relativeCameraOrientation
+ case 90:
+ y = height - y;
+ break;
+ case 180:
+ x = width - x;
+ y = height - y;
+ break;
+ case 270:
+ x = width - x;
+ break;
+ default:
+ break;
+ }
+
+ // Swap x if it's a mirrored preview
+ if (compensateForMirroring) {
+ x = width - x;
+ }
+
+ // Grab the x, y position from within the View and normalize it to -1000 to 1000
+ x = min + distance(max, min) * (x / width);
+ y = min + distance(max, min) * (y / height);
+
+ // Modify the rect to the bounding area
+ rect.top = (int) y - areaSize / 2;
+ rect.left = (int) x - areaSize / 2;
+ rect.bottom = rect.top + areaSize;
+ rect.right = rect.left + areaSize;
+
+ // Cap at -1000 to 1000
+ rect.top = rangeLimit(rect.top, max, min);
+ rect.left = rangeLimit(rect.left, max, min);
+ rect.bottom = rangeLimit(rect.bottom, max, min);
+ rect.right = rangeLimit(rect.right, max, min);
+ }
+
+ private int rangeLimit(int val, int max, int min) {
+ return Math.min(Math.max(val, min), max);
+ }
+
+ float rangeLimit(float val, float max, float min) {
+ return Math.min(Math.max(val, min), max);
+ }
+
+ private int distance(int a, int b) {
+ return Math.abs(a - b);
+ }
+
+ /**
+ * Returns whether the view allows pinch-to-zoom.
+ *
+ * @return True if pinch to zoom is enabled.
+ */
+ public boolean isPinchToZoomEnabled() {
+ return isPinchToZoomEnabled;
+ }
+
+ /**
+ * Sets whether the view should allow pinch-to-zoom.
+ *
+ * <p>When enabled, the user can pinch the camera to zoom in/out. This only has an effect if the
+ * bound camera supports zoom.
+ *
+ * @param enabled True to enable pinch-to-zoom.
+ */
+ public void setPinchToZoomEnabled(boolean enabled) {
+ isPinchToZoomEnabled = enabled;
+ }
+
+ /**
+ * Returns the current zoom level.
+ *
+ * @return The current zoom level.
+ */
+ public float getZoomLevel() {
+ return cameraModule.getZoomLevel();
+ }
+
+ /**
+ * Sets the current zoom level.
+ *
+ * <p>Valid zoom values range from 1 to {@link #getMaxZoomLevel()}.
+ *
+ * @param zoomLevel The requested zoom level.
+ */
+ public void setZoomLevel(float zoomLevel) {
+ cameraModule.setZoomLevel(zoomLevel);
+ }
+
+ /**
+ * Returns the minimum zoom level.
+ *
+ * <p>For most cameras this should return a zoom level of 1. A zoom level of 1 corresponds to a
+ * non-zoomed image.
+ *
+ * @return The minimum zoom level.
+ */
+ public float getMinZoomLevel() {
+ return cameraModule.getMinZoomLevel();
+ }
+
+ /**
+ * Returns the maximum zoom level.
+ *
+ * <p>The zoom level corresponds to the ratio between both the widths and heights of a
+ * non-zoomed image and a maximally zoomed image for the selected camera.
+ *
+ * @return The maximum zoom level.
+ */
+ public float getMaxZoomLevel() {
+ return cameraModule.getMaxZoomLevel();
+ }
+
+ /**
+ * Returns whether the bound camera supports zooming.
+ *
+ * @return True if the camera supports zooming.
+ */
+ public boolean isZoomSupported() {
+ return cameraModule.isZoomSupported();
+ }
+
+ /**
+ * Turns on/off torch.
+ *
+ * @param torch True to turn on torch, false to turn off torch.
+ */
+ public void enableTorch(boolean torch) {
+ cameraModule.enableTorch(torch);
+ }
+
+ /**
+ * Returns current torch status.
+ *
+ * @return true if torch is on , otherwise false
+ */
+ public boolean isTorchOn() {
+ return cameraModule.isTorchOn();
+ }
+
+ /** Options for scaling the bounds of the view finder to the bounds of this view. */
+ public enum ScaleType {
+ /**
+ * Scale the view finder, maintaining the source aspect ratio, so the view finder fills the
+ * entire view. This will cause the view finder to crop the source image if the camera
+ * aspect ratio does not match the view aspect ratio.
+ */
+ CENTER_CROP(0),
+ /**
+ * Scale the view finder, maintaining the source aspect ratio, so the view finder is
+ * entirely contained within the view.
+ */
+ CENTER_INSIDE(1);
+
+ final int id;
+
+ ScaleType(int id) {
+ this.id = id;
+ }
+
+ static ScaleType fromId(int id) {
+ for (ScaleType st : values()) {
+ if (st.id == id) {
+ return st;
+ }
+ }
+ throw new IllegalArgumentException();
+ }
+ }
+
+ /**
+ * Determines the resolution of CameraView's outputs. All resolutions are best attempts, and
+ * will fall to lower qualities if the Android device cannot support them. Resolutions also may
+ * change in the future (if, say, Android adds 8k resolution).
+ *
+ * <p>(*) {@link Quality#MAX} will output at 4k. (*) {@link Quality#HIGH} will output at 1080p.
+ * (*) {@link Quality#MEDIUM} will output at 720. (*) {@link Quality#LOW} will output at 480.
+ *
+ * @hide Not currently connected to use cases.
+ */
+ public enum Quality {
+ MAX(0),
+ HIGH(1),
+ MEDIUM(2),
+ LOW(3);
+
+ final int id;
+
+ Quality(int id) {
+ this.id = id;
+ }
+
+ static Quality fromId(int id) {
+ for (Quality f : values()) {
+ if (f.id == id) {
+ return f;
+ }
+ }
+ throw new IllegalArgumentException();
+ }
+ }
+
+ /**
+ * The capture mode used by CameraView.
+ *
+ * <p>This enum can be used to determine which capture mode will be enabled for {@link
+ * CameraView}.
+ */
+ public enum CaptureMode {
+ /** A mode where image capture is enabled. */
+ IMAGE(0),
+ /** A mode where video capture is enabled. */
+ VIDEO(1),
+ /**
+ * A mode where both image capture and video capture are simultaneously enabled. Note that
+ * this mode may not be available on every device.
+ */
+ MIXED(2);
+
+ final int id;
+
+ CaptureMode(int id) {
+ this.id = id;
+ }
+
+ static CaptureMode fromId(int id) {
+ for (CaptureMode f : values()) {
+ if (f.id == id) {
+ return f;
+ }
+ }
+ throw new IllegalArgumentException();
+ }
+ }
+
+ static class S extends ScaleGestureDetector.SimpleOnScaleGestureListener {
+ private ScaleGestureDetector.OnScaleGestureListener listener;
+
+ void setRealGestureDetector(ScaleGestureDetector.OnScaleGestureListener l) {
+ listener = l;
+ }
+
+ @Override
+ public boolean onScale(ScaleGestureDetector detector) {
+ return listener.onScale(detector);
+ }
+ }
+
+ private class PinchToZoomGestureDetector extends ScaleGestureDetector
+ implements ScaleGestureDetector.OnScaleGestureListener {
+ private static final float SCALE_MULTIPIER = 0.75f;
+ private final BaseInterpolator interpolator = new DecelerateInterpolator(2f);
+ private float normalizedScaleFactor = 0;
+
+ PinchToZoomGestureDetector(Context context) {
+ this(context, new S());
+ }
+
+ PinchToZoomGestureDetector(Context context, S s) {
+ super(context, s);
+ s.setRealGestureDetector(this);
+ }
+
+ @Override
+ public boolean onScale(ScaleGestureDetector detector) {
+ normalizedScaleFactor += (detector.getScaleFactor() - 1f) * SCALE_MULTIPIER;
+ // Since the scale factor is normalized, it should always be in the range [0, 1]
+ normalizedScaleFactor = rangeLimit(normalizedScaleFactor, 1f, 0);
+
+ // Apply decelerate interpolation. This will cause the differences to seem less
+ // pronounced
+ // at higher zoom levels.
+ float transformedScale = interpolator.getInterpolation(normalizedScaleFactor);
+
+ // Transform back from normalized coordinates to the zoom scale
+ float zoomLevel =
+ (getMaxZoomLevel() == getMinZoomLevel())
+ ? getMinZoomLevel()
+ : getMinZoomLevel()
+ + transformedScale * (getMaxZoomLevel() - getMinZoomLevel());
+
+ setZoomLevel(rangeLimit(zoomLevel, getMaxZoomLevel(), getMinZoomLevel()));
+ return true;
+ }
+
+ @Override
+ public boolean onScaleBegin(ScaleGestureDetector detector) {
+ float initialZoomLevel = getZoomLevel();
+ normalizedScaleFactor =
+ (getMaxZoomLevel() == getMinZoomLevel())
+ ? 0
+ : (initialZoomLevel - getMinZoomLevel())
+ / (getMaxZoomLevel() - getMinZoomLevel());
+ return true;
+ }
+
+ @Override
+ public void onScaleEnd(ScaleGestureDetector detector) {
+ }
+ }
+}
diff --git a/camera/view/src/main/java/androidx/camera/view/CameraXModule.java b/camera/view/src/main/java/androidx/camera/view/CameraXModule.java
new file mode 100644
index 0000000..9871825
--- /dev/null
+++ b/camera/view/src/main/java/androidx/camera/view/CameraXModule.java
@@ -0,0 +1,786 @@
+/*
+ * Copyright (C) 2019 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.view;
+
+import android.Manifest.permission;
+import android.annotation.SuppressLint;
+import android.content.Context;
+import android.graphics.Matrix;
+import android.graphics.Rect;
+import android.graphics.SurfaceTexture;
+import android.hardware.camera2.CameraAccessException;
+import android.hardware.camera2.CameraCharacteristics;
+import android.hardware.camera2.CameraManager;
+import android.os.Looper;
+import android.util.Log;
+import android.util.Rational;
+import android.util.Size;
+
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresPermission;
+import androidx.annotation.UiThread;
+import androidx.camera.core.CameraInfo;
+import androidx.camera.core.CameraInfoUnavailableException;
+import androidx.camera.core.CameraOrientationUtil;
+import androidx.camera.core.CameraX;
+import androidx.camera.core.CameraX.LensFacing;
+import androidx.camera.core.FlashMode;
+import androidx.camera.core.ImageCaptureUseCase;
+import androidx.camera.core.ImageCaptureUseCase.OnImageCapturedListener;
+import androidx.camera.core.ImageCaptureUseCase.OnImageSavedListener;
+import androidx.camera.core.ImageCaptureUseCaseConfiguration;
+import androidx.camera.core.VideoCaptureUseCase;
+import androidx.camera.core.VideoCaptureUseCase.OnVideoSavedListener;
+import androidx.camera.core.VideoCaptureUseCaseConfiguration;
+import androidx.camera.core.ViewFinderUseCase;
+import androidx.camera.core.ViewFinderUseCaseConfiguration;
+import androidx.camera.view.CameraView.CaptureMode;
+import androidx.camera.view.CameraView.Quality;
+import androidx.lifecycle.DefaultLifecycleObserver;
+import androidx.lifecycle.Lifecycle;
+import androidx.lifecycle.LifecycleObserver;
+import androidx.lifecycle.LifecycleOwner;
+
+import java.io.File;
+import java.util.Arrays;
+import java.util.LinkedHashSet;
+import java.util.Set;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+/** CameraX use case operation built on @{link androidx.camera.core}. */
+final class CameraXModule {
+ public static final String TAG = "CameraXModule";
+
+ private static final int MAX_VIEW_DIMENSION = 2000;
+ private static final float UNITY_ZOOM_SCALE = 1f;
+ private static final float ZOOM_NOT_SUPPORTED = UNITY_ZOOM_SCALE;
+ private static final Rational ASPECT_RATIO_16_9 = new Rational(16, 9);
+ private static final Rational ASPECT_RATIO_4_3 = new Rational(4, 3);
+
+ private final CameraManager cameraManager;
+ private final ViewFinderUseCaseConfiguration.Builder viewFinderConfigBuilder;
+ private final VideoCaptureUseCaseConfiguration.Builder videoCaptureConfigBuilder;
+ private final ImageCaptureUseCaseConfiguration.Builder imageCaptureConfigBuilder;
+ private final CameraView cameraView;
+ final AtomicBoolean videoIsRecording = new AtomicBoolean(false);
+ private CameraView.Quality quality = CameraView.Quality.HIGH;
+ private CameraView.CaptureMode captureMode = CaptureMode.IMAGE;
+ private long maxVideoDuration = CameraView.INDEFINITE_VIDEO_DURATION;
+ private long maxVideoSize = CameraView.INDEFINITE_VIDEO_SIZE;
+ private FlashMode flash = FlashMode.OFF;
+ @Nullable
+ private ImageCaptureUseCase imageCaptureUseCase;
+ @Nullable
+ private VideoCaptureUseCase videoCaptureUseCase;
+ @Nullable
+ ViewFinderUseCase viewFinderUseCase;
+ @Nullable
+ LifecycleOwner currentLifecycle;
+ private final LifecycleObserver currentLifecycleObserver =
+ new DefaultLifecycleObserver() {
+ @Override
+ public void onDestroy(LifecycleOwner owner) {
+ if (owner == currentLifecycle) {
+ clearCurrentLifecycle();
+ viewFinderUseCase.removeViewFinderOutputListener();
+ }
+ }
+ };
+ @Nullable
+ private LifecycleOwner newLifecycle;
+ private float zoomLevel = UNITY_ZOOM_SCALE;
+ @Nullable
+ private Rect cropRegion;
+ @Nullable
+ private CameraX.LensFacing cameraLensFacing = LensFacing.BACK;
+
+ public CameraXModule(CameraView view) {
+ this.cameraView = view;
+
+ cameraManager = (CameraManager) view.getContext().getSystemService(Context.CAMERA_SERVICE);
+
+ viewFinderConfigBuilder =
+ new ViewFinderUseCaseConfiguration.Builder().setTargetName("ViewFinder");
+
+ imageCaptureConfigBuilder =
+ new ImageCaptureUseCaseConfiguration.Builder().setTargetName("ImageCapture");
+
+ videoCaptureConfigBuilder =
+ new VideoCaptureUseCaseConfiguration.Builder().setTargetName("VideoCapture");
+ }
+
+ /**
+ * Rescales view rectangle with dimensions in [-1000, 1000] to a corresponding rectangle in the
+ * sensor coordinate frame.
+ */
+ private static Rect rescaleViewRectToSensorRect(Rect view, Rect sensor) {
+ // Scale width and height.
+ int newWidth = Math.round(view.width() * sensor.width() / (float) MAX_VIEW_DIMENSION);
+ int newHeight = Math.round(view.height() * sensor.height() / (float) MAX_VIEW_DIMENSION);
+
+ // Scale top/left corner.
+ int halfViewDimension = MAX_VIEW_DIMENSION / 2;
+ int leftOffset =
+ Math.round(
+ (view.left + halfViewDimension)
+ * sensor.width()
+ / (float) MAX_VIEW_DIMENSION)
+ + sensor.left;
+ int topOffset =
+ Math.round(
+ (view.top + halfViewDimension)
+ * sensor.height()
+ / (float) MAX_VIEW_DIMENSION)
+ + sensor.top;
+
+ // Now, produce the scaled rect.
+ Rect scaled = new Rect();
+ scaled.left = leftOffset;
+ scaled.top = topOffset;
+ scaled.right = scaled.left + newWidth;
+ scaled.bottom = scaled.top + newHeight;
+ return scaled;
+ }
+
+ @RequiresPermission(permission.CAMERA)
+ public void bindToLifecycle(LifecycleOwner lifecycleOwner) {
+ newLifecycle = lifecycleOwner;
+
+ if (getMeasuredWidth() > 0 && getMeasuredHeight() > 0) {
+ bindToLifecycleAfterViewMeasured();
+ }
+ }
+
+ @RequiresPermission(permission.CAMERA)
+ void bindToLifecycleAfterViewMeasured() {
+ if (newLifecycle == null) {
+ return;
+ }
+
+ clearCurrentLifecycle();
+ currentLifecycle = newLifecycle;
+ newLifecycle = null;
+ if (currentLifecycle.getLifecycle().getCurrentState() == Lifecycle.State.DESTROYED) {
+ currentLifecycle = null;
+ throw new IllegalArgumentException("Cannot bind to lifecycle in a destroyed state.");
+ }
+
+ int cameraOrientation;
+ try {
+ String cameraId;
+ Set<LensFacing> available = getAvailableCameraLensFacing();
+
+ if (available.isEmpty()) {
+ Log.w(TAG, "Unable to bindToLifeCycle since no cameras available");
+ cameraLensFacing = null;
+ }
+
+ // Ensure the current camera exists, or default to another camera
+ if (cameraLensFacing != null && !available.contains(cameraLensFacing)) {
+ Log.w(TAG, "Camera does not exist with direction " + cameraLensFacing);
+
+ // Default to the first available camera direction
+ cameraLensFacing = available.iterator().next();
+
+ Log.w(TAG, "Defaulting to primary camera with direction " + cameraLensFacing);
+ }
+
+ // Do not attempt to create use cases for a null cameraLensFacing. This could occur if
+ // the
+ // user explicitly sets the LensFacing to null, or if we determined there
+ // were no available cameras, which should be logged in the logic above.
+ if (cameraLensFacing == null) {
+ return;
+ }
+
+ cameraId = CameraX.getCameraWithLensFacing(cameraLensFacing);
+ if (cameraId == null) {
+ return;
+ }
+ CameraInfo cameraInfo = CameraX.getCameraInfo(cameraId);
+ cameraOrientation = cameraInfo.getSensorRotationDegrees();
+ } catch (Exception e) {
+ throw new IllegalStateException("Unable to bind to lifecycle.", e);
+ }
+
+ // Set the preferred aspect ratio as 4:3 if it is IMAGE only mode. Set the preferred aspect
+ // ratio as 16:9 if it is VIDEO or MIXED mode. Then, it will be WYSIWYG when the view finder
+ // is
+ // in CENTER_INSIDE mode.
+ if (getCaptureMode() == CaptureMode.IMAGE) {
+ imageCaptureConfigBuilder.setTargetAspectRatio(ASPECT_RATIO_4_3);
+ viewFinderConfigBuilder.setTargetAspectRatio(ASPECT_RATIO_4_3);
+ } else {
+ imageCaptureConfigBuilder.setTargetAspectRatio(ASPECT_RATIO_16_9);
+ viewFinderConfigBuilder.setTargetAspectRatio(ASPECT_RATIO_16_9);
+ }
+
+ imageCaptureConfigBuilder.setTargetRotation(getDisplaySurfaceRotation());
+ imageCaptureConfigBuilder.setLensFacing(cameraLensFacing);
+ imageCaptureUseCase = new ImageCaptureUseCase(imageCaptureConfigBuilder.build());
+
+ videoCaptureConfigBuilder.setTargetRotation(getDisplaySurfaceRotation());
+ videoCaptureConfigBuilder.setLensFacing(cameraLensFacing);
+ videoCaptureUseCase = new VideoCaptureUseCase(videoCaptureConfigBuilder.build());
+ viewFinderConfigBuilder.setLensFacing(cameraLensFacing);
+
+ int relativeCameraOrientation = getRelativeCameraOrientation(false);
+
+ if (relativeCameraOrientation == 90 || relativeCameraOrientation == 270) {
+ viewFinderConfigBuilder.setTargetResolution(
+ new Size(getMeasuredHeight(), getMeasuredWidth()));
+ } else {
+ viewFinderConfigBuilder.setTargetResolution(
+ new Size(getMeasuredWidth(), getMeasuredHeight()));
+ }
+
+ viewFinderUseCase = new ViewFinderUseCase(viewFinderConfigBuilder.build());
+ viewFinderUseCase.setOnViewFinderOutputUpdateListener(
+ output -> {
+ boolean needReverse = cameraOrientation != 0 && cameraOrientation != 180;
+ int textureWidth =
+ needReverse
+ ? output.getTextureSize().getHeight()
+ : output.getTextureSize().getWidth();
+ int textureHeight =
+ needReverse
+ ? output.getTextureSize().getWidth()
+ : output.getTextureSize().getHeight();
+ onViewfinderSourceDimensUpdated(textureWidth, textureHeight);
+ setSurfaceTexture(output.getSurfaceTexture());
+ });
+
+ if (getCaptureMode() == CaptureMode.IMAGE) {
+ CameraX.bindToLifecycle(currentLifecycle, imageCaptureUseCase, viewFinderUseCase);
+ } else if (getCaptureMode() == CaptureMode.VIDEO) {
+ CameraX.bindToLifecycle(currentLifecycle, videoCaptureUseCase, viewFinderUseCase);
+ } else {
+ CameraX.bindToLifecycle(
+ currentLifecycle, imageCaptureUseCase, videoCaptureUseCase, viewFinderUseCase);
+ }
+ setZoomLevel(zoomLevel);
+ currentLifecycle.getLifecycle().addObserver(currentLifecycleObserver);
+ // Enable flash setting in ImageCaptureUseCase after use cases are created and binded.
+ setFlash(getFlash());
+ }
+
+ public void open() {
+ throw new UnsupportedOperationException(
+ "Explicit open/close of camera not yet supported. Use bindtoLifecycle() instead.");
+ }
+
+ public void close() {
+ throw new UnsupportedOperationException(
+ "Explicit open/close of camera not yet supported. Use bindtoLifecycle() instead.");
+ }
+
+ public void takePicture(OnImageCapturedListener listener) {
+ if (imageCaptureUseCase == null) {
+ return;
+ }
+
+ if (getCaptureMode() == CaptureMode.VIDEO) {
+ throw new IllegalStateException("Can not take picture under VIDEO capture mode.");
+ }
+
+ if (listener == null) {
+ throw new IllegalArgumentException("OnImageCapturedListener should not be empty");
+ }
+
+ imageCaptureUseCase.takePicture(listener);
+ }
+
+ public void takePicture(File saveLocation, OnImageSavedListener listener) {
+ if (imageCaptureUseCase == null) {
+ return;
+ }
+
+ if (getCaptureMode() == CaptureMode.VIDEO) {
+ throw new IllegalStateException("Can not take picture under VIDEO capture mode.");
+ }
+
+ if (listener == null) {
+ throw new IllegalArgumentException("OnImageSavedListener should not be empty");
+ }
+
+ ImageCaptureUseCase.Metadata metadata = new ImageCaptureUseCase.Metadata();
+ metadata.isReversedHorizontal = cameraLensFacing == LensFacing.FRONT;
+ imageCaptureUseCase.takePicture(saveLocation, listener, metadata);
+ }
+
+ public void startRecording(File file, OnVideoSavedListener listener) {
+ if (videoCaptureUseCase == null) {
+ return;
+ }
+
+ if (getCaptureMode() == CaptureMode.IMAGE) {
+ throw new IllegalStateException("Can not record video under IMAGE capture mode.");
+ }
+
+ if (listener == null) {
+ throw new IllegalArgumentException("OnVideoSavedListener should not be empty");
+ }
+
+ videoIsRecording.set(true);
+ videoCaptureUseCase.startRecording(
+ file,
+ new VideoCaptureUseCase.OnVideoSavedListener() {
+ @Override
+ public void onVideoSaved(File savedFile) {
+ videoIsRecording.set(false);
+ listener.onVideoSaved(savedFile);
+ }
+
+ @Override
+ public void onError(
+ VideoCaptureUseCase.UseCaseError useCaseError,
+ String message,
+ @Nullable Throwable cause) {
+ videoIsRecording.set(false);
+ Log.e(TAG, message, cause);
+ listener.onError(useCaseError, message, cause);
+ }
+ });
+ }
+
+ public void stopRecording() {
+ if (videoCaptureUseCase == null) {
+ return;
+ }
+
+ videoCaptureUseCase.stopRecording();
+ }
+
+ public boolean isRecording() {
+ return videoIsRecording.get();
+ }
+
+ // TODO(b/124269166): Rethink how we can handle permissions here.
+ @SuppressLint("MissingPermission")
+ public void setCameraByLensFacing(@Nullable LensFacing lensFacing) {
+ // Setting same lens facing is a no-op, so check for that first
+ if (cameraLensFacing != lensFacing) {
+ // If we're not bound to a lifecycle, just update the camera that will be opened when we
+ // attach to a lifecycle.
+ cameraLensFacing = lensFacing;
+
+ if (currentLifecycle != null) {
+ // Re-bind to lifecycle with new camera
+ bindToLifecycle(currentLifecycle);
+ }
+ }
+ }
+
+ @RequiresPermission(permission.CAMERA)
+ public boolean hasCameraWithLensFacing(LensFacing lensFacing) {
+ String cameraId;
+ try {
+ cameraId = CameraX.getCameraWithLensFacing(lensFacing);
+ } catch (Exception e) {
+ throw new IllegalStateException("Unable to query lens facing.", e);
+ }
+
+ return cameraId != null;
+ }
+
+ @Nullable
+ public LensFacing getLensFacing() {
+ return cameraLensFacing;
+ }
+
+ public void toggleCamera() {
+ // TODO(b/124269166): Rethink how we can handle permissions here.
+ @SuppressLint("MissingPermission")
+ Set<LensFacing> availableCameraLensFacing = getAvailableCameraLensFacing();
+
+ if (availableCameraLensFacing.isEmpty()) {
+ return;
+ }
+
+ if (cameraLensFacing == null) {
+ setCameraByLensFacing(availableCameraLensFacing.iterator().next());
+ return;
+ }
+
+ if (cameraLensFacing == LensFacing.BACK
+ && availableCameraLensFacing.contains(LensFacing.FRONT)) {
+ setCameraByLensFacing(LensFacing.FRONT);
+ return;
+ }
+
+ if (cameraLensFacing == LensFacing.FRONT
+ && availableCameraLensFacing.contains(LensFacing.BACK)) {
+ setCameraByLensFacing(LensFacing.BACK);
+ return;
+ }
+ }
+
+ public void focus(Rect focus, Rect metering) {
+ if (viewFinderUseCase == null) {
+ // Nothing to focus on since we don't yet have a viewfinder
+ return;
+ }
+
+ Rect rescaledFocus;
+ Rect rescaledMetering;
+ try {
+ Rect sensorRegion;
+ if (cropRegion != null) {
+ sensorRegion = cropRegion;
+ } else {
+ sensorRegion = getSensorSize(getActiveCamera());
+ }
+ rescaledFocus = rescaleViewRectToSensorRect(focus, sensorRegion);
+ rescaledMetering = rescaleViewRectToSensorRect(metering, sensorRegion);
+ } catch (Exception e) {
+ Log.e(TAG, "Failed to rescale the focus and metering rectangles.", e);
+ return;
+ }
+
+ viewFinderUseCase.focus(rescaledFocus, rescaledMetering);
+ }
+
+ public float getZoomLevel() {
+ return zoomLevel;
+ }
+
+ public void setZoomLevel(float zoomLevel) {
+ // Set the zoom level in case it is set before binding to a lifecycle
+ this.zoomLevel = zoomLevel;
+
+ if (viewFinderUseCase == null) {
+ // Nothing to zoom on yet since we don't have a viewfinder. Defer calculating crop
+ // region.
+ return;
+ }
+
+ Rect sensorSize;
+ try {
+ sensorSize = getSensorSize(getActiveCamera());
+ if (sensorSize == null) {
+ Log.e(TAG, "Failed to get the sensor size.");
+ return;
+ }
+ } catch (Exception e) {
+ Log.e(TAG, "Failed to get the sensor size.", e);
+ return;
+ }
+
+ float minZoom = getMinZoomLevel();
+ float maxZoom = getMaxZoomLevel();
+
+ if (this.zoomLevel < minZoom) {
+ Log.e(TAG, "Requested zoom level is less than minimum zoom level.");
+ }
+ if (this.zoomLevel > maxZoom) {
+ Log.e(TAG, "Requested zoom level is greater than maximum zoom level.");
+ }
+ this.zoomLevel = Math.max(minZoom, Math.min(maxZoom, this.zoomLevel));
+
+ float zoomScaleFactor =
+ (maxZoom == minZoom) ? minZoom : (this.zoomLevel - minZoom) / (maxZoom - minZoom);
+ int minWidth = Math.round(sensorSize.width() / maxZoom);
+ int minHeight = Math.round(sensorSize.height() / maxZoom);
+ int diffWidth = sensorSize.width() - minWidth;
+ int diffHeight = sensorSize.height() - minHeight;
+ float cropWidth = diffWidth * zoomScaleFactor;
+ float cropHeight = diffHeight * zoomScaleFactor;
+
+ Rect cropRegion =
+ new Rect(
+ /*left=*/ (int) Math.ceil(cropWidth / 2 - 0.5f),
+ /*top=*/ (int) Math.ceil(cropHeight / 2 - 0.5f),
+ /*right=*/ (int) Math.floor(sensorSize.width() - cropWidth / 2 + 0.5f),
+ /*bottom=*/ (int) Math.floor(sensorSize.height() - cropHeight / 2 + 0.5f));
+
+ if (cropRegion.width() < 50 || cropRegion.height() < 50) {
+ Log.e(TAG, "Crop region is too small to compute 3A stats, so ignoring further zoom.");
+ return;
+ }
+ this.cropRegion = cropRegion;
+
+ viewFinderUseCase.zoom(cropRegion);
+ }
+
+ public float getMinZoomLevel() {
+ return UNITY_ZOOM_SCALE;
+ }
+
+ public float getMaxZoomLevel() {
+ try {
+ CameraCharacteristics characteristics =
+ cameraManager.getCameraCharacteristics(getActiveCamera());
+ Float maxZoom =
+ characteristics.get(CameraCharacteristics.SCALER_AVAILABLE_MAX_DIGITAL_ZOOM);
+ if (maxZoom == null) {
+ return ZOOM_NOT_SUPPORTED;
+ }
+ if (maxZoom == ZOOM_NOT_SUPPORTED) {
+ return ZOOM_NOT_SUPPORTED;
+ }
+ return maxZoom;
+ } catch (Exception e) {
+ Log.e(TAG, "Failed to get SCALER_AVAILABLE_MAX_DIGITAL_ZOOM.", e);
+ }
+ return ZOOM_NOT_SUPPORTED;
+ }
+
+ public boolean isZoomSupported() {
+ return getMaxZoomLevel() != ZOOM_NOT_SUPPORTED;
+ }
+
+ // TODO(b/124269166): Rethink how we can handle permissions here.
+ @SuppressLint("MissingPermission")
+ private void rebindToLifecycle() {
+ if (currentLifecycle != null) {
+ bindToLifecycle(currentLifecycle);
+ }
+ }
+
+ int getRelativeCameraOrientation(boolean compensateForMirroring) {
+ int rotationDegrees;
+ try {
+ String cameraId = CameraX.getCameraWithLensFacing(getLensFacing());
+ CameraInfo cameraInfo = CameraX.getCameraInfo(cameraId);
+ rotationDegrees = cameraInfo.getSensorRotationDegrees(getDisplaySurfaceRotation());
+ if (compensateForMirroring) {
+ rotationDegrees = (360 - rotationDegrees) % 360;
+ }
+ } catch (Exception e) {
+ Log.e(TAG, "Failed to query camera", e);
+ rotationDegrees = 0;
+ }
+
+ return rotationDegrees;
+ }
+
+ public CameraView.Quality getQuality() {
+ return quality;
+ }
+
+ public void setQuality(Quality quality) {
+ if (quality != Quality.HIGH) {
+ throw new UnsupportedOperationException("Only supported Quality is HIGH");
+ }
+ this.quality = quality;
+ }
+
+ public void invalidateView() {
+ transformPreview();
+ updateViewInfo();
+ }
+
+ void clearCurrentLifecycle() {
+ if (currentLifecycle != null) {
+ // Remove previous use cases
+ CameraX.unbind(imageCaptureUseCase, videoCaptureUseCase, viewFinderUseCase);
+ }
+
+ currentLifecycle = null;
+ }
+
+ private Rect getSensorSize(String cameraId) throws CameraAccessException {
+ CameraCharacteristics characteristics = cameraManager.getCameraCharacteristics(cameraId);
+ return characteristics.get(CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE);
+ }
+
+ String getActiveCamera() throws CameraInfoUnavailableException {
+ return CameraX.getCameraWithLensFacing(cameraLensFacing);
+ }
+
+ @UiThread
+ private void transformPreview() {
+ int viewfinderWidth = getViewFinderWidth();
+ int viewfinderHeight = getViewFinderHeight();
+ int displayOrientation = getDisplayRotationDegrees();
+
+ Matrix matrix = new Matrix();
+
+ // Apply rotation of the display
+ int rotation = -displayOrientation;
+
+ int px = (int) Math.round(viewfinderWidth / 2d);
+ int py = (int) Math.round(viewfinderHeight / 2d);
+
+ matrix.postRotate(rotation, px, py);
+
+ if (displayOrientation == 90 || displayOrientation == 270) {
+ // Swap width and height
+ float xScale = viewfinderWidth / (float) viewfinderHeight;
+ float yScale = viewfinderHeight / (float) viewfinderWidth;
+
+ matrix.postScale(xScale, yScale, px, py);
+ }
+
+ setTransform(matrix);
+ }
+
+ // Update view related information used in use cases
+ private void updateViewInfo() {
+ if (imageCaptureUseCase != null) {
+ imageCaptureUseCase.setTargetAspectRatio(new Rational(getWidth(), getHeight()));
+ imageCaptureUseCase.setTargetRotation(getDisplaySurfaceRotation());
+ }
+
+ if (videoCaptureUseCase != null) {
+ videoCaptureUseCase.setTargetRotation(getDisplaySurfaceRotation());
+ }
+ }
+
+ @RequiresPermission(permission.CAMERA)
+ private Set<LensFacing> getAvailableCameraLensFacing() {
+ // Start with all camera directions
+ Set<LensFacing> available = new LinkedHashSet<>(Arrays.asList(LensFacing.values()));
+
+ // If we're bound to a lifecycle, remove unavailable cameras
+ if (currentLifecycle != null) {
+ if (!hasCameraWithLensFacing(LensFacing.BACK)) {
+ available.remove(LensFacing.BACK);
+ }
+
+ if (!hasCameraWithLensFacing(LensFacing.FRONT)) {
+ available.remove(LensFacing.FRONT);
+ }
+ }
+
+ return available;
+ }
+
+ public FlashMode getFlash() {
+ return flash;
+ }
+
+ public void setFlash(FlashMode flash) {
+ this.flash = flash;
+
+ if (imageCaptureUseCase == null) {
+ // Do nothing if there is no imageCaptureUseCase
+ return;
+ }
+
+ imageCaptureUseCase.setFlashMode(flash);
+ }
+
+ public void enableTorch(boolean torch) {
+ if (viewFinderUseCase == null) {
+ return;
+ }
+ viewFinderUseCase.enableTorch(torch);
+ }
+
+ public boolean isTorchOn() {
+ if (viewFinderUseCase == null) {
+ return false;
+ }
+ return viewFinderUseCase.isTorchOn();
+ }
+
+ public Context getContext() {
+ return cameraView.getContext();
+ }
+
+ public int getWidth() {
+ return cameraView.getWidth();
+ }
+
+ public int getHeight() {
+ return cameraView.getHeight();
+ }
+
+ public int getDisplayRotationDegrees() {
+ return CameraOrientationUtil.surfaceRotationToDegrees(getDisplaySurfaceRotation());
+ }
+
+ protected int getDisplaySurfaceRotation() {
+ return cameraView.getDisplaySurfaceRotation();
+ }
+
+ public void setSurfaceTexture(SurfaceTexture st) {
+ cameraView.setSurfaceTexture(st);
+ }
+
+ private int getViewFinderWidth() {
+ return cameraView.getViewFinderWidth();
+ }
+
+ private int getViewFinderHeight() {
+ return cameraView.getViewFinderHeight();
+ }
+
+ private int getMeasuredWidth() {
+ return cameraView.getMeasuredWidth();
+ }
+
+ private int getMeasuredHeight() {
+ return cameraView.getMeasuredHeight();
+ }
+
+ void setTransform(final Matrix matrix) {
+ if (Looper.myLooper() != Looper.getMainLooper()) {
+ cameraView.post(
+ new Runnable() {
+ @Override
+ public void run() {
+ setTransform(matrix);
+ }
+ });
+ } else {
+ cameraView.setTransform(matrix);
+ }
+ }
+
+ /**
+ * Notify the view that the source dimensions have changed.
+ *
+ * <p>This will allow the view to layout the viewfinder to display the correct aspect ratio.
+ *
+ * @param width width of camera source buffers.
+ * @param height height of camera source buffers.
+ */
+ private void onViewfinderSourceDimensUpdated(int width, int height) {
+ cameraView.onViewfinderSourceDimensUpdated(width, height);
+ }
+
+ public CameraView.CaptureMode getCaptureMode() {
+ return captureMode;
+ }
+
+ public void setCaptureMode(CameraView.CaptureMode captureMode) {
+ this.captureMode = captureMode;
+ rebindToLifecycle();
+ }
+
+ public long getMaxVideoDuration() {
+ return maxVideoDuration;
+ }
+
+ public void setMaxVideoDuration(long duration) {
+ maxVideoDuration = duration;
+ }
+
+ public long getMaxVideoSize() {
+ return maxVideoSize;
+ }
+
+ public void setMaxVideoSize(long size) {
+ maxVideoSize = size;
+ }
+
+ public boolean isPaused() {
+ return false;
+ }
+}
diff --git a/camera/view/src/main/res-public/values/public_attrs.xml b/camera/view/src/main/res-public/values/public_attrs.xml
new file mode 100644
index 0000000..3202640
--- /dev/null
+++ b/camera/view/src/main/res-public/values/public_attrs.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2019 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.
+-->
+<!-- Definitions of attributes to be exposed as public -->
+<resources>
+ <!-- CameraView -->
+ <public name="scaleType" type="attr" />
+ <public name="direction" type="attr" />
+ <public name="captureMode" type="attr" />
+ <public name="flash" type="attr" />
+</resources>
diff --git a/camera/view/src/main/res/values/attrs.xml b/camera/view/src/main/res/values/attrs.xml
new file mode 100644
index 0000000..9b01df2
--- /dev/null
+++ b/camera/view/src/main/res/values/attrs.xml
@@ -0,0 +1,46 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2019 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.
+-->
+<resources>
+ <declare-styleable name="CameraView">
+ <attr name="scaleType" format="enum">
+ <enum name="centerCrop" value="0" />
+ <enum name="centerInside" value="1" />
+ </attr>
+ <attr name="lensFacing" format="enum">
+ <enum name="none" value="0" />
+ <enum name="front" value="1" />
+ <enum name="back" value="2" />
+ </attr>
+ <attr name="quality" format="enum">
+ <enum name="max" value="0" />
+ <enum name="high" value="1" />
+ <enum name="medium" value="2" />
+ <enum name="low" value="3" />
+ </attr>
+ <attr name="captureMode" format="enum">
+ <enum name="image" value="0" />
+ <enum name="video" value="1" />
+ <enum name="mixed" value="2" />
+ </attr>
+ <attr name="flash" format="enum">
+ <enum name="auto" value="1" />
+ <enum name="on" value="2" />
+ <enum name="off" value="4" />
+ </attr>
+
+ <attr name="pinchToZoomEnabled" format="boolean" />
+ </declare-styleable>
+</resources>