Update CarEvsCameraActivity and CarEvsCameraPreviewActivity

- CarEvsCameraActivity explicitly stops monitoring the car service's
  status in its onDestroy(), to make sure it does not respond to any
  status changes before it is handled by the garbage collector.
- CarEvsCameraPreviewActivity stops rendering the contents but keeps
  circulating frame buffers when it becomes invisible.
- When the car service is restored from the incidents,
  CarEvsCameraPreviewApp will request staring a video stream if it was
  initially launched by the user.

Fix: 235697779
Test: Run EVS regression tests
Change-Id: Id81f1dba1d98de9f0b32fb1838eae877d91aa63b
diff --git a/tests/CarEvsCameraPreviewApp/src/com/google/android/car/evs/CarEvsCameraActivity.java b/tests/CarEvsCameraPreviewApp/src/com/google/android/car/evs/CarEvsCameraActivity.java
index e7a9ccd..4c3eaa1 100644
--- a/tests/CarEvsCameraPreviewApp/src/com/google/android/car/evs/CarEvsCameraActivity.java
+++ b/tests/CarEvsCameraPreviewApp/src/com/google/android/car/evs/CarEvsCameraActivity.java
@@ -16,8 +16,11 @@
 
 package com.google.android.car.evs;
 
+import static android.car.evs.CarEvsManager.ERROR_NONE;
+
 import android.app.Activity;
 import android.car.Car;
+import android.car.Car.CarServiceLifecycleListener;
 import android.car.evs.CarEvsManager;
 import android.os.Bundle;
 import android.util.Log;
@@ -26,21 +29,42 @@
     private static final String TAG = CarEvsCameraActivity.class.getSimpleName();
     private static final int CAR_WAIT_TIMEOUT_MS = 3_000;
 
+    /** CarService status listener  */
+    private final CarServiceLifecycleListener mCarServiceLifecycleListener = (car, ready) -> {
+        if (!ready) {
+            return;
+        }
+
+        try {
+            CarEvsManager evsManager = (CarEvsManager) car.getCarManager(
+                    Car.CAR_EVS_SERVICE);
+            if (evsManager.startActivity(CarEvsManager.SERVICE_TYPE_REARVIEW) != ERROR_NONE) {
+                Log.e(TAG, "Failed to start a camera preview activity");
+            }
+        } finally {
+            mCar = car;
+            finish();
+        }
+    };
+
+    private Car mCar;
+
     @Override
     protected void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
 
         Car.createCar(getApplicationContext(), /* handler = */ null, CAR_WAIT_TIMEOUT_MS,
-                (car, ready) -> {
-                    if (!ready) {
-                        finish();
-                        return;
-                    }
-                    CarEvsManager evsManager = (CarEvsManager) car.getCarManager(
-                            Car.CAR_EVS_SERVICE);
-                    int result = evsManager.startActivity(CarEvsManager.SERVICE_TYPE_REARVIEW);
-                    Log.i(TAG, "startActivity(): " + result);
-                    finish();
-                });
+                mCarServiceLifecycleListener);
+    }
+
+    @Override
+    protected void onDestroy() {
+        Log.d(TAG, "onDestroy");
+        if (mCar != null) {
+            // Explicitly stops monitoring the car service's status
+            mCar.disconnect();
+        }
+
+        super.onDestroy();
     }
 }
diff --git a/tests/CarEvsCameraPreviewApp/src/com/google/android/car/evs/CarEvsCameraPreviewActivity.java b/tests/CarEvsCameraPreviewApp/src/com/google/android/car/evs/CarEvsCameraPreviewActivity.java
index 90fd608..bb1c6a1 100644
--- a/tests/CarEvsCameraPreviewApp/src/com/google/android/car/evs/CarEvsCameraPreviewActivity.java
+++ b/tests/CarEvsCameraPreviewApp/src/com/google/android/car/evs/CarEvsCameraPreviewActivity.java
@@ -16,6 +16,7 @@
 
 package com.google.android.car.evs;
 
+import static android.car.evs.CarEvsManager.ERROR_NONE;
 import static android.hardware.display.DisplayManager.DisplayListener;
 
 import android.app.Activity;
@@ -41,6 +42,8 @@
 import android.view.WindowManager;
 import android.widget.LinearLayout;
 
+import androidx.annotation.GuardedBy;
+
 import java.util.ArrayList;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
