TestingCamera2: Add new functionalities

- Ability to select between all available cameras
- Ability to stream physical streams for logical camera.

Test: Run TestingCamera2 and select between cameras
Bug: 76013895
Change-Id: I83ea3f3e118d2471ac4fb97b74968c13425022ff
diff --git a/apps/TestingCamera2/res/layout/main.xml b/apps/TestingCamera2/res/layout/main.xml
index ad12d43..488265d 100644
--- a/apps/TestingCamera2/res/layout/main.xml
+++ b/apps/TestingCamera2/res/layout/main.xml
@@ -39,6 +39,11 @@
         android:layout_width="240dp"
         android:layout_height="180dp" />
 
+    <SurfaceView
+        android:id="@+id/preview_view2"
+        android:layout_width="240dp"
+        android:layout_height="180dp" />
+
   </GridLayout>
 
   <ScrollView
@@ -54,6 +59,14 @@
         android:layout_marginLeft="5dp"
         android:orientation="vertical" >
 
+        <Spinner
+            android:id="@+id/camera_id_spinner"
+            android:layout_width="fill_parent"
+            android:layout_height="wrap_content"
+            android:spinnerMode="dropdown"
+            android:prompt="@string/camera_id_spinner_prompt"
+            />
+
         <Button
             android:id="@+id/info_button"
             android:text="@string/info_button_label"
diff --git a/apps/TestingCamera2/res/values/strings.xml b/apps/TestingCamera2/res/values/strings.xml
index 5f749aa..c3bc711 100644
--- a/apps/TestingCamera2/res/values/strings.xml
+++ b/apps/TestingCamera2/res/values/strings.xml
@@ -86,6 +86,7 @@
     <!-- V1 strings -->
 
     <string name="v1_app_name">TestingCam2</string>
+    <string name="camera_id_spinner_prompt">Camera Id</string>
     <string name="info_button_label">JPEG!</string>
     <string name="flush_button_label">Flush device</string>
     <string name="focus_lock_button_label">Focus Lock</string>
diff --git a/apps/TestingCamera2/src/com/android/testingcamera2/v1/CameraOps.java b/apps/TestingCamera2/src/com/android/testingcamera2/v1/CameraOps.java
index aae07e9..753fa00 100644
--- a/apps/TestingCamera2/src/com/android/testingcamera2/v1/CameraOps.java
+++ b/apps/TestingCamera2/src/com/android/testingcamera2/v1/CameraOps.java
@@ -26,6 +26,7 @@
 import android.hardware.camera2.CameraCharacteristics;
 import android.hardware.camera2.CaptureRequest;
 import android.hardware.camera2.CaptureRequest.Builder;
+import android.hardware.camera2.params.OutputConfiguration;
 import android.util.Size;
 import android.media.Image;
 import android.media.ImageReader;
