Merge changes from topic "b149346795" into rvc-dev

* changes:
  Camera: Verify offline image timestamps
  Camera: Test offline processing along with regular session
  Camera: Extend offline processing testing
diff --git a/tests/camera/AndroidManifest.xml b/tests/camera/AndroidManifest.xml
index 85221d3..efe5700 100644
--- a/tests/camera/AndroidManifest.xml
+++ b/tests/camera/AndroidManifest.xml
@@ -70,6 +70,13 @@
             android:process=":camera2ActivityProcess">
         </activity>
 
+        <activity android:name="android.hardware.camera2.cts.Camera2OfflineTestActivity"
+            android:label="RemoteCamera2OfflineTestActivity"
+            android:screenOrientation="landscape"
+            android:configChanges="keyboardHidden|orientation|screenSize"
+            android:process=":camera2ActivityProcess">
+        </activity>
+
         <activity android:name="android.hardware.multiprocess.camera.cts.MediaRecorderCameraActivity"
             android:label="RemoteMediaRecorderCameraActivity"
             android:screenOrientation="landscape"
diff --git a/tests/camera/src/android/hardware/camera2/cts/Camera2OfflineTestActivity.java b/tests/camera/src/android/hardware/camera2/cts/Camera2OfflineTestActivity.java
new file mode 100644
index 0000000..2483eef
--- /dev/null
+++ b/tests/camera/src/android/hardware/camera2/cts/Camera2OfflineTestActivity.java
@@ -0,0 +1,158 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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 android.hardware.camera2.cts;
+
+import static android.hardware.camera2.cts.CameraTestUtils.OFFLINE_CAMERA_ID;
+
+import android.app.Activity;
+import android.content.Context;
+import android.content.Intent;
+import android.hardware.camera2.CameraAccessException;
+import android.hardware.camera2.CameraDevice;
+import android.hardware.camera2.CameraManager;
+import android.hardware.multiprocess.camera.cts.ErrorLoggingService;
+import android.hardware.multiprocess.camera.cts.TestConstants;
+import android.os.Bundle;
+import android.os.Handler;
+import android.util.Log;
+
+/**
+ * Activity implementing basic access of the Camera2 API.
+ *
+ * <p />
+ * This will log all errors to {@link android.hardware.multiprocess.camera.cts.ErrorLoggingService}.
+ */
+public class Camera2OfflineTestActivity extends Activity {
+    private static final String TAG = "Camera2OfflineTestActivity";
+
+    ErrorLoggingService.ErrorServiceConnection mErrorServiceConnection;
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        Log.i(TAG, "onCreate called.");
+        super.onCreate(savedInstanceState);
+        mErrorServiceConnection = new ErrorLoggingService.ErrorServiceConnection(this);
+        mErrorServiceConnection.start();
+    }
+
+    @Override
+    protected void onPause() {
+        Log.i(TAG, "onPause called.");
+        super.onPause();
+    }
+
+    @Override
+    protected void onResume() {
+        Log.i(TAG, "onResume called.");
+        super.onResume();
+
+        try {
+            CameraManager manager = (CameraManager) getSystemService(Context.CAMERA_SERVICE);
+
+            if (manager == null) {
+                mErrorServiceConnection.logAsync(TestConstants.EVENT_CAMERA_ERROR, TAG +
+                        " could not connect camera service");
+                return;
+            }
+            Intent intent = getIntent();
+            Bundle bundledExtras = intent.getExtras();
+            if (null == bundledExtras) {
+                mErrorServiceConnection.logAsync(TestConstants.EVENT_CAMERA_ERROR, TAG +
+                        " not bundled intent extras");
+                return;
+            }
+
+            String cameraId = bundledExtras.getString(OFFLINE_CAMERA_ID);
+            if (null == cameraId) {
+                mErrorServiceConnection.logAsync(TestConstants.EVENT_CAMERA_ERROR, TAG +
+                        " no camera id present in bundled extra");
+                return;
+            }
+
+            manager.registerAvailabilityCallback(new CameraManager.AvailabilityCallback() {
+                @Override
+                public void onCameraAvailable(String cameraId) {
+                    super.onCameraAvailable(cameraId);
+                    mErrorServiceConnection.logAsync(TestConstants.EVENT_CAMERA_AVAILABLE,
+                            cameraId);
+                    Log.i(TAG, "Camera " + cameraId + " is available");
+                }
+
+                @Override
+                public void onCameraUnavailable(String cameraId) {
+                    super.onCameraUnavailable(cameraId);
+                    mErrorServiceConnection.logAsync(TestConstants.EVENT_CAMERA_UNAVAILABLE,
+                            cameraId);
+                    Log.i(TAG, "Camera " + cameraId + " is unavailable");
+                }
+
+                @Override
+                public void onPhysicalCameraAvailable(String cameraId, String physicalCameraId) {
+                    super.onPhysicalCameraAvailable(cameraId, physicalCameraId);
+                    mErrorServiceConnection.logAsync(TestConstants.EVENT_CAMERA_AVAILABLE,
+                            cameraId + " : " + physicalCameraId);
+                    Log.i(TAG, "Camera " + cameraId + " : " + physicalCameraId + " is available");
+                }
+
+                @Override
+                public void onPhysicalCameraUnavailable(String cameraId, String physicalCameraId) {
+                    super.onPhysicalCameraUnavailable(cameraId, physicalCameraId);
+                    mErrorServiceConnection.logAsync(TestConstants.EVENT_CAMERA_UNAVAILABLE,
+                            cameraId + " : " + physicalCameraId);
+                    Log.i(TAG, "Camera " + cameraId + " : " + physicalCameraId + " is unavailable");
+                }
+            }, null);
+
+            manager.openCamera(cameraId, new CameraDevice.StateCallback() {
+                @Override
+                public void onOpened(CameraDevice cameraDevice) {
+                    mErrorServiceConnection.logAsync(TestConstants.EVENT_CAMERA_CONNECT,
+                            cameraId);
+                    Log.i(TAG, "Camera " + cameraId + " is opened");
+                }
+
+                @Override
+                public void onDisconnected(CameraDevice cameraDevice) {
+                    mErrorServiceConnection.logAsync(TestConstants.EVENT_CAMERA_EVICTED,
+                            cameraId);
+                    Log.i(TAG, "Camera " + cameraId + " is disconnected");
+                }
+
+                @Override
+                public void onError(CameraDevice cameraDevice, int i) {
+                    mErrorServiceConnection.logAsync(TestConstants.EVENT_CAMERA_ERROR, TAG +
+                            " Camera " + cameraId + " experienced error " + i);
+                    Log.e(TAG, "Camera " + cameraId + " onError called with error " + i);
+                }
+            }, null);
+        } catch (CameraAccessException e) {
+            mErrorServiceConnection.logAsync(TestConstants.EVENT_CAMERA_ERROR, TAG +
+                    " camera exception during connection: " + e);
+            Log.e(TAG, "Access exception: " + e);
+        }
+    }
+
+    @Override
+    protected void onDestroy() {
+        Log.i(TAG, "onDestroy called.");
+        super.onDestroy();
+        if (mErrorServiceConnection != null) {
+            mErrorServiceConnection.stop();
+            mErrorServiceConnection = null;
+        }
+    }
+}
diff --git a/tests/camera/src/android/hardware/camera2/cts/OfflineSessionTest.java b/tests/camera/src/android/hardware/camera2/cts/OfflineSessionTest.java
index a897944..e504803 100644
--- a/tests/camera/src/android/hardware/camera2/cts/OfflineSessionTest.java
+++ b/tests/camera/src/android/hardware/camera2/cts/OfflineSessionTest.java
@@ -24,8 +24,11 @@
 import com.android.ex.camera2.blocking.BlockingSessionCallback;
 import com.android.ex.camera2.blocking.BlockingOfflineSessionCallback;
 