@@ -48,7 +51,35 @@
 public class CarEvsCameraPreviewActivity extends Activity {
     private static final String TAG = CarEvsCameraPreviewActivity.class.getSimpleName();
 
+    /**
+     * Defines internal states.
+     */
+    private final static int STREAM_STATE_STOPPED = 0;
+    private final static int STREAM_STATE_VISIBLE = 1;
+    private final static int STREAM_STATE_INVISIBLE = 2;
+    private final static int STREAM_STATE_LOST = 3;
+
+    private static String streamStateToString(int state) {
+        switch (state) {
+            case STREAM_STATE_STOPPED:
+                return "STOPPED";
+
+            case STREAM_STATE_VISIBLE:
+                return "VISIBLE";
+
+            case STREAM_STATE_INVISIBLE:
+                return "INVISIBLE";
+
+            case STREAM_STATE_LOST:
+                return "LOST";
+
+            default:
+                return "UNKNOWN: " + state;
+        }
+    }
+
     /** Buffer queue to store references of received frames */
+    @GuardedBy("mLock")
     private final ArrayList<CarEvsBufferDescriptor> mBufferQueue = new ArrayList<>();
 
     private final Object mLock = new Object();
@@ -68,11 +99,16 @@
     private int mDisplayState = Display.STATE_OFF;
 
     /** Tells whether or not a video stream is running */
-    private boolean mStreamRunning = false;
+    @GuardedBy("mLock")
+    private int mStreamState = STREAM_STATE_STOPPED;
 
+    @GuardedBy("mLock")
     private Car mCar;
+
+    @GuardedBy("mLock")
     private CarEvsManager mEvsManager;
 
+    @GuardedBy("mLock")
     private IBinder mSessionToken;
 
     private boolean mUseSystemWindow;
@@ -93,9 +129,15 @@
 
         @Override
         public void onNewFrame(CarEvsBufferDescriptor buffer) {
-            // Enqueues a new frame and posts a rendering job
-            synchronized (mBufferQueue) {
-                mBufferQueue.add(buffer);
+            synchronized (mLock) {
+                if (mStreamState == STREAM_STATE_INVISIBLE) {
+                    // When the activity becomes invisible (e.g. goes background), we immediately
+                    // returns received frame buffers instead of stopping a video stream.
+                    returnBufferLocked(buffer);
+                } else {
+                    // Enqueues a new frame and posts a rendering job
+                    mBufferQueue.add(buffer);
+                }
             }
         }
     };
@@ -119,34 +161,37 @@
             int state = decideViewVisibility();
             synchronized (mLock) {
                 mDisplayState = state;
-                handleVideoStreamLocked();
+                handleVideoStreamLocked(state == Display.STATE_ON ?
+                        STREAM_STATE_VISIBLE : STREAM_STATE_INVISIBLE);
             }
         }
     };
 
     /** CarService status listener  */
     private final CarServiceLifecycleListener mCarServiceLifecycleListener = (car, ready) -> {
-        if (!ready) {
-            Log.d(TAG, "Disconnected from the Car Service");
-            // Upon the CarService's accidental termination, CarEvsService gets released and
-            // CarEvsManager deregisters all listeners and callbacks.  So, we simply release
-            // CarEvsManager instance and update the status in handleVideoStreamLocked().
+        try {
             synchronized (mLock) {
-                mCar = null;
-                mEvsManager = null;
-                handleVideoStreamLocked();
-            }
-        } else {
-            Log.d(TAG, "Connected to the Car Service");
-            try {
-                synchronized (mLock) {
-                    mCar = car;
-                    mEvsManager = (CarEvsManager) car.getCarManager(Car.CAR_EVS_SERVICE);
-                    handleVideoStreamLocked();
+                mCar = ready ? car : null;
+                mEvsManager = ready ? (CarEvsManager) car.getCarManager(Car.CAR_EVS_SERVICE) : null;
+                if (!ready) {
+                    if (!mUseSystemWindow) {
+                        // If we were launched by the user manually, we enter the LOST state and
+                        // wait for the car service's restoration.
+                        handleVideoStreamLocked(STREAM_STATE_LOST);
+                    } else {
+                        // If we were launched by the system,we will clean up the states and
+                        // then finish; the car service will request a new instance when it comes
+                        // back from the incident while the system still requires the rearview.
+                        handleVideoStreamLocked(STREAM_STATE_STOPPED);
+                        finish();
+                    }
+                } else {
+                    // We request to start a video stream if we get connected to the car service.
+                    handleVideoStreamLocked(STREAM_STATE_VISIBLE);
                 }
-            } catch (CarNotConnectedException err) {
-                Log.e(TAG, "Failed to connect to the Car Service");
             }
+        } catch (CarNotConnectedException err) {
+            Log.e(TAG, "Failed to connect to the Car Service");
         }
     };
 
@@ -246,78 +291,105 @@
     }
 
     @Override
-    protected void onStart() {
-        Log.d(TAG, "onStart");
-        super.onStart();
-        handleVideoStreamLocked();
+    protected void onRestart() {
+        Log.d(TAG, "onRestart");
+        super.onRestart();
+        synchronized (mLock) {
+            // When we come back to the top task, we start rendering the view.
+            handleVideoStreamLocked(STREAM_STATE_VISIBLE);
+        }
     }
 
-
     @Override
     protected void onStop() {
         Log.d(TAG, "onStop");
-        super.onStop();
+        try {
+            synchronized (mLock) {
+                handleVideoStreamLocked(STREAM_STATE_INVISIBLE);
+            }
+        } finally {
+            super.onStop();
+        }
     }
 
     @Override
     protected void onDestroy() {
-        super.onDestroy();
         Log.d(TAG, "onDestroy");
-        // Request to stop current service and unregister a status listener
-        synchronized (mBufferQueue) {
-            mBufferQueue.clear();
-        }
-        synchronized (mLock) {
-            if (mEvsManager != null) {
-                mEvsManager.stopVideoStream();
-                mEvsManager.stopActivity();
-                mEvsManager.clearStatusListener();
+        try {
+            // Request to stop current service and unregister a status listener
+            synchronized (mLock) {
+                if (mEvsManager != null) {
+                    mEvsManager.stopActivity();
+                    mEvsManager.clearStatusListener();
+                }
+                if (mCar != null) {
+                    mCar.disconnect();
+                }
             }
-            if (mCar != null) {
-                mCar.disconnect();
-            }
-        }
-        mDisplayManager.unregisterDisplayListener(mDisplayListener);
-        if (mUseSystemWindow) {
-            WindowManager wm = getSystemService(WindowManager.class);
-            wm.removeView(mRootView);
-        }
 
-        unregisterReceiver(mBroadcastReceiver);
+            mDisplayManager.unregisterDisplayListener(mDisplayListener);
+            if (mUseSystemWindow) {
+                WindowManager wm = getSystemService(WindowManager.class);
+                wm.removeView(mRootView);
+            }
+
+            unregisterReceiver(mBroadcastReceiver);
+        } finally {
+            super.onDestroy();
+        }
     }
 
-    private void handleVideoStreamLocked() {
-        if (mEvsManager == null) {
-            Log.w(TAG, "CarEvsManager is not available.");
+    @GuardedBy("mLock")
+    private void handleVideoStreamLocked(int newState) {
+        Log.d(TAG, "Requested: " + streamStateToString(mStreamState) + " -> " +
+                streamStateToString(newState));
+        if (newState == mStreamState) {
+            // Safely ignore a request of transitioning to the current state.
             return;
         }
 
-        if (mDisplayState == Display.STATE_ON) {
-            // We show a camera preview only when the activity has been resumed and the display is
-            // on.
-            if (!mStreamRunning) {
-                Log.d(TAG, "Request to start a video stream");
-                mEvsManager.startVideoStream(CarEvsManager.SERVICE_TYPE_REARVIEW,
-                        mSessionToken, mCallbackExecutor, mStreamHandler);
-                mStreamRunning = true;
-            }
+        boolean needToUpdateState = false;
+        switch (newState) {
+            case STREAM_STATE_STOPPED:
+                if (mEvsManager != null) {
+                    mEvsManager.stopVideoStream();
+                    mBufferQueue.clear();
+                    needToUpdateState = true;
+                } else {
+                    Log.w(TAG, "EvsManager is not available");
+                }
+                break;
 
-            return;
+            case STREAM_STATE_VISIBLE:
+                // Starts a video stream
+                if (mEvsManager != null) {
+                    int result = mEvsManager.startVideoStream(CarEvsManager.SERVICE_TYPE_REARVIEW,
+                            mSessionToken, mCallbackExecutor, mStreamHandler);
+                    if (result != ERROR_NONE) {
+                        Log.e(TAG, "Failed to start a video stream, error = " + result);
+                    } else {
+                        needToUpdateState = true;
+                    }
+                } else {
+                    Log.w(TAG, "EvsManager is not available");
+                }
+                break;
+
+            case STREAM_STATE_INVISIBLE:
+                needToUpdateState = true;
+                break;
+
+            case STREAM_STATE_LOST:
+                needToUpdateState = true;
+                break;
+
+            default:
+                throw new IllegalArgumentException();
         }
 
-        // Otherwise, we do not need a video stream.
-        if (mStreamRunning) {
-            Log.d(TAG, "Request to stop a video stream");
-            mEvsManager.stopVideoStream();
-            mStreamRunning = false;
-
-            // We already stopped an active video stream so are safe to drop all buffer references.
-            synchronized (mBufferQueue) {
-                mBufferQueue.clear();
-            }
-
-            // Clear a buffer reference CarEvsCameraGLSurfaceView holds.
-            mEvsView.clearBuffer();
+        if (needToUpdateState) {
+            mStreamState = newState;
+            Log.d(TAG, "Completed: " + streamStateToString(mStreamState));
         }
     }
 
@@ -337,8 +409,8 @@
     }
 
     /** Get a new frame */
-    public CarEvsBufferDescriptor getNewFrame() {
-        synchronized (mBufferQueue) {
+    CarEvsBufferDescriptor getNewFrame() {
+        synchronized (mLock) {
             if (mBufferQueue.isEmpty()) {
                 return null;
             }
@@ -353,7 +425,18 @@
     }
 
     /** Request to return a buffer we're done with */
-    public void returnBuffer(CarEvsBufferDescriptor buffer) {
-        mEvsManager.returnFrameBuffer(buffer);
+    void returnBuffer(CarEvsBufferDescriptor buffer) {
+        synchronized (mLock) {
+            returnBufferLocked(buffer);
+        }
+    }
+
+    @GuardedBy("mLock")
+    private void returnBufferLocked(CarEvsBufferDescriptor buffer) {
+        try {
+            mEvsManager.returnFrameBuffer(buffer);
+        } catch (Exception e) {
+            Log.w(TAG, "CarEvsService is not available.");
+        }
     }
 }