Add all Camera code from internal repo
This is a direct copy of our code from our internal repo. Still needs
to be cleaned up for code style and to match AndroidX build files.
Bug: 121003129
Test: All unit tests pass before copy.
Change-Id: Id18d2e13c53fc35a013c7709598ee59b53472576
diff --git a/camera/core/src/androidTest/java/androidx/camera/app/camera2interopburst/AndroidManifest.xml b/camera/core/src/androidTest/java/androidx/camera/app/camera2interopburst/AndroidManifest.xml
new file mode 100644
index 0000000..69bde8e
--- /dev/null
+++ b/camera/core/src/androidTest/java/androidx/camera/app/camera2interopburst/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.app.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/core/src/androidTest/java/androidx/camera/app/camera2interopburst/MainActivity.java b/camera/core/src/androidTest/java/androidx/camera/app/camera2interopburst/MainActivity.java
new file mode 100644
index 0000000..4261c2c
--- /dev/null
+++ b/camera/core/src/androidTest/java/androidx/camera/app/camera2interopburst/MainActivity.java
@@ -0,0 +1,357 @@
+/*
+ * 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.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();
+
+ // Tracks the burst state.
+ private final Object burstLock = new Object();
+
+ @GuardedBy("burstLock")
+ private boolean burstInProgress = false;
+
+ @GuardedBy("burstLock")
+ private int burstFrameCount = 0;
+
+ // Camera2 interop objects.
+ private final SessionUpdatingSessionStateCallback sessionStateCallback =
+ new SessionUpdatingSessionStateCallback();
+ private final RequestUpdatingSessionCaptureCallback sessionCaptureCallback =
+ new RequestUpdatingSessionCaptureCallback();
+
+ // For visualizing the images captured in the burst.
+ private ImageView imageView;
+ 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<>();
+
+ // For running ops on a background thread.
+ private Handler backgroundHandler;
+ private HandlerThread backgroundHandlerThread;
+
+ @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);
+ }
+ }
+
+ private static void makePermissionRequest(Activity context) {
+ ActivityCompat.requestPermissions(context, new String[]{Manifest.permission.CAMERA},
+ CAMERA_REQUEST_CODE);
+ }
+
+ @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: {}
+ }
+ }
+
+ /** 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;
+ }
+ }
+
+ 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);
+ }
+ }
+ }
+ }
+}
diff --git a/camera/core/src/androidTest/java/androidx/camera/app/camera2interopburst/RequestUpdatingSessionCaptureCallback.java b/camera/core/src/androidTest/java/androidx/camera/app/camera2interopburst/RequestUpdatingSessionCaptureCallback.java
new file mode 100644
index 0000000..f5244b7
--- /dev/null
+++ b/camera/core/src/androidTest/java/androidx/camera/app/camera2interopburst/RequestUpdatingSessionCaptureCallback.java
@@ -0,0 +1,63 @@
+/*
+ * 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.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/core/src/androidTest/java/androidx/camera/app/camera2interopburst/SessionUpdatingSessionStateCallback.java b/camera/core/src/androidTest/java/androidx/camera/app/camera2interopburst/SessionUpdatingSessionStateCallback.java
new file mode 100644
index 0000000..c408944
--- /dev/null
+++ b/camera/core/src/androidTest/java/androidx/camera/app/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.app.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/core/src/androidTest/java/androidx/camera/app/camera2interopburst/build.gradle b/camera/core/src/androidTest/java/androidx/camera/app/camera2interopburst/build.gradle
new file mode 100644
index 0000000..3b12af3
--- /dev/null
+++ b/camera/core/src/androidTest/java/androidx/camera/app/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.app.camera2interopburst"
+ minSdkVersion project.ext.minSdk
+ targetSdkVersion project.ext.targetSdk
+ versionCode 1
+ versionName project.ext.version
+ multiDexEnabled true
+ }
+
+ sourceSets {
+ main.manifest.srcFile 'AndroidManifest.xml'
+ main.java.srcDirs = ['.']
+ main.java.excludes = ['**/build/**']
+ main.java.includes = ['*.java']
+ main.res.srcDirs = ['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/core/src/androidTest/java/androidx/camera/app/camera2interopburst/res/layout/activity_main.xml b/camera/core/src/androidTest/java/androidx/camera/app/camera2interopburst/res/layout/activity_main.xml
new file mode 100644
index 0000000..5a19a22
--- /dev/null
+++ b/camera/core/src/androidTest/java/androidx/camera/app/camera2interopburst/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:elevation="2dp"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="0dp"
+ android:layout_marginLeft="0dp" />
+ </RelativeLayout>
+
+</android.support.constraint.ConstraintLayout>
diff --git a/camera/core/src/androidTest/java/androidx/camera/app/camera2interopburst/res/values/strings.xml b/camera/core/src/androidTest/java/androidx/camera/app/camera2interopburst/res/values/strings.xml
new file mode 100644
index 0000000..f88c72a
--- /dev/null
+++ b/camera/core/src/androidTest/java/androidx/camera/app/camera2interopburst/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/core/src/androidTest/java/androidx/camera/app/camera2interoperror/AndroidManifest.xml b/camera/core/src/androidTest/java/androidx/camera/app/camera2interoperror/AndroidManifest.xml
new file mode 100644
index 0000000..54d7570
--- /dev/null
+++ b/camera/core/src/androidTest/java/androidx/camera/app/camera2interoperror/AndroidManifest.xml
@@ -0,0 +1,35 @@
+<?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.app.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="androidx.camera.app.camera2interoperror.CameraXInteropErrorApplication"
+ android:theme="@style/AppTheme">
+ <activity
+ android:name="androidx.camera.app.camera2interoperror.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/core/src/androidTest/java/androidx/camera/app/camera2interoperror/Camera2InteropErrorUseCase.java b/camera/core/src/androidTest/java/androidx/camera/app/camera2interoperror/Camera2InteropErrorUseCase.java
new file mode 100644
index 0000000..f0b2606
--- /dev/null
+++ b/camera/core/src/androidTest/java/androidx/camera/app/camera2interoperror/Camera2InteropErrorUseCase.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.app.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 CameraDevice cameraDevice;
+ private ImageReader imageReader;
+ private final Camera2InteropErrorUseCaseConfiguration configuration;
+
+ 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 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()");
+ }
+ };
+
+ 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/core/src/androidTest/java/androidx/camera/app/camera2interoperror/Camera2InteropErrorUseCaseConfiguration.java b/camera/core/src/androidTest/java/androidx/camera/app/camera2interoperror/Camera2InteropErrorUseCaseConfiguration.java
new file mode 100644
index 0000000..1ec830c
--- /dev/null
+++ b/camera/core/src/androidTest/java/androidx/camera/app/camera2interoperror/Camera2InteropErrorUseCaseConfiguration.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.app.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/core/src/androidTest/java/androidx/camera/app/camera2interoperror/CameraXInteropErrorActivity.java b/camera/core/src/androidTest/java/androidx/camera/app/camera2interoperror/CameraXInteropErrorActivity.java
new file mode 100644
index 0000000..df1066f
--- /dev/null
+++ b/camera/core/src/androidTest/java/androidx/camera/app/camera2interoperror/CameraXInteropErrorActivity.java
@@ -0,0 +1,287 @@
+/*
+ * 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.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";
+
+ /** 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
+ }
+
+ 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
+ }
+ }
+}
diff --git a/camera/core/src/androidTest/java/androidx/camera/app/camera2interoperror/CameraXInteropErrorApplication.java b/camera/core/src/androidTest/java/androidx/camera/app/camera2interoperror/CameraXInteropErrorApplication.java
new file mode 100644
index 0000000..759be10
--- /dev/null
+++ b/camera/core/src/androidTest/java/androidx/camera/app/camera2interoperror/CameraXInteropErrorApplication.java
@@ -0,0 +1,29 @@
+/*
+ * 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.camera2interoperror;
+
+import android.app.Application;
+
+/** An application for CameraX. */
+public class CameraXInteropErrorApplication extends Application {
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+ }
+}
+
diff --git a/camera/core/src/androidTest/java/androidx/camera/app/camera2interoperror/build.gradle b/camera/core/src/androidTest/java/androidx/camera/app/camera2interoperror/build.gradle
new file mode 100644
index 0000000..9f471fc
--- /dev/null
+++ b/camera/core/src/androidTest/java/androidx/camera/app/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.app.camera2interoperror"
+ minSdkVersion project.ext.minSdk
+ targetSdkVersion project.ext.targetSdk
+ versionCode 1
+ versionName project.ext.version
+ multiDexEnabled true
+ }
+
+ sourceSets {
+ main.manifest.srcFile 'AndroidManifest.xml'
+ main.java.srcDirs = ['.']
+ main.java.excludes = ['**/build/**']
+ main.java.includes = ['*.java']
+ main.res.srcDirs = ['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/core/src/androidTest/java/androidx/camera/app/camera2interoperror/res/layout/activity_camera_2main.xml b/camera/core/src/androidTest/java/androidx/camera/app/camera2interoperror/res/layout/activity_camera_2main.xml
new file mode 100644
index 0000000..55366dc
--- /dev/null
+++ b/camera/core/src/androidTest/java/androidx/camera/app/camera2interoperror/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_constraintTop_toTopOf="parent"
+ app:layout_constraintStart_toStartOf="parent"/>
+
+ <TextView
+ android:id="@+id/textView"
+ android:elevation="2dp"
+ android:layout_width="0dp"
+ android:layout_height="0dp"
+ android:background="#FFF"
+ android:src="@android:drawable/btn_radio"
+ app:layout_constraintBottom_toBottomOf="parent"
+ android:scaleType="fitXY"
+ 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:text="Select Error"
+ app:layout_constraintBottom_toBottomOf="parent"
+ android:scaleType="fitXY"
+ 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:text="Cause Error"
+ app:layout_constraintBottom_toBottomOf="parent"
+ android:scaleType="fitXY"
+ 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/core/src/androidTest/java/androidx/camera/app/camera2interoperror/res/values/strings.xml b/camera/core/src/androidTest/java/androidx/camera/app/camera2interoperror/res/values/strings.xml
new file mode 100644
index 0000000..2df0565
--- /dev/null
+++ b/camera/core/src/androidTest/java/androidx/camera/app/camera2interoperror/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/core/src/androidTest/java/androidx/camera/app/camera2interoperror/res/values/style.xml b/camera/core/src/androidTest/java/androidx/camera/app/camera2interoperror/res/values/style.xml
new file mode 100644
index 0000000..0c1afa6
--- /dev/null
+++ b/camera/core/src/androidTest/java/androidx/camera/app/camera2interoperror/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/core/src/androidTest/java/androidx/camera/app/hellocamerax/AndroidManifest.xml b/camera/core/src/androidTest/java/androidx/camera/app/hellocamerax/AndroidManifest.xml
new file mode 100644
index 0000000..deeaa4e
--- /dev/null
+++ b/camera/core/src/androidTest/java/androidx/camera/app/hellocamerax/AndroidManifest.xml
@@ -0,0 +1,37 @@
+<?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.app.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="androidx.camera.app.hellocamerax.CameraXApplication"
+ android:theme="@style/AppTheme">
+ <activity
+ android:name="androidx.camera.app.hellocamerax.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/core/src/androidTest/java/androidx/camera/app/hellocamerax/CameraXActivity.java b/camera/core/src/androidTest/java/androidx/camera/app/hellocamerax/CameraXActivity.java
new file mode 100644
index 0000000..da52ca4
--- /dev/null
+++ b/camera/core/src/androidTest/java/androidx/camera/app/hellocamerax/CameraXActivity.java
@@ -0,0 +1,680 @@
+/*
+ * 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.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 VideoFileSaver videoFileSaver;
+
+ /** The cameraId to use. Assume that 0 is the typical back facing camera. */
+ private LensFacing currentCameraLensFacing = LensFacing.BACK;
+
+ private String currentCameraDirection = "BACKWARD";
+
+ // TODO: Move the analysis processing, capture processing to separate threads, so
+ // there is smaller impact on the preview.
+
+ private ViewFinderUseCase viewFinderUseCase;
+ private ImageAnalysisUseCase imageAnalysisUseCase;
+ private ImageCaptureUseCase imageCaptureUseCase;
+ private VideoCaptureUseCase videoCaptureUseCase;
+
+ private final MutableLiveData<String> imageAnalysisResult = new MutableLiveData<>();
+
+ /**
+ * 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
+ }
+ }
+
+ /** 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();
+ }
+ }
+
+ 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;
+ }
+}
diff --git a/camera/core/src/androidTest/java/androidx/camera/app/hellocamerax/CameraXApplication.java b/camera/core/src/androidTest/java/androidx/camera/app/hellocamerax/CameraXApplication.java
new file mode 100644
index 0000000..60db9b9
--- /dev/null
+++ b/camera/core/src/androidTest/java/androidx/camera/app/hellocamerax/CameraXApplication.java
@@ -0,0 +1,29 @@
+/*
+ * 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.hellocamerax;
+
+import android.app.Application;
+
+/** An application for CameraX. */
+public class CameraXApplication extends Application {
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+ }
+}
+
diff --git a/camera/core/src/androidTest/java/androidx/camera/app/hellocamerax/VideoFileSaver.java b/camera/core/src/androidTest/java/androidx/camera/app/hellocamerax/VideoFileSaver.java
new file mode 100644
index 0000000..ecf0554
--- /dev/null
+++ b/camera/core/src/androidTest/java/androidx/camera/app/hellocamerax/VideoFileSaver.java
@@ -0,0 +1,90 @@
+/*
+ * 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.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 File rootDirectory;
+ private final Format formatter = new SimpleDateFormat("yyyy-MM-dd-HH-mm-ss", Locale.ENGLISH);
+
+ private final Object lock = new Object();
+ @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/core/src/androidTest/java/androidx/camera/app/hellocamerax/build.gradle b/camera/core/src/androidTest/java/androidx/camera/app/hellocamerax/build.gradle
new file mode 100644
index 0000000..09c6dd0
--- /dev/null
+++ b/camera/core/src/androidTest/java/androidx/camera/app/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.app.hellocamerax"
+ minSdkVersion project.ext.minSdk
+ targetSdkVersion project.ext.targetSdk
+ versionCode 1
+ versionName project.ext.version
+ multiDexEnabled true
+ }
+
+ sourceSets {
+ main.manifest.srcFile 'AndroidManifest.xml'
+ main.java.srcDirs = ['.']
+ main.java.excludes = ['**/build/**']
+ main.java.includes = ['*.java']
+ main.res.srcDirs = ['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/core/src/androidTest/java/androidx/camera/app/hellocamerax/res/layout/activity_camera_xmain.xml b/camera/core/src/androidTest/java/androidx/camera/app/hellocamerax/res/layout/activity_camera_xmain.xml
new file mode 100644
index 0000000..a37b6ab
--- /dev/null
+++ b/camera/core/src/androidTest/java/androidx/camera/app/hellocamerax/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_constraintTop_toTopOf="parent"
+ app:layout_constraintStart_toStartOf="parent"/>
+
+ <TextView
+ android:id="@+id/textView"
+ android:elevation="2dp"
+ android:layout_width="0dp"
+ android:layout_height="0dp"
+ android:background="#FFF"
+ android:src="@android:drawable/btn_radio"
+ app:layout_constraintBottom_toBottomOf="parent"
+ android:scaleType="fitXY"
+ 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:text="Picture"
+ app:layout_constraintBottom_toBottomOf="parent"
+ android:scaleType="fitXY"
+ 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:text="Record"
+ app:layout_constraintBottom_toBottomOf="parent"
+ android:scaleType="fitXY"
+ 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:text="Video"
+ app:layout_constraintBottom_toBottomOf="parent"
+ android:scaleType="fitXY"
+ 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:text="Photo"
+ app:layout_constraintBottom_toBottomOf="parent"
+ android:scaleType="fitXY"
+ 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:text="Analysis"
+ app:layout_constraintBottom_toBottomOf="parent"
+ android:scaleType="fitXY"
+ 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:text="Preview"
+ app:layout_constraintBottom_toBottomOf="parent"
+ android:scaleType="fitXY"
+ 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/core/src/androidTest/java/androidx/camera/app/hellocamerax/res/values/strings.xml b/camera/core/src/androidTest/java/androidx/camera/app/hellocamerax/res/values/strings.xml
new file mode 100644
index 0000000..d4eaf3c
--- /dev/null
+++ b/camera/core/src/androidTest/java/androidx/camera/app/hellocamerax/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/core/src/androidTest/java/androidx/camera/app/hellocamerax/res/values/style.xml b/camera/core/src/androidTest/java/androidx/camera/app/hellocamerax/res/values/style.xml
new file mode 100644
index 0000000..0c1afa6
--- /dev/null
+++ b/camera/core/src/androidTest/java/androidx/camera/app/hellocamerax/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/core/src/androidTest/java/androidx/camera/app/javacameraxpermissions/AndroidManifest.xml b/camera/core/src/androidTest/java/androidx/camera/app/javacameraxpermissions/AndroidManifest.xml
new file mode 100644
index 0000000..b085f64
--- /dev/null
+++ b/camera/core/src/androidTest/java/androidx/camera/app/javacameraxpermissions/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.app.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="androidx.camera.app.javacameraxpermissions.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/core/src/androidTest/java/androidx/camera/app/javacameraxpermissions/MainActivity.java b/camera/core/src/androidTest/java/androidx/camera/app/javacameraxpermissions/MainActivity.java
new file mode 100644
index 0000000..517c7a7
--- /dev/null
+++ b/camera/core/src/androidTest/java/androidx/camera/app/javacameraxpermissions/MainActivity.java
@@ -0,0 +1,115 @@
+/*
+ * 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.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 final CompletableFuture<Integer> cf = new CompletableFuture<>();
+ private static final String TAG = MainActivity.class.getName();
+ private static final int CAMERA_REQUEST_CODE = 101;
+
+ @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);
+ }
+ }
+
+ private static void makePermissionRequest(Activity context) {
+ ActivityCompat.requestPermissions(context, new String[]{Manifest.permission.CAMERA},
+ CAMERA_REQUEST_CODE);
+ }
+
+ @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/core/src/androidTest/java/androidx/camera/app/javacameraxpermissions/build.gradle b/camera/core/src/androidTest/java/androidx/camera/app/javacameraxpermissions/build.gradle
new file mode 100644
index 0000000..2316300
--- /dev/null
+++ b/camera/core/src/androidTest/java/androidx/camera/app/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.app.javacameraxpermissions"
+ minSdkVersion project.ext.minSdk
+ targetSdkVersion project.ext.targetSdk
+ versionCode 1
+ versionName project.ext.version
+ multiDexEnabled true
+ }
+
+ sourceSets {
+ main.manifest.srcFile 'AndroidManifest.xml'
+ main.java.srcDirs = ['.']
+ main.java.excludes = ['**/build/**']
+ main.java.includes = ['*.java']
+ main.res.srcDirs = ['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/core/src/androidTest/java/androidx/camera/app/javacameraxpermissions/res/layout/activity_main.xml b/camera/core/src/androidTest/java/androidx/camera/app/javacameraxpermissions/res/layout/activity_main.xml
new file mode 100644
index 0000000..dced21a
--- /dev/null
+++ b/camera/core/src/androidTest/java/androidx/camera/app/javacameraxpermissions/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/core/src/androidTest/java/androidx/camera/app/javacameraxpermissions/res/values/strings.xml b/camera/core/src/androidTest/java/androidx/camera/app/javacameraxpermissions/res/values/strings.xml
new file mode 100644
index 0000000..c954827
--- /dev/null
+++ b/camera/core/src/androidTest/java/androidx/camera/app/javacameraxpermissions/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/core/src/androidTest/java/androidx/camera/app/timingapp/AndroidManifest.xml b/camera/core/src/androidTest/java/androidx/camera/app/timingapp/AndroidManifest.xml
new file mode 100644
index 0000000..3d112dd
--- /dev/null
+++ b/camera/core/src/androidTest/java/androidx/camera/app/timingapp/AndroidManifest.xml
@@ -0,0 +1,35 @@
+<?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.app.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="androidx.camera.app.timingapp.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/core/src/androidTest/java/androidx/camera/app/timingapp/BaseActivity.java b/camera/core/src/androidTest/java/androidx/camera/app/timingapp/BaseActivity.java
new file mode 100644
index 0000000..8b0084c
--- /dev/null
+++ b/camera/core/src/androidTest/java/androidx/camera/app/timingapp/BaseActivity.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.app.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 {
+ 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 static final long MICROS_IN_SECOND = TimeUnit.SECONDS.toMillis(1);
+ public static final long PREVIEW_FILL_BUFFER_TIME = 1500;
+
+ 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/core/src/androidTest/java/androidx/camera/app/timingapp/CustomLifecycle.java b/camera/core/src/androidTest/java/androidx/camera/app/timingapp/CustomLifecycle.java
new file mode 100644
index 0000000..9ed751d
--- /dev/null
+++ b/camera/core/src/androidTest/java/androidx/camera/app/timingapp/CustomLifecycle.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.app.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/core/src/androidTest/java/androidx/camera/app/timingapp/TakePhotoActivity.java b/camera/core/src/androidTest/java/androidx/camera/app/timingapp/TakePhotoActivity.java
new file mode 100644
index 0000000..3b00d13
--- /dev/null
+++ b/camera/core/src/androidTest/java/androidx/camera/app/timingapp/TakePhotoActivity.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.app.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";
+
+ /** The default cameraId to use. */
+ private LensFacing currentCameraLensFacing = LensFacing.BACK;
+
+ private final String defaultCameraFacing = CAMERA_FACING_BACK;
+
+ // 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 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);
+ }
+ }
+
+ 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.");
+ }
+ };
+}
diff --git a/camera/core/src/androidTest/java/androidx/camera/app/timingapp/build.gradle b/camera/core/src/androidTest/java/androidx/camera/app/timingapp/build.gradle
new file mode 100644
index 0000000..412e3ec
--- /dev/null
+++ b/camera/core/src/androidTest/java/androidx/camera/app/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.app.timingapp"
+ minSdkVersion project.ext.minSdk
+ targetSdkVersion project.ext.targetSdk
+ versionCode 1
+ versionName project.ext.version
+ multiDexEnabled true
+ }
+
+ sourceSets {
+ main.manifest.srcFile 'AndroidManifest.xml'
+ main.java.srcDirs = ['.']
+ main.java.excludes = ['**/build/**']
+ main.java.includes = ['*.java']
+ main.res.srcDirs = ['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/core/src/androidTest/java/androidx/camera/app/timingapp/res/layout/activity_main.xml b/camera/core/src/androidTest/java/androidx/camera/app/timingapp/res/layout/activity_main.xml
new file mode 100644
index 0000000..193f84c
--- /dev/null
+++ b/camera/core/src/androidTest/java/androidx/camera/app/timingapp/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_constraintTop_toTopOf="parent"
+ app:layout_constraintStart_toStartOf="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:text="Picture"
+ app:layout_constraintBottom_toBottomOf="parent"
+ android:scaleType="fitXY"
+ 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/core/src/androidTest/java/androidx/camera/app/timingapp/res/values/strings.xml b/camera/core/src/androidTest/java/androidx/camera/app/timingapp/res/values/strings.xml
new file mode 100644
index 0000000..2df0565
--- /dev/null
+++ b/camera/core/src/androidTest/java/androidx/camera/app/timingapp/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/core/src/androidTest/java/androidx/camera/app/timingapp/res/values/style.xml b/camera/core/src/androidTest/java/androidx/camera/app/timingapp/res/values/style.xml
new file mode 100644
index 0000000..0c1afa6
--- /dev/null
+++ b/camera/core/src/androidTest/java/androidx/camera/app/timingapp/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/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..5b7f18c
--- /dev/null
+++ b/camera/core/src/androidTest/java/androidx/camera/core/AndroidImageProxyAndroidTest.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.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 java.nio.ByteBuffer;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@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..b46451e
--- /dev/null
+++ b/camera/core/src/androidTest/java/androidx/camera/core/AndroidImageReaderProxyAndroidTest.java
@@ -0,0 +1,121 @@
+/*
+ * 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..b0e3f24
--- /dev/null
+++ b/camera/core/src/androidTest/java/androidx/camera/core/BaseUseCaseAndroidTest.java
@@ -0,0 +1,200 @@
+/*
+ * 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 java.util.Map;
+import java.util.Set;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mockito;
+
+@RunWith(AndroidJUnit4.class)
+public class BaseUseCaseAndroidTest {
+ 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;
+ }
+ }
+
+ 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);
+ }
+}
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..128a51d
--- /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..d456c68
--- /dev/null
+++ b/camera/core/src/androidTest/java/androidx/camera/core/CameraCaptureFailureAndroidTest.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 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..0b18d98
--- /dev/null
+++ b/camera/core/src/androidTest/java/androidx/camera/core/CameraCaptureSessionStateCallbacksAndroidTest.java
@@ -0,0 +1,76 @@
+/*
+ * 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..b26f7c6
--- /dev/null
+++ b/camera/core/src/androidTest/java/androidx/camera/core/CameraDeviceStateCallbacksAndroidTest.java
@@ -0,0 +1,56 @@
+/*
+ * 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..58aa22f
--- /dev/null
+++ b/camera/core/src/androidTest/java/androidx/camera/core/CameraRepositoryAndroidTest.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 static org.junit.Assert.assertThrows;
+
+import androidx.camera.testing.fakes.FakeCameraFactory;
+import androidx.test.runner.AndroidJUnit4;
+import java.util.Set;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@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..b101ca6e
--- /dev/null
+++ b/camera/core/src/androidTest/java/androidx/camera/core/CameraUtil.java
@@ -0,0 +1,145 @@
+/*
+ * 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..0d4b9a4
--- /dev/null
+++ b/camera/core/src/androidTest/java/androidx/camera/core/CameraXAndroidTest.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 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.FakeDefaultUseCaseConfigFactory;
+import androidx.camera.testing.fakes.FakeLifecycleOwner;
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.runner.AndroidJUnit4;
+import java.util.Map;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mockito;
+
+@RunWith(AndroidJUnit4.class)
+public final class CameraXAndroidTest {
+ private FakeLifecycleOwner lifecycle;
+ String cameraId;
+ BaseCamera camera;
+ static CameraFactory cameraFactory = new FakeCameraFactory();
+
+ 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();
+ }
+ }
+
+ private CountingErrorListener errorListener;
+ private CountDownLatch latch;
+ private HandlerThread handlerThread;
+ private Handler handler;
+
+ @Before
+ public void setUp() {
+ Context context = ApplicationProvider.getApplicationContext();
+ CameraDeviceSurfaceManager surfaceManager = new FakeCameraDeviceSurfaceManager();
+ UseCaseConfigurationFactory defaultConfigFactory = new FakeDefaultUseCaseConfigFactory();
+ 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();
+ }
+
+ /** 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;
+ }
+ }
+
+ 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);
+ }
+ }
+}
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..b80648b
--- /dev/null
+++ b/camera/core/src/androidTest/java/androidx/camera/core/CaptureRequestConfigurationAndroidTest.java
@@ -0,0 +1,131 @@
+/*
+ * 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 java.util.List;
+import java.util.Map;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mockito;
+
+@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..ea0bd15
--- /dev/null
+++ b/camera/core/src/androidTest/java/androidx/camera/core/CaptureRequestParameterAndroidTest.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.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..a0adb4e
--- /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 java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+public class ErrorHandlerAndroidTest {
+ private ErrorHandler errorHandler;
+
+ 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();
+ }
+ }
+
+ 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);
+ }
+}
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..54658e1
--- /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..98d4afc
--- /dev/null
+++ b/camera/core/src/androidTest/java/androidx/camera/core/FakeOtherUseCase.java
@@ -0,0 +1,63 @@
+/*
+ * 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..83624df
--- /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..2e0dd65
--- /dev/null
+++ b/camera/core/src/androidTest/java/androidx/camera/core/FakeUseCase.java
@@ -0,0 +1,63 @@
+/*
+ * 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.support.annotation.RestrictTo;
+import android.support.annotation.RestrictTo.Scope;
+import android.util.Size;
+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..f8e77e8
--- /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..27eb93c
--- /dev/null
+++ b/camera/core/src/androidTest/java/androidx/camera/core/ForwardingImageProxyAndroidTest.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 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 java.util.concurrent.Semaphore;
+import java.util.concurrent.atomic.AtomicReference;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@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..d186b0b
--- /dev/null
+++ b/camera/core/src/androidTest/java/androidx/camera/core/ForwardingImageReaderListenerAndroidTest.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 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 java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.Semaphore;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@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;
+
+ @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 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 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);
+ }
+
+ 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();
+ };
+ }
+}
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..66b6323
--- /dev/null
+++ b/camera/core/src/androidTest/java/androidx/camera/core/ImageProxyDownsamplerAndroidTest.java
@@ -0,0 +1,218 @@
+/*
+ * 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 java.nio.ByteBuffer;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+public final class ImageProxyDownsamplerAndroidTest {
+ private static final int WIDTH = 8;
+ private static final int HEIGHT = 8;
+
+ @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);
+ }
+
+ 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);
+ }
+ }
+ }
+ }
+}
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..2b986b4
--- /dev/null
+++ b/camera/core/src/androidTest/java/androidx/camera/core/ImageSaverAndroidTest.java
@@ -0,0 +1,267 @@
+/*
+ * 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.support.annotation.Nullable;
+import android.util.Base64;
+import android.util.Rational;
+import androidx.camera.core.ImageSaver.OnImageSavedListener;
+import androidx.camera.core.ImageSaver.SaveError;
+import androidx.test.runner.AndroidJUnit4;
+import java.io.File;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.util.concurrent.Semaphore;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+
+@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..3a2248a
--- /dev/null
+++ b/camera/core/src/androidTest/java/androidx/camera/core/ImmediateSurfaceAndroidTest.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 static com.google.common.truth.Truth.assertThat;
+
+import android.view.Surface;
+import androidx.test.runner.AndroidJUnit4;
+import com.google.common.util.concurrent.ListenableFuture;
+import java.util.concurrent.ExecutionException;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mockito;
+
+@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..2b1b953
--- /dev/null
+++ b/camera/core/src/androidTest/java/androidx/camera/core/IoExecutorAndroidTest.java
@@ -0,0 +1,145 @@
+/*
+ * 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.support.annotation.GuardedBy;
+import androidx.test.runner.AndroidJUnit4;
+import java.util.concurrent.Executor;
+import java.util.concurrent.locks.Condition;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReentrantLock;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+public final class IoExecutorAndroidTest {
+
+ private Executor ioExecutor;
+
+ private enum RunnableState {
+ CLEAR,
+ RUNNABLE1_WAITING,
+ RUNNABLE1_FINISHED,
+ RUNNABLE2_FINISHED
+ }
+
+ 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.
+ }
+}
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..d5c3b5e
--- /dev/null
+++ b/camera/core/src/androidTest/java/androidx/camera/core/QueuedImageReaderProxyAndroidTest.java
@@ -0,0 +1,306 @@
+/*
+ * 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 java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.Semaphore;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@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;
+
+ @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 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 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 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());
+ }
+
+ 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..94f75f7
--- /dev/null
+++ b/camera/core/src/androidTest/java/androidx/camera/core/ReferenceCountedImageProxyAndroidTest.java
@@ -0,0 +1,156 @@
+/*
+ * 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 java.nio.ByteBuffer;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@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..77aa84f
--- /dev/null
+++ b/camera/core/src/androidTest/java/androidx/camera/core/SessionConfigurationAndroidTest.java
@@ -0,0 +1,285 @@
+/*
+ * 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 java.util.List;
+import java.util.Map;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mockito;
+
+@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..2519b8b
--- /dev/null
+++ b/camera/core/src/androidTest/java/androidx/camera/core/SingleCloseImageProxyAndroidTest.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 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..07862a1
--- /dev/null
+++ b/camera/core/src/androidTest/java/androidx/camera/core/StreamConfigurationMapUtil.java
@@ -0,0 +1,211 @@
+/*
+ * 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..6fad240
--- /dev/null
+++ b/camera/core/src/androidTest/java/androidx/camera/core/UseCaseAttachStateAndroidTest.java
@@ -0,0 +1,335 @@
+/*
+ * 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 java.util.HashMap;
+import java.util.Map;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mockito;
+
+@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;
+
+ 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;
+ }
+ }
+
+ @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));
+ }
+}
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..2a7d81c
--- /dev/null
+++ b/camera/core/src/androidTest/java/androidx/camera/core/UseCaseGroupAndroidTest.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 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 FakeUseCaseConfiguration fakeUseCaseConfiguration;
+ private FakeOtherUseCaseConfiguration fakeOtherUseCaseConfiguration;
+ private UseCaseGroup useCaseGroup;
+ private FakeUseCase fakeUseCase;
+ private FakeOtherUseCase fakeOtherUseCase;
+ private final UseCaseGroup.StateChangeListener mockListener =
+ Mockito.mock(UseCaseGroup.StateChangeListener.class);
+
+ @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..8feebac
--- /dev/null
+++ b/camera/core/src/androidTest/java/androidx/camera/core/UseCaseGroupLifecycleControllerAndroidTest.java
@@ -0,0 +1,96 @@
+/*
+ * 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 UseCaseGroupLifecycleController useCaseGroupLifecycleController;
+ private FakeLifecycleOwner lifecycleOwner;
+ private final UseCaseGroup.StateChangeListener mockListener =
+ Mockito.mock(UseCaseGroup.StateChangeListener.class);
+
+ @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..59e88df
--- /dev/null
+++ b/camera/core/src/androidTest/java/androidx/camera/core/UseCaseGroupRepositoryAndroidTest.java
@@ -0,0 +1,112 @@
+/*
+ * 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 android.arch.lifecycle.LifecycleOwner;
+import androidx.camera.testing.fakes.FakeLifecycleOwner;
+import androidx.test.runner.AndroidJUnit4;
+import java.util.Map;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@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));
+ }
+}