+import android.app.Activity;
+import android.app.ActivityManager;
+import android.content.Context;
+import android.content.Intent;
 import android.graphics.ImageFormat;
-import android.hardware.Camera;
 import android.hardware.camera2.CameraCaptureSession;
 import android.hardware.camera2.CameraDevice;
 import android.hardware.camera2.CaptureFailure;
@@ -34,7 +37,13 @@
 import android.hardware.camera2.CaptureRequest;
 import android.hardware.camera2.CaptureResult;
 import android.hardware.camera2.TotalCaptureResult;
+import android.hardware.camera2.params.InputConfiguration;
 import android.hardware.camera2.params.OutputConfiguration;
+import android.hardware.multiprocess.camera.cts.ErrorLoggingService;
+import android.hardware.multiprocess.camera.cts.TestConstants;
+import android.media.Image;
+import android.media.ImageReader;
+import android.os.Bundle;
 import android.util.Log;
 import android.util.Size;
 import android.view.Surface;
@@ -46,6 +55,7 @@
 import org.junit.Test;
 
 import java.lang.IllegalArgumentException;
+import java.util.concurrent.TimeoutException;
 import java.util.ArrayList;
 import java.util.List;
 
@@ -54,9 +64,30 @@
     private static final String TAG = "OfflineSessionTest";
     private static final boolean VERBOSE = Log.isLoggable(TAG, Log.VERBOSE);
     private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