@@ -45,6 +46,7 @@
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
+import java.util.Set;
 
 /**
  * A camera controller class that runs in its own thread, to
@@ -67,6 +69,7 @@
     private final BlockingStateCallback mDeviceListener =
             new BlockingStateCallback();
 
+    private String mCameraId;
     private CameraDevice mCamera;
     private CameraCaptureSession mSession;
 
@@ -80,6 +83,7 @@
     private CaptureRequest.Builder mRecordingRequestBuilder;
     List<Surface> mOutputSurfaces = new ArrayList<Surface>(2);
     private Surface mPreviewSurface;
+    private Surface mPreviewSurface2;
     // How many JPEG buffers do we want to hold on to at once
     private static final int MAX_CONCURRENT_JPEGS = 2;
 
@@ -103,6 +107,11 @@
     private final Listener mListener;
     private final Handler mListenerHandler;
 
+    // Physical camera id of the current logical multi-camera. "" if this is not a logical
+    // multi-camera.
+    private String mPhysicalCameraId1;
+    private String mPhysicalCameraId2;
+
     private void checkOk() {
         if (mStatus < STATUS_OK) {
             throw new IllegalStateException(String.format("Device not OK: %d", mStatus ));
@@ -173,18 +182,36 @@
     }
 
     private void minimalOpenCamera() throws ApiFailureException {
-        if (mCamera == null) {
-            final String[] devices;
-            final CameraCharacteristics characteristics;
+        // Open camera if not yet opened, or the currently opened camera is not the right one.
+        if (mCamera == null || !mCameraId.equals(mCamera.getId())) {
+            closeDevice();
 
+            mPhysicalCameraId1 = "";
+            mPhysicalCameraId2 = "";
+            final CameraCharacteristics characteristics;
             try {
-                devices = mCameraManager.getCameraIdList();
-                if (devices == null || devices.length == 0) {
-                    throw new ApiFailureException("no devices");
+                CameraCharacteristics c = mCameraManager.getCameraCharacteristics(mCameraId);
+                int[] caps = c.get(CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES);
+                for (int cap : caps) {
+                    if (CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_LOGICAL_MULTI_CAMERA
+                            != cap) {
+                        continue;
+                    }
+
+                    Set<String> physicalIds = c.getPhysicalCameraIds();
+                    if (physicalIds.size() != 2) {
+                        throw new ApiFailureException(
+                                "3 or more physical cameras are not yet supported");
+                    }
+                    String[] physicalIdsArray = physicalIds.toArray(new String[2]);
+                    mPhysicalCameraId1 = physicalIdsArray[0];
+                    mPhysicalCameraId2 = physicalIdsArray[1];
+                    break;
                 }
-                mCamera = mBlockingCameraManager.openCamera(devices[0],
+                Log.i(TAG, "Opening " + mCameraId);
+                mCamera = mBlockingCameraManager.openCamera(mCameraId,
                         mDeviceListener, mOpsHandler);
-                mCameraCharacteristics = mCameraManager.getCameraCharacteristics(mCamera.getId());
+                mCameraCharacteristics = mCameraManager.getCameraCharacteristics(mCameraId);
                 characteristics = mCameraCharacteristics;
             } catch (CameraAccessException e) {
                 throw new ApiFailureException("open failure", e);
@@ -197,7 +224,7 @@
                 mListenerHandler.post(new Runnable() {
                     @Override
                     public void run() {
-                        mListener.onCameraOpened(devices[0], characteristics);
+                        mListener.onCameraOpened(mCameraId, characteristics);
                     }
                 });
             }
@@ -212,11 +239,20 @@
         mSession = sessionListener.waitAndGetSession(IDLE_WAIT_MS);
     }
 
+    private void configureOutputsByConfigs(List<OutputConfiguration> outputConfigs)
+            throws CameraAccessException {
+        BlockingSessionCallback sessionListener = new BlockingSessionCallback();
+        mCamera.createCaptureSessionByOutputConfigurations(outputConfigs, sessionListener, mOpsHandler);
+        mSession = sessionListener.waitAndGetSession(IDLE_WAIT_MS);
+    }
+
     /**
      * Set up SurfaceView dimensions for camera preview
      */
