Merge "[Extensions] Fixed surface abandoned crash when pause/resume too quickly on Samsung Z Fold2" into androidx-main
diff --git a/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/internal/compat/workaround/OnEnableDisableSessionDurationCheckTest.kt b/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/internal/compat/workaround/OnEnableDisableSessionDurationCheckTest.kt
new file mode 100644
index 0000000..dd416ee
--- /dev/null
+++ b/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/internal/compat/workaround/OnEnableDisableSessionDurationCheckTest.kt
@@ -0,0 +1,97 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.extensions.internal.compat.workaround
+
+import android.os.SystemClock
+import androidx.camera.extensions.internal.compat.workaround.OnEnableDisableSessionDurationCheck.MIN_DURATION_FOR_ENABLE_DISABLE_SESSION
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SdkSuppress
+import androidx.test.filters.SmallTest
+import com.google.common.collect.Range
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+@SdkSuppress(minSdkVersion = 21)
+class OnEnableDisableSessionDurationCheckTest {
+    companion object {
+        const val TOLERANCE = 15L
+    }
+
+    @Test
+    fun enabled_ensureMinimalDuration() {
+        // Arrange
+        val check = OnEnableDisableSessionDurationCheck(/* enabledMinimumDuration */true)
+
+        val duration = 40L
+        // Act
+        val startTime = SystemClock.elapsedRealtime()
+        check.onEnableSessionInvoked()
+        Thread.sleep(duration)
+        check.onDisableSessionInvoked()
+        val endTime = SystemClock.elapsedRealtime()
+
+        // Assert
+        assertThat((endTime - startTime))
+            .isIn(
+                Range.closed(
+                    MIN_DURATION_FOR_ENABLE_DISABLE_SESSION,
+                    MIN_DURATION_FOR_ENABLE_DISABLE_SESSION + TOLERANCE
+                ))
+    }
+
+    @Test
+    fun enabled_doNotWaitExtraIfDurationExceeds() {
+        // 1. Arrange
+        val check = OnEnableDisableSessionDurationCheck(/* enabledMinimumDuration */true)
+
+        // make the duration of onEnable to onDisable to be the minimal duration.
+        val duration = MIN_DURATION_FOR_ENABLE_DISABLE_SESSION
+
+        // 2. Act
+        val startTime = SystemClock.elapsedRealtime()
+        check.onEnableSessionInvoked()
+        // make the duration of onEnable to onDisable to be the minimal duration.
+        Thread.sleep(duration)
+        check.onDisableSessionInvoked()
+        val endTime = SystemClock.elapsedRealtime()
+
+        // 3. Assert: no extra time waited.
+        assertThat((endTime - startTime))
+            .isLessThan(
+                duration + TOLERANCE
+            )
+    }
+
+    @Test
+    fun disabled_doNotWait() {
+        // 1. Arrange
+        val check = OnEnableDisableSessionDurationCheck(/* enabledMinimumDuration */ false)
+
+        // 2. Act
+        val startTime = SystemClock.elapsedRealtime()
+        check.onEnableSessionInvoked()
+        check.onDisableSessionInvoked()
+        val endTime = SystemClock.elapsedRealtime()
+
+        // 3. Assert
+        assertThat((endTime - startTime))
+            .isLessThan(TOLERANCE)
+    }
+}
\ No newline at end of file
diff --git a/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/compat/quirk/CrashWhenOnDisableTooSoon.java b/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/compat/quirk/CrashWhenOnDisableTooSoon.java
new file mode 100644
index 0000000..5ed2ce16
--- /dev/null
+++ b/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/compat/quirk/CrashWhenOnDisableTooSoon.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.extensions.internal.compat.quirk;
+
+import android.os.Build;
+
+import androidx.annotation.RequiresApi;
+import androidx.camera.core.impl.Quirk;
+
+/**
+ * <p>QuirkSummary
+ * Bug Id: b/245226085,
+ * Description: When enabling Extensions on devices that implement the Basic Extender. If
+ * onDisableSession is invoked within 50ms after onEnableSession, It could cause crashes like
+ * surface abandoned.
+ * Device(s): All Samsung devices
+ */
+@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
+public class CrashWhenOnDisableTooSoon implements Quirk {
+    static boolean load() {
+        return Build.BRAND.equalsIgnoreCase("SAMSUNG");
+    }
+}
diff --git a/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/compat/quirk/DeviceQuirksLoader.java b/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/compat/quirk/DeviceQuirksLoader.java
index bd85647..82bb2e7 100644
--- a/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/compat/quirk/DeviceQuirksLoader.java
+++ b/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/compat/quirk/DeviceQuirksLoader.java
@@ -44,6 +44,10 @@
             quirks.add(new ExtensionDisabledQuirk());
         }
 