+    private final String REMOTE_PROCESS_NAME = "camera2ActivityProcess";
+    private final java.lang.Class<?> REMOTE_PROCESS_CLASS = Camera2OfflineTestActivity.class;
 
+    private static final Size MANDATORY_STREAM_BOUND = new Size(1920, 1080);
     private static final int WAIT_FOR_FRAMES_TIMEOUT_MS = 3000;
     private static final int WAIT_FOR_STATE_TIMEOUT_MS = 5000;
+    private static final int WAIT_FOR_REMOTE_ACTIVITY_LAUNCH_MS = 2000;
+    private static final int WAIT_FOR_REMOTE_ACTIVITY_DESTROY_MS = 2000;
+
+    private enum OfflineTestSequence {
+        /** Regular offline switch without extra calls */
+        NoExtraSteps,
+
+        /** Offline switch followed by immediate offline session close */
+        CloseOfflineSession,
+
+        /** Offline switch followed in parallel with immediate device close and same device open
+         *  in a separate activity
+         */
+        CloseDeviceAndOpenRemote,
+
+        /** Offline session running in parallel with reinitialized regular capture session */
+        InitializeRegularSession,
+    }
 
     /**
      * Test offline switch behavior in case of invalid/bad input.
@@ -171,8 +202,8 @@
                 }
 
                 openDevice(mCameraIdsUnderTest[i]);
-                camera2OfflineSessionTest(mOrderedStillSizes.get(0), ImageFormat.JPEG,
-                        false /*closeDevice*/, false /*closeSession*/);
+                camera2OfflineSessionTest(mCameraIdsUnderTest[i], mOrderedStillSizes.get(0),
+                        ImageFormat.JPEG, OfflineTestSequence.NoExtraSteps);
             } finally {
                 closeDevice();
             }
@@ -213,8 +244,8 @@
                         mCameraIdsUnderTest[i], mCameraManager, ImageFormat.DEPTH_JPEG,
                         null /*bound*/);
                 openDevice(mCameraIdsUnderTest[i]);
-                camera2OfflineSessionTest(depthJpegSizes.get(0), ImageFormat.DEPTH_JPEG,
-                        false /*closeDevice*/, false /*closeSession*/);
+                camera2OfflineSessionTest(mCameraIdsUnderTest[i], depthJpegSizes.get(0),
+                        ImageFormat.DEPTH_JPEG, OfflineTestSequence.NoExtraSteps);
             } finally {
                 closeDevice();
             }
@@ -254,8 +285,8 @@
                 List<Size> heicSizes = CameraTestUtils.getSupportedHeicSizes(
                         mCameraIdsUnderTest[i], mCameraManager, null /*bound*/);
                 openDevice(mCameraIdsUnderTest[i]);
-                camera2OfflineSessionTest(heicSizes.get(0), ImageFormat.HEIC,
-                        false /*closeDevice*/, false /*closeSession*/);
+                camera2OfflineSessionTest(mCameraIdsUnderTest[i], heicSizes.get(0),
+                        ImageFormat.HEIC, OfflineTestSequence.NoExtraSteps);
             } finally {
                 closeDevice();
             }
@@ -263,13 +294,17 @@
     }
 
     /**
-     * Test camera offline session behavior during inactive camera device.
+     * Test camera offline session behavior after close and reopen.
      *
-     * <p> Verify that closing the initial camera device does not impact the
-     * offline camera session.</p>
+     * <p> Verify that closing the initial camera device and opening the same
+     * sensor during offline processing does not have any unexpected side effects.</p>
      */
     @Test