-    public void minimalPreviewConfig(SurfaceHolder previewHolder) throws ApiFailureException {
+    public void minimalPreviewConfig(String cameraId, SurfaceHolder previewHolder,
+            SurfaceHolder previewHolder2) throws ApiFailureException {
 
+        mCameraId = cameraId;
         minimalOpenCamera();
         try {
             CameraCharacteristics properties =
@@ -236,7 +272,9 @@
             }
             Log.i(TAG, "Set preview size to " + sz.toString());
             previewHolder.setFixedSize(sz.getWidth(), sz.getHeight());
+            previewHolder2.setFixedSize(sz.getWidth(), sz.getHeight());
             mPreviewSurface = previewHolder.getSurface();
+            mPreviewSurface2 = previewHolder2.getSurface();
         }  catch (CameraAccessException e) {
             throw new ApiFailureException("Error setting up minimal preview", e);
         }
@@ -329,24 +367,40 @@
     /**
      * Configure streams and run minimal preview
      */
-    public void minimalPreview(SurfaceHolder previewHolder, CameraControls camCtl)
-            throws ApiFailureException {
+    public void minimalPreview(SurfaceHolder previewHolder, SurfaceHolder previewHolder2,
+            CameraControls camCtl) throws ApiFailureException {
 
         minimalOpenCamera();
 
-        if (mPreviewSurface == null) {
+        if (mPreviewSurface == null || mPreviewSurface2 == null) {
             throw new ApiFailureException("Preview surface is not created");
         }
         try {
-            List<Surface> outputSurfaces = new ArrayList<Surface>(/*capacity*/1);
-            outputSurfaces.add(mPreviewSurface);
+            List<OutputConfiguration> outputConfigs =
+                    new ArrayList<OutputConfiguration>(/*capacity*/2);
+            boolean isLogicalCamera =
+                    !mPhysicalCameraId1.equals("") && !mPhysicalCameraId2.equals("");
+            if (isLogicalCamera) {
+                OutputConfiguration config1 = new OutputConfiguration(previewHolder.getSurface());
+                config1.setPhysicalCameraId(mPhysicalCameraId1);
+                outputConfigs.add(config1);
 
-            configureOutputs(outputSurfaces);
+                OutputConfiguration config2 = new OutputConfiguration(previewHolder2.getSurface());
+                config2.setPhysicalCameraId(mPhysicalCameraId2);
+                outputConfigs.add(config2);
+            } else {
+                OutputConfiguration config = new OutputConfiguration(previewHolder.getSurface());
+                outputConfigs.add(config);
+            }
+            configureOutputsByConfigs(outputConfigs);
 
             mPreviewRequestBuilder = mCamera.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);
             updateCaptureRequest(mPreviewRequestBuilder, camCtl);
 
             mPreviewRequestBuilder.addTarget(mPreviewSurface);
+            if (isLogicalCamera) {
+                mPreviewRequestBuilder.addTarget(mPreviewSurface2);
+            }
 
             mSession.setRepeatingRequest(mPreviewRequestBuilder.build(), null, null);
         } catch (CameraAccessException e) {
diff --git a/apps/TestingCamera2/src/com/android/testingcamera2/v1/TestingCamera2.java b/apps/TestingCamera2/src/com/android/testingcamera2/v1/TestingCamera2.java
index 9bd3113..7a4a9b5 100644
--- a/apps/TestingCamera2/src/com/android/testingcamera2/v1/TestingCamera2.java
+++ b/apps/TestingCamera2/src/com/android/testingcamera2/v1/TestingCamera2.java
@@ -71,6 +71,8 @@
     private static final String TAG = "TestingCamera2";
     private static final boolean VERBOSE = Log.isLoggable(TAG, Log.VERBOSE);
     private CameraOps mCameraOps;
+    private String[] mAvailableCameraIds;
+    private String mCameraId;
     private static final int mSeekBarMax = 100;
     private static final long MAX_EXPOSURE = 200000000L; // 200ms
     private static final long MIN_EXPOSURE = 100000L; // 100us
@@ -85,10 +87,14 @@
     private int mLastOrientation = ORIENTATION_UNINITIALIZED;
     private OrientationEventListener mOrientationEventListener;
     private SurfaceView mPreviewView;
+    private SurfaceView mPreviewView2;
     private ImageView mStillView;
 
     private SurfaceHolder mCurrentPreviewHolder = null;
+    private SurfaceHolder mCurrentPreviewHolder2 = null;
 
+    private Spinner mCameraIdSpinner;
+    private OnItemSelectedListener mCameraIdSelectionListener;
     private Button mInfoButton;
     private Button mFlushButton;
     private ToggleButton mFocusLockToggle;
@@ -127,8 +133,12 @@
         mPreviewView = (SurfaceView) findViewById(R.id.preview_view);
         mPreviewView.getHolder().addCallback(this);
 
+        mPreviewView2 = (SurfaceView) findViewById(R.id.preview_view2);
+        mPreviewView2.getHolder().addCallback(this);
+
         mStillView = (ImageView) findViewById(R.id.still_view);
 
+        mCameraIdSpinner = (Spinner) findViewById(R.id.camera_id_spinner);
         mInfoButton  = (Button) findViewById(R.id.info_button);
         mInfoButton.setOnClickListener(mInfoButtonListener);
         mFlushButton  = (Button) findViewById(R.id.flush_button);
@@ -180,6 +190,25 @@
             logException("Cannot create camera ops!",e);
         }
 
+        // Map available camera Ids to camera id spinner dropdown list of strings
+        try {
+            mAvailableCameraIds = mCameraOps.getDevices();
+            if (mAvailableCameraIds == null || mAvailableCameraIds.length == 0) {
+                throw new ApiFailureException("no devices");
+            }
+        } catch(ApiFailureException e) {
+            logException("CameraOps::getDevices failed!",e);
+        }
+        mCameraId = mAvailableCameraIds[0];
+        ArrayAdapter<String> cameraIdDataAdapter = new ArrayAdapter<>(TestingCamera2.this,
+                android.R.layout.simple_spinner_item, mAvailableCameraIds);
+        mCameraIdSpinner.setAdapter(cameraIdDataAdapter);
+
+        /*
+         * Change the camera ID and update preview when camera ID spinner's selected item changes
+         */
+        mCameraIdSpinner.setOnItemSelectedListener(mCameraIdSelectedListener);
+
         mOrientationEventListener = new OrientationEventListener(this) {
             @Override
             public void onOrientationChanged(int orientation) {
@@ -237,8 +266,10 @@
 
     private void setUpPreview() {
         try {
-            mCameraOps.minimalPreviewConfig(mPreviewView.getHolder());
+            mCameraOps.minimalPreviewConfig(mCameraId, mPreviewView.getHolder(),
+                    mPreviewView2.getHolder());
             mCurrentPreviewHolder = mPreviewView.getHolder();
+            mCurrentPreviewHolder2 = mPreviewView2.getHolder();
         } catch (ApiFailureException e) {
             logException("Can't configure preview surface: ",e);
         }
@@ -255,6 +286,7 @@
             logException("Can't close device: ",e);
         }
         mCurrentPreviewHolder = null;
+        mCurrentPreviewHolder2 = null;
     }
 
     @Override
@@ -289,6 +321,7 @@
 
     private void updatePreviewOrientation() {
         LayoutParams params = mPreviewView.getLayoutParams();
+        LayoutParams params2 = mPreviewView2.getLayoutParams();
         int width = params.width;
         int height = params.height;
 
@@ -323,8 +356,11 @@
             }
             params.width = width;
             params.height = height;
-
             mPreviewView.setLayoutParams(params);
+
+            params2.width = width;
+            params2.height = height;
+            mPreviewView2.setLayoutParams(params2);
         }
     }
 
@@ -338,9 +374,11 @@
             Log.v(TAG, String.format("surfaceChanged: format %x, width %d, height %d", format,
                     width, height));
         }
-        if (mCurrentPreviewHolder != null && holder == mCurrentPreviewHolder) {
+        if ((mCurrentPreviewHolder != null && holder == mCurrentPreviewHolder) ||
+                (mCurrentPreviewHolder2 != null && holder == mCurrentPreviewHolder2)) {
             try {
-                mCameraOps.minimalPreview(holder, mCameraControl);
+                mCameraOps.minimalPreview(mCurrentPreviewHolder, mCurrentPreviewHolder2,
+                        mCameraControl);
             } catch (ApiFailureException e) {
                 logException("Can't start minimal preview: ", e);
             }
@@ -366,8 +404,9 @@
                     try {
                         mCameraOps.minimalJpegCapture(mCaptureCallback, mCaptureResultListener,
                                 uiHandler, mCameraControl);
-                        if (mCurrentPreviewHolder != null) {
-                            mCameraOps.minimalPreview(mCurrentPreviewHolder, mCameraControl);
+                        if (mCurrentPreviewHolder != null && mCurrentPreviewHolder2 != null) {
+                            mCameraOps.minimalPreview(mCurrentPreviewHolder,
+                                    mCurrentPreviewHolder2, mCameraControl);
                         }
                     } catch (ApiFailureException e) {
                         logException("Can't take a JPEG! ", e);
@@ -702,6 +741,36 @@
         }
     };
 
+    private final OnItemSelectedListener mCameraIdSelectedListener =
+            new OnItemSelectedListener() {
+        @Override
+        public void onItemSelected(AdapterView<?> parent, View view, int position,
+                long id) {
+            String cameraId = mAvailableCameraIds[position];
+
+            if (cameraId != mCameraId) {
+                Log.i(TAG, "Switch to camera " + cameraId);
+                try {
+                    mCameraOps.minimalPreviewConfig(cameraId, mPreviewView.getHolder(),
+                            mPreviewView2.getHolder());
+
+                    if (mCurrentPreviewHolder != null && mCurrentPreviewHolder2 != null) {
+                        mCameraOps.minimalPreview(mCurrentPreviewHolder,
+                                mCurrentPreviewHolder2, mCameraControl);
+                    }
+                } catch (ApiFailureException e) {
+                    logException("Can't configure preview surface: ", e);
+                }
+                mCameraId = cameraId;
+            }
+        }
+
+        @Override
+        public void onNothingSelected(AdapterView<?> parent) {
+            // Do nothing
+        }
+    };
+
     private final CameraOps.Listener mCameraOpsListener = new CameraOps.Listener() {
         @Override
         public void onCameraOpened(String cameraId, CameraCharacteristics characteristics) {
@@ -726,7 +795,8 @@
 
             ArrayAdapter<String> dataAdapter = new ArrayAdapter<>(TestingCamera2.this,
                     android.R.layout.simple_spinner_item, afModeList);
-            dataAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
+            dataAdapter.setDropDownViewResource(
+                    android.R.layout.simple_spinner_dropdown_item);
             mFocusModeSpinner.setAdapter(dataAdapter);
 
             /*