+        if (CrashWhenOnDisableTooSoon.load()) {
+            quirks.add(new CrashWhenOnDisableTooSoon());
+        }
+
         return quirks;
     }
 }
diff --git a/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/compat/quirk/ExtensionDisabledQuirk.java b/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/compat/quirk/ExtensionDisabledQuirk.java
index 992962c..c1ba5c4 100644
--- a/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/compat/quirk/ExtensionDisabledQuirk.java
+++ b/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/compat/quirk/ExtensionDisabledQuirk.java
@@ -40,17 +40,16 @@
  */
 @RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
 public class ExtensionDisabledQuirk implements Quirk {
-    private final boolean mIsAdvancedInterface = isAdvancedExtenderSupported();
 
     static boolean load() {
-        return isPixel5() || isMoto() || isAdvancedExtenderSupported();
+        return isPixel5() || isMoto();
     }
 
     /**
      * Checks whether extension should be disabled.
      */
     public boolean shouldDisableExtension() {
-        if (isPixel5() && !mIsAdvancedInterface) {
+        if (isPixel5() && !isAdvancedExtenderSupported()) {
             // 1. Disables Pixel 5's Basic Extender capability.
             return true;
         } else if (isMoto() && ExtensionVersion.isMaximumCompatibleVersion(Version.VERSION_1_1)) {
diff --git a/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/compat/workaround/OnEnableDisableSessionDurationCheck.java b/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/compat/workaround/OnEnableDisableSessionDurationCheck.java
new file mode 100644
index 0000000..ffc0791
--- /dev/null
+++ b/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/compat/workaround/OnEnableDisableSessionDurationCheck.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.extensions.internal.compat.workaround;
+
+import android.os.SystemClock;
+
+import androidx.annotation.RequiresApi;
+import androidx.annotation.VisibleForTesting;
+import androidx.camera.core.Logger;
+import androidx.camera.extensions.internal.compat.quirk.CrashWhenOnDisableTooSoon;
+import androidx.camera.extensions.internal.compat.quirk.DeviceQuirks;
+
+/**
+ * A workaround to ensure the duration of onEnableSession to onDisableSession is long enough.
+ */
+@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
+public class OnEnableDisableSessionDurationCheck {
+    private static final String TAG = "OnEnableDisableSessionDurationCheck";
+    private final boolean mEnabledMinimumDuration;
+    private long mOnEnableSessionTimeStamp = 0;
+    @VisibleForTesting
+    static final long MIN_DURATION_FOR_ENABLE_DISABLE_SESSION = 100L;
+
+    public OnEnableDisableSessionDurationCheck() {
+        mEnabledMinimumDuration = DeviceQuirks.get(CrashWhenOnDisableTooSoon.class) != null;
+    }
+
+    @VisibleForTesting
+    OnEnableDisableSessionDurationCheck(boolean enabledMinimalDuration) {
+        mEnabledMinimumDuration = enabledMinimalDuration;
+    }
+
+    /**
+     * Notify onEnableSession is invoked.
+     */
+    public void onEnableSessionInvoked() {
+        if (mEnabledMinimumDuration) {
+            mOnEnableSessionTimeStamp = SystemClock.elapsedRealtime();
+        }
+    }
+
+    /**
+     * Notify onDisableSession is invoked.
+     */
+    public void onDisableSessionInvoked() {
+        if (mEnabledMinimumDuration) {
+            ensureMinDurationAfterOnEnableSession();
+        }
+    }
+
+    /**
+     * Ensures onDisableSession not invoked too soon after onDisableSession. OEMs usually
+     * releases resources at onDisableSession. Invoking onDisableSession too soon might cause
+     * some crash during the initialization triggered by onEnableSession or onInit.
+     *
+     * It will ensure the duration is at least 100 ms after onEnabledSession is invoked. If the
+     * camera is opened more than 100ms, then it won't add any extra delay. Since a regular
+     * camera session will take more than 100ms, this change shouldn't cause any real impact for
+     * the user. It only affects the auto testing by increasing a little bit delay which
+     * should be okay.
+     */
+    private void ensureMinDurationAfterOnEnableSession() {
+        long timeAfterOnEnableSession =
+                SystemClock.elapsedRealtime() - mOnEnableSessionTimeStamp;
+        if (timeAfterOnEnableSession < MIN_DURATION_FOR_ENABLE_DISABLE_SESSION) {
+            try {
+                long timeToWait =
+                        MIN_DURATION_FOR_ENABLE_DISABLE_SESSION - timeAfterOnEnableSession;
+                Logger.d(TAG, "onDisableSession too soon, wait " + timeToWait + " ms");
+                Thread.sleep(timeToWait);
+            } catch (InterruptedException e) {
+                Logger.e(TAG, "sleep interrupted");
+            }
+        }
+    }
+}
diff --git a/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/sessionprocessor/BasicExtenderSessionProcessor.java b/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/sessionprocessor/BasicExtenderSessionProcessor.java
index 5049106..5452a8a 100644
--- a/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/sessionprocessor/BasicExtenderSessionProcessor.java
+++ b/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/sessionprocessor/BasicExtenderSessionProcessor.java
@@ -49,6 +49,7 @@
 import androidx.camera.extensions.impl.PreviewExtenderImpl;
 import androidx.camera.extensions.impl.PreviewImageProcessorImpl;
 import androidx.camera.extensions.impl.RequestUpdateProcessorImpl;
+import androidx.camera.extensions.internal.compat.workaround.OnEnableDisableSessionDurationCheck;
 import androidx.core.util.Preconditions;
 
 import java.util.ArrayList;
@@ -90,6 +91,8 @@
     static AtomicInteger sLastOutputConfigId = new AtomicInteger(0);
     @GuardedBy("mLock")
     private final Map<CaptureRequest.Key<?>, Object> mParameters = new LinkedHashMap<>();
+    private OnEnableDisableSessionDurationCheck mOnEnableDisableSessionDurationCheck =
+            new OnEnableDisableSessionDurationCheck();
 
     public BasicExtenderSessionProcessor(@NonNull PreviewExtenderImpl previewExtenderImpl,
             @NonNull ImageCaptureExtenderImpl imageCaptureExtenderImpl,
@@ -252,6 +255,7 @@
         if (captureStage2 != null) {
             captureStages.add(captureStage2);
         }
+        mOnEnableDisableSessionDurationCheck.onEnableSessionInvoked();
 
         if (!captureStages.isEmpty()) {
             submitRequestByCaptureStages(requestProcessor, captureStages);
@@ -323,6 +327,7 @@
 
     @Override
     public void onCaptureSessionEnd() {
+        mOnEnableDisableSessionDurationCheck.onDisableSessionInvoked();
         List<CaptureStageImpl> captureStages = new ArrayList<>();
         CaptureStageImpl captureStage1 = mPreviewExtenderImpl.onDisableSession();
         Logger.d(TAG, "preview onDisableSession: " + captureStage1);