-    public void testDeviceClose() throws Exception {
+    public void testDeviceCloseAndOpen() throws Exception {
+        ErrorLoggingService.ErrorServiceConnection errorConnection =
+                new ErrorLoggingService.ErrorServiceConnection(mContext);
+
+        errorConnection.start();
         for (int i = 0; i < mCameraIdsUnderTest.length; i++) {
             try {
                 Log.i(TAG, "Testing camera2 API for camera device " + mCameraIdsUnderTest[i]);
@@ -287,12 +322,26 @@
                 }
 
                 openDevice(mCameraIdsUnderTest[i]);
-                camera2OfflineSessionTest(mOrderedStillSizes.get(0), ImageFormat.JPEG,
-                        true /*closeDevice*/, false /*closeSession*/);
+                camera2OfflineSessionTest(mCameraIdsUnderTest[i], mOrderedStillSizes.get(0),
+                        ImageFormat.JPEG, OfflineTestSequence.CloseDeviceAndOpenRemote);
+
+                // Verify that the remote camera was opened correctly
+                List<ErrorLoggingService.LogEvent> allEvents = null;
+                try {
+                    allEvents = errorConnection.getLog(WAIT_FOR_STATE_TIMEOUT_MS,
+                            TestConstants.EVENT_CAMERA_CONNECT);
+                } catch (TimeoutException e) {
+                    fail("Timed out waiting on remote offline process error log!");
+                }
+                assertNotNull("Failed to connect to camera device in remote offline process!",
+                        allEvents);
             } finally {
                 closeDevice();
+
             }
         }
+
+        errorConnection.stop();
     }
 
     /**
@@ -320,8 +369,41 @@
                 }
 
                 openDevice(mCameraIdsUnderTest[i]);
-                camera2OfflineSessionTest(mOrderedStillSizes.get(0), ImageFormat.JPEG,
-                        false /*closeDevice*/, true /*closeSession*/);
+                camera2OfflineSessionTest(mCameraIdsUnderTest[i], mOrderedStillSizes.get(0),
+                        ImageFormat.JPEG, OfflineTestSequence.CloseOfflineSession);
+            } finally {
+                closeDevice();
+            }
+        }
+    }
+
+    /**
+     * Test camera offline session in case of new capture session
+     *
+     * <p>Verify that clients are able to initialize a new regular capture session
+     * in parallel with the offline session.</p>
+     */
+    @Test
+    public void testOfflineSessionWithRegularSession() throws Exception {
+        for (int i = 0; i < mCameraIdsUnderTest.length; i++) {
+            try {
+                Log.i(TAG, "Testing camera2 API for camera device " + mCameraIdsUnderTest[i]);
+
+                if (!mAllStaticInfo.get(mCameraIdsUnderTest[i]).isColorOutputSupported()) {
+                    Log.i(TAG, "Camera " + mCameraIdsUnderTest[i] +
+                            " does not support color outputs, skipping");
+                    continue;
+                }
+
+                if (!mAllStaticInfo.get(mCameraIdsUnderTest[i]).isOfflineProcessingSupported()) {
+                    Log.i(TAG, "Camera " + mCameraIdsUnderTest[i] +
+                            " does not support offline processing, skipping");
+                    continue;
+                }
+
+                openDevice(mCameraIdsUnderTest[i]);
+                camera2OfflineSessionTest(mCameraIdsUnderTest[i], mOrderedStillSizes.get(0),
+                        ImageFormat.JPEG, OfflineTestSequence.InitializeRegularSession);
             } finally {
                 closeDevice();
             }
@@ -363,35 +445,38 @@
         }
     }
 
-    // Find the last frame number received in results and failures.
-    private long findLastFrameNumber(SimpleCaptureCallback captureListener) {
-        long lastFrameNumber = -1;
-        while (captureListener.hasMoreResults()) {
-            TotalCaptureResult result = captureListener.getTotalCaptureResult(0 /*timeout*/);
-            if (lastFrameNumber < result.getFrameNumber()) {
-                lastFrameNumber = result.getFrameNumber();
+    private void verifyCaptureResults(SimpleCaptureCallback resultListener,
+            SimpleImageReaderListener imageListener, int sequenceId, boolean offlineResults)
+            throws Exception {
+        long sequenceLastFrameNumber = resultListener.getCaptureSequenceLastFrameNumber(
+                sequenceId, 0 /*timeoutMs*/);
+
+        long lastFrameNumberReceived = -1;
+        while (resultListener.hasMoreResults()) {
+            TotalCaptureResult result = resultListener.getTotalCaptureResult(0 /*timeout*/);
+            if (lastFrameNumberReceived < result.getFrameNumber()) {
+                lastFrameNumberReceived = result.getFrameNumber();
+            }
+
+            if (imageListener != null) {
+                long resultTimestamp = result.get(CaptureResult.SENSOR_TIMESTAMP);
+                Image offlineImage = imageListener.getImage(CAPTURE_IMAGE_TIMEOUT_MS);
+                assertEquals("Offline image timestamp: " + offlineImage.getTimestamp() +
+                        " doesn't match with the result timestamp: " + resultTimestamp,
+                        offlineImage.getTimestamp(), resultTimestamp);
             }
         }
 
-        while (captureListener.hasMoreFailures()) {
-            ArrayList<CaptureFailure> failures = captureListener.getCaptureFailures(
+        while (resultListener.hasMoreFailures()) {
+            ArrayList<CaptureFailure> failures = resultListener.getCaptureFailures(
                     /*maxNumFailures*/ 1);
             for (CaptureFailure failure : failures) {
-                if (lastFrameNumber < failure.getFrameNumber()) {
-                    lastFrameNumber = failure.getFrameNumber();
+                if (lastFrameNumberReceived < failure.getFrameNumber()) {
+                    lastFrameNumberReceived = failure.getFrameNumber();
                 }
             }
         }
 
-        return lastFrameNumber;
-    }
-
-    private void verifyCaptureResults(SimpleCaptureCallback resultListener, int sequenceId,
-            boolean offlineResults) {
-        long sequenceLastFrameNumber = resultListener.getCaptureSequenceLastFrameNumber(
-                sequenceId, 0 /*timeoutMs*/);
-
-        long lastFrameNumberReceived = findLastFrameNumber(resultListener);
         String assertString = offlineResults ?
                 "Last offline frame number from " +
                 "onCaptureSequenceCompleted (%d) doesn't match the last frame number " +
@@ -403,16 +488,34 @@
                 sequenceLastFrameNumber, lastFrameNumberReceived);
     }
 
-    private void camera2OfflineSessionTest(Size offlineSize, int offlineFormat, boolean closeDevice,
-            boolean closeSession) throws Exception {
+    private void camera2OfflineSessionTest(String cameraId, Size offlineSize, int offlineFormat,
+            OfflineTestSequence testSequence) throws Exception {
+        int remoteOfflinePID = -1;
+        Size previewSize = mOrderedPreviewSizes.get(0);
+        for (Size sz : mOrderedPreviewSizes) {
+            if (sz.getWidth() <= MANDATORY_STREAM_BOUND.getWidth() && sz.getHeight() <=
+                    MANDATORY_STREAM_BOUND.getHeight()) {
+                previewSize = sz;
+                break;
+            }
+        }
+        Size privateSize = previewSize;
+        if (mAllStaticInfo.get(cameraId).isPrivateReprocessingSupported()) {
+            privateSize = mAllStaticInfo.get(cameraId).getSortedSizesForInputFormat(
+                    ImageFormat.PRIVATE, MANDATORY_STREAM_BOUND).get(0);
+        }
+
         CaptureRequest.Builder previewRequest =
                 mCamera.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);
         CaptureRequest.Builder stillCaptureRequest =
                 mCamera.createCaptureRequest(CameraDevice.TEMPLATE_STILL_CAPTURE);
-        Size previewSize = mOrderedPreviewSizes.get(0);
         SimpleCaptureCallback resultListener = new SimpleCaptureCallback();
+        SimpleCaptureCallback regularResultListener = new SimpleCaptureCallback();
         SimpleCaptureCallback offlineResultListener = new SimpleCaptureCallback();
         SimpleImageReaderListener imageListener = new SimpleImageReaderListener();
+        ImageReader privateReader = null;
+        ImageReader yuvCallbackReader = null;
+        ImageReader jpegReader = null;
 
         // Update preview size.
         updatePreviewSurface(previewSize);
@@ -441,18 +544,7 @@
         // Start preview.
         int repeatingSeqId = mSession.setRepeatingRequest(previewRequest.build(), resultListener,
                 mHandler);
-
-        CaptureResult result = resultListener.getCaptureResult(WAIT_FOR_FRAMES_TIMEOUT_MS);
-
-        Long timestamp = result.get(CaptureResult.SENSOR_TIMESTAMP);
-        assertNotNull("Can't read a capture result timestamp", timestamp);
-
-        CaptureResult result2 = resultListener.getCaptureResult(WAIT_FOR_FRAMES_TIMEOUT_MS);
-
-        Long timestamp2 = result2.get(CaptureResult.SENSOR_TIMESTAMP);
-        assertNotNull("Can't read a capture result 2 timestamp", timestamp2);
-
-        assertTrue("Bad timestamps", timestamp2 > timestamp);
+        checkInitialResults(resultListener);
 
         ArrayList<Integer> allowedOfflineStates = new ArrayList<Integer>();
         allowedOfflineStates.add(BlockingOfflineSessionCallback.STATE_READY);
@@ -487,45 +579,128 @@
             verify(mockOfflineCb, times(0)).onError(offlineSession,
                     CameraOfflineSessionCallback.STATUS_INTERNAL_ERROR);
 
-            verifyCaptureResults(resultListener, repeatingSeqId, false /*offlineResults*/);
-            verifyCaptureResults(offlineResultListener, offlineSeqId, true /*offlineResults*/);
+            verifyCaptureResults(resultListener, null /*imageListener*/, repeatingSeqId,
+                    false /*offlineResults*/);
+            verifyCaptureResults(offlineResultListener, null /*imageListener*/, offlineSeqId,
+                    true /*offlineResults*/);
         } else {
             verify(mockOfflineCb, times(1)).onReady(offlineSession);
             verify(mockOfflineCb, times(0)).onSwitchFailed(offlineSession);
 
-            if (closeDevice) {
-                // According to specification, closing the initial camera device after
-                // successful offline session switch must not have any noticeable impact
-                // on the offline client.
-                closeDevice();
+            switch (testSequence) {
+                case CloseDeviceAndOpenRemote:
+                    // According to the documentation, closing the initial camera device and
+                    // re-opening the same device from a different client after successful
+                    // offline session switch must not have any noticeable impact on the
+                    // offline processing.
+                    closeDevice();
+                    remoteOfflinePID = startRemoteOfflineTestProcess(cameraId);
+
+                    break;
+                case CloseOfflineSession:
+                    offlineSession.close();
+
+                    break;
+                case InitializeRegularSession:
+                    // According to the documentation, initializing a regular capture session
+                    // along with the offline session should not have any side effects.
+                    // We also don't want to re-use the same offline output surface as part
+                    // of the new  regular capture session.
+                    outputSurfaces.remove(mReaderSurface);
+
+                    // According to the specification, an active offline session must allow
+                    // camera devices to support at least one preview stream, one yuv stream
+                    // of size up-to 1080p, one jpeg stream with any supported size and
+                    // an extra input/output private pair in case reprocessing is also available.
+                    yuvCallbackReader = makeImageReader(previewSize, ImageFormat.YUV_420_888,
+                            1 /*maxNumImages*/, new SimpleImageReaderListener(), mHandler);
+                    outputSurfaces.add(yuvCallbackReader.getSurface());
+
+                    jpegReader = makeImageReader(offlineSize, ImageFormat.JPEG,
+                            1 /*maxNumImages*/, new SimpleImageReaderListener(), mHandler);
+                    outputSurfaces.add(jpegReader.getSurface());
+
+                    if (mAllStaticInfo.get(cameraId).isPrivateReprocessingSupported()) {
+                        privateReader = makeImageReader(privateSize, ImageFormat.PRIVATE,
+                                1 /*maxNumImages*/, new SimpleImageReaderListener(), mHandler);
+                        outputSurfaces.add(privateReader.getSurface());
+
+                        InputConfiguration inputConfig = new InputConfiguration(
+                                privateSize.getWidth(), privateSize.getHeight(),
+                                ImageFormat.PRIVATE);
+                        mSession = CameraTestUtils.configureReprocessableCameraSession(mCamera,
+                                inputConfig, outputSurfaces, mSessionListener, mHandler);
+
+                    } else {
+                        mSession = configureCameraSession(mCamera, outputSurfaces, mSessionListener,
+                                mHandler);
+                    }
+
+                    mSession.setRepeatingRequest(previewRequest.build(), regularResultListener,
+                            mHandler);
+
+                    break;
+                case NoExtraSteps:
+                default:
             }
 
-            if (closeSession) {
-                offlineSession.close();
-            }
+            // The repeating non-offline request should be done after the switch returns.
+            verifyCaptureResults(resultListener, null /*imageListener*/, repeatingSeqId,
+                    false /*offlineResults*/);
 
-            // The repeating non-offline request should be completed after the switch returns.
-            verifyCaptureResults(resultListener, repeatingSeqId, false /*offlineResults*/);
-
-            if (!closeSession) {
+            if (testSequence != OfflineTestSequence.CloseOfflineSession) {
                 offlineCb.waitForState(BlockingOfflineSessionCallback.STATE_IDLE,
                         WAIT_FOR_STATE_TIMEOUT_MS);
                 verify(mockOfflineCb, times(1)).onIdle(offlineSession);
                 verify(mockOfflineCb, times(0)).onError(offlineSession,
                         CameraOfflineSessionCallback.STATUS_INTERNAL_ERROR);
 
-                // The offline requests should be completed after we reach idle state.
-                verifyCaptureResults(offlineResultListener, offlineSeqId, true /*offlineResults*/);
+                // The offline requests should be done after we reach idle state.
+                verifyCaptureResults(offlineResultListener, imageListener, offlineSeqId,
+                        true /*offlineResults*/);
 
                 offlineSession.close();
-                offlineCb.waitForState(BlockingOfflineSessionCallback.STATE_CLOSED,
-                        WAIT_FOR_STATE_TIMEOUT_MS);
             }
 
+            if (testSequence == OfflineTestSequence.InitializeRegularSession) {
+                checkInitialResults(regularResultListener);
+                stopPreview();
+
+                if (privateReader != null) {
+                    privateReader.close();
+                }
+
+                if (yuvCallbackReader != null) {
+                    yuvCallbackReader.close();
+                }
+
+                if (jpegReader != null) {
+                    jpegReader.close();
+                }
+            }
+
+            offlineCb.waitForState(BlockingOfflineSessionCallback.STATE_CLOSED,
+                  WAIT_FOR_STATE_TIMEOUT_MS);
             verify(mockOfflineCb, times(1)).onClosed(offlineSession);
         }
 
         closeImageReader();
+
+        stopRemoteOfflineTestProcess(remoteOfflinePID);
+    }
+
+    private void checkInitialResults(SimpleCaptureCallback resultListener) {
+        CaptureResult result = resultListener.getCaptureResult(WAIT_FOR_FRAMES_TIMEOUT_MS);
+
+        Long timestamp = result.get(CaptureResult.SENSOR_TIMESTAMP);
+        assertNotNull("Can't read a capture result timestamp", timestamp);
+
+        CaptureResult result2 = resultListener.getCaptureResult(WAIT_FOR_FRAMES_TIMEOUT_MS);
+
+        Long timestamp2 = result2.get(CaptureResult.SENSOR_TIMESTAMP);
+        assertNotNull("Can't read a capture result 2 timestamp", timestamp2);
+
+        assertTrue("Bad timestamps", timestamp2 > timestamp);
     }
 
     private void camera2UnsupportedOfflineOutputTest(boolean useSurfaceGroup) throws Exception {
@@ -574,4 +749,56 @@
 
         session.close();
     }
+
+    private int startRemoteOfflineTestProcess(String cameraId) throws InterruptedException {
+        // Ensure no running activity process with same name
+        String cameraActivityName = mContext.getPackageName() + ":" + REMOTE_PROCESS_NAME;
+        ActivityManager activityManager = (ActivityManager) mContext.getSystemService(
+                Context.ACTIVITY_SERVICE);
+        List<ActivityManager.RunningAppProcessInfo> list = activityManager.getRunningAppProcesses();
+        for (ActivityManager.RunningAppProcessInfo rai : list) {
+            if (cameraActivityName.equals(rai.processName)) {
+                fail("Remote offline session test activity already running");
+                return -1;
+            }
+        }
+
+        Activity activity = mActivityRule.getActivity();
+        Intent activityIntent = new Intent(activity, REMOTE_PROCESS_CLASS);
+        Bundle b = new Bundle();
+        b.putString(CameraTestUtils.OFFLINE_CAMERA_ID, cameraId);
+        activityIntent.putExtras(b);
+        activity.startActivity(activityIntent);
+        Thread.sleep(WAIT_FOR_REMOTE_ACTIVITY_LAUNCH_MS);
+
+        // Fail if activity isn't running
+        list = activityManager.getRunningAppProcesses();
+        for (ActivityManager.RunningAppProcessInfo rai : list) {
+            if (cameraActivityName.equals(rai.processName))
+                return rai.pid;
+        }
+
+        fail("Remote offline session test activity failed to start");
+
+        return -1;
+    }
+
+    private void stopRemoteOfflineTestProcess(int remotePID) throws InterruptedException {
+        if (remotePID < 0) {
+            return;
+        }
+
+        android.os.Process.killProcess(remotePID);
+        Thread.sleep(WAIT_FOR_REMOTE_ACTIVITY_DESTROY_MS);
+
+        ActivityManager activityManager = (ActivityManager) mContext.getSystemService(
+                Context.ACTIVITY_SERVICE);
+        String cameraActivityName = mContext.getPackageName() + ":" + REMOTE_PROCESS_NAME;
+        List<ActivityManager.RunningAppProcessInfo> list = activityManager.getRunningAppProcesses();
+        for (ActivityManager.RunningAppProcessInfo rai : list) {
+            if (cameraActivityName.equals(rai.processName))
+                fail("Remote offline session test activity is still running");
+        }
+
+    }
 }
diff --git a/tests/camera/utils/src/android/hardware/camera2/cts/CameraTestUtils.java b/tests/camera/utils/src/android/hardware/camera2/cts/CameraTestUtils.java
index f64ff08..da6593f 100644
--- a/tests/camera/utils/src/android/hardware/camera2/cts/CameraTestUtils.java
+++ b/tests/camera/utils/src/android/hardware/camera2/cts/CameraTestUtils.java
@@ -120,6 +120,8 @@
 
     public static final int MAX_READER_IMAGES = 5;
 
+    public static final String OFFLINE_CAMERA_ID = "offline_camera_id";
+
     private static final int EXIF_DATETIME_LENGTH = 19;
     private static final int EXIF_DATETIME_ERROR_MARGIN_SEC = 60;
     private static final float EXIF_FOCAL_LENGTH_ERROR_MARGIN = 0.001f;
diff --git a/tests/camera/utils/src/android/hardware/camera2/cts/helpers/StaticMetadata.java b/tests/camera/utils/src/android/hardware/camera2/cts/helpers/StaticMetadata.java
index bcad582..a5bedea 100644
--- a/tests/camera/utils/src/android/hardware/camera2/cts/helpers/StaticMetadata.java
+++ b/tests/camera/utils/src/android/hardware/camera2/cts/helpers/StaticMetadata.java
@@ -2082,6 +2082,45 @@
     }
 
     /**
+     * Determine whether the current device supports a private reprocessing capability or not.
+     *
+     * @return {@code true} if the capability is supported, {@code false} otherwise.
+     *
+     * @throws IllegalArgumentException if {@code capability} was negative
+     */
+    public boolean isPrivateReprocessingSupported() {
+        return isCapabilitySupported(
+                CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_PRIVATE_REPROCESSING);
+    }
+
+    /**
+     * Get sorted (descending order) size list for given input format. Remove the sizes larger than
+     * the bound. If the bound is null, don't do the size bound filtering.
+     *
+     * @param format input format
+     * @param bound maximum allowed size bound
+     *
+     * @return Sorted input size list (descending order)
+     */
+    public List<Size> getSortedSizesForInputFormat(int format, Size bound) {
+        Size[] availableSizes = getAvailableSizesForFormatChecked(format, StreamDirection.Input);
+        if (bound == null) {
+            return CameraTestUtils.getAscendingOrderSizes(Arrays.asList(availableSizes),
+                    /*ascending*/false);
+        }
+
+        List<Size> sizes = new ArrayList<Size>();
+        for (Size sz: availableSizes) {
+            if (sz.getWidth() <= bound.getWidth() && sz.getHeight() <= bound.getHeight()) {
+                sizes.add(sz);
+            }
+        }
+
+        return CameraTestUtils.getAscendingOrderSizes(sizes, /*ascending*/false);
+    }
+
+
+    /**
      * Determine whether or not all the {@code keys} are available characteristics keys
      * (as in {@link CameraCharacteristics#getKeys}.
      *