Integrate new OnBackPressedInvoked APIs into DrawerLayout

Fixes: 226594045
Test: DrawerBackHandlingTest
Change-Id: I97c65c3d0f26a88039b78046e89a0e84e5527e36
diff --git a/drawerlayout/drawerlayout/build.gradle b/drawerlayout/drawerlayout/build.gradle
index 4e5ebdd..8bf6fa0 100644
--- a/drawerlayout/drawerlayout/build.gradle
+++ b/drawerlayout/drawerlayout/build.gradle
@@ -3,18 +3,22 @@
 plugins {
     id("AndroidXPlugin")
     id("com.android.library")
+    id("kotlin-android")
 }
 
 dependencies {
-    api("androidx.annotation:annotation:1.1.0")
+    api("androidx.annotation:annotation:1.2.0")
     api("androidx.core:core:1.2.0")
     api(project(":customview:customview"))
 
+    androidTestImplementation(libs.kotlinStdlib)
     androidTestImplementation(libs.testExtJunit)
     androidTestImplementation(libs.testCore)
     androidTestImplementation(libs.testRunner)
     androidTestImplementation(libs.testRules)
     androidTestImplementation(libs.truth)
+    androidTestImplementation(libs.espressoCore, excludes.espresso)
+    androidTestImplementation(project(":internal-testutils-runtime"))
 }
 
 androidx {
diff --git a/drawerlayout/drawerlayout/src/androidTest/AndroidManifest.xml b/drawerlayout/drawerlayout/src/androidTest/AndroidManifest.xml
index 62cff76..ee402d3 100644
--- a/drawerlayout/drawerlayout/src/androidTest/AndroidManifest.xml
+++ b/drawerlayout/drawerlayout/src/androidTest/AndroidManifest.xml
@@ -23,6 +23,8 @@
                 android:theme="@style/CustomDrawerLayoutTheme"/>
         <activity
             android:name="androidx.drawerlayout.widget.DrawerNoThemeActivity"/>
+        <activity
+            android:name="androidx.drawerlayout.widget.DrawerSingleStartActivity"/>
     </application>
 
 </manifest>
diff --git a/drawerlayout/drawerlayout/src/androidTest/java/androidx/drawerlayout/widget/DrawerBackHandlingTest.kt b/drawerlayout/drawerlayout/src/androidTest/java/androidx/drawerlayout/widget/DrawerBackHandlingTest.kt
new file mode 100644
index 0000000..f9f638f
--- /dev/null
+++ b/drawerlayout/drawerlayout/src/androidTest/java/androidx/drawerlayout/widget/DrawerBackHandlingTest.kt
@@ -0,0 +1,101 @@
+/*
+ * Copyright 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.drawerlayout.widget
+
+import android.os.Build
+import android.view.KeyEvent
+import android.view.View
+import androidx.drawerlayout.test.R
+import androidx.test.espresso.Espresso
+import androidx.test.espresso.action.ViewActions
+import androidx.test.espresso.matcher.ViewMatchers
+import androidx.test.ext.junit.rules.ActivityScenarioRule
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import androidx.testutils.PollingCheck
+import androidx.testutils.withActivity
+import org.junit.Assert
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+public class DrawerBackHandlingTest {
+    @get:Rule
+    public val activityScenarioRule = ActivityScenarioRule(
+        DrawerSingleStartActivity::class.java
+    )
+
+    @Test
+    @SmallTest
+    public fun testBackPress() {
+        val listener = ObservableDrawerListener()
+        val drawerLayout = activityScenarioRule.withActivity {
+            val drawerLayout = findViewById<DrawerLayout>(R.id.drawer)
+            drawerLayout.addDrawerListener(listener)
+            drawerLayout.open()
+            drawerLayout
+        }
+
+        // Wait until the animation ends. We disable animations on test
+        // devices, but this is useful when running on a local device.
+        PollingCheck.waitFor {
+            listener.drawerOpenedCalled
+        }
+        listener.reset()
+
+        // Ensure that back pressed dispatcher callback is registered on T+.
+        if (Build.VERSION.SDK_INT >= 33) {
+            Assert.assertTrue(drawerLayout.isBackInvokedCallbackRegistered)
+        }
+
+        Espresso.onView(ViewMatchers.isRoot()).perform(ViewActions.pressKey(KeyEvent.KEYCODE_BACK))
+
+        PollingCheck.waitFor {
+            listener.drawerClosedCalled
+        }
+        listener.reset()
+
+        Assert.assertNull(drawerLayout.findOpenDrawer())
+
+        // Ensure that back pressed dispatcher callback is unregistered on T+.
+        if (Build.VERSION.SDK_INT >= 33) {
+            Assert.assertFalse(drawerLayout.isBackInvokedCallbackRegistered)
+        }
+    }
+
+    internal inner class ObservableDrawerListener : DrawerLayout.DrawerListener {
+        var drawerOpenedCalled = false
+        var drawerClosedCalled = false
+
+        override fun onDrawerSlide(drawerView: View, slideOffset: Float) {}
+
+        override fun onDrawerOpened(drawerView: View) {
+            drawerOpenedCalled = true
+        }
+
+        override fun onDrawerClosed(drawerView: View) {
+            drawerClosedCalled = true
+        }
+
+        override fun onDrawerStateChanged(newState: Int) {}
+
+        fun reset() {
+            drawerOpenedCalled = false
+            drawerClosedCalled = false
+        }
+    }
+}
diff --git a/drawerlayout/drawerlayout/src/androidTest/java/androidx/drawerlayout/widget/DrawerSingleStartActivity.java b/drawerlayout/drawerlayout/src/androidTest/java/androidx/drawerlayout/widget/DrawerSingleStartActivity.java
new file mode 100644
index 0000000..56596e78
--- /dev/null
+++ b/drawerlayout/drawerlayout/src/androidTest/java/androidx/drawerlayout/widget/DrawerSingleStartActivity.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright 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.drawerlayout.widget;
+
+import android.app.Activity;
+import android.os.Bundle;
+
+import androidx.drawerlayout.test.R;
+
+public class DrawerSingleStartActivity extends Activity {
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        setContentView(R.layout.drawer_single_start_layout);
+    }
+}
diff --git a/drawerlayout/drawerlayout/src/androidTest/res/layout/drawer_single_start_layout.xml b/drawerlayout/drawerlayout/src/androidTest/res/layout/drawer_single_start_layout.xml
new file mode 100644
index 0000000..8993394
--- /dev/null
+++ b/drawerlayout/drawerlayout/src/androidTest/res/layout/drawer_single_start_layout.xml
@@ -0,0 +1,38 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  Copyright 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.
+  -->
+
+<androidx.drawerlayout.widget.DrawerLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/drawer"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent">
+
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:orientation="vertical" />
+
+    <LinearLayout
+        android:id="@+id/panel"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:layout_gravity="start"
+        android:orientation="vertical"
+        android:background="@android:color/holo_blue_bright" />
+
+</androidx.drawerlayout.widget.DrawerLayout>
+
diff --git a/drawerlayout/drawerlayout/src/main/java/androidx/drawerlayout/widget/DrawerLayout.java b/drawerlayout/drawerlayout/src/main/java/androidx/drawerlayout/widget/DrawerLayout.java
index f577db8..a841c29 100644
--- a/drawerlayout/drawerlayout/src/main/java/androidx/drawerlayout/widget/DrawerLayout.java
+++ b/drawerlayout/drawerlayout/src/main/java/androidx/drawerlayout/widget/DrawerLayout.java
@@ -43,13 +43,18 @@
 import android.view.ViewGroup;
 import android.view.ViewParent;
 import android.view.accessibility.AccessibilityEvent;
+import android.window.OnBackInvokedCallback;
+import android.window.OnBackInvokedDispatcher;
 
 import androidx.annotation.ColorInt;
+import androidx.annotation.DoNotInline;
 import androidx.annotation.DrawableRes;
 import androidx.annotation.IntDef;
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
 import androidx.annotation.RestrictTo;
+import androidx.annotation.VisibleForTesting;
 import androidx.core.content.ContextCompat;
 import androidx.core.graphics.Insets;
 import androidx.core.graphics.drawable.DrawableCompat;
@@ -218,6 +223,14 @@
     private boolean mInLayout;
     private boolean mFirstLayout = true;
 
+    // The callback handling back events. If this is non-null, the
+    // callback has been registered at least once.
+    private OnBackInvokedCallback mBackInvokedCallback;
+
+    // The dispatcher on which the callback was registered. If this
+    // value is null, the callback is not registered anywhere.
+    private OnBackInvokedDispatcher mBackInvokedDispatcher;
+
     private @LockMode int mLockModeLeft = LOCK_MODE_UNDEFINED;
     private @LockMode int mLockModeRight = LOCK_MODE_UNDEFINED;
     private @LockMode int mLockModeStart = LOCK_MODE_UNDEFINED;
@@ -883,6 +896,7 @@
 
             updateChildrenImportantForAccessibility(drawerView, false);
             updateChildAccessibilityAction(drawerView);
+            updateBackInvokedCallbackState();
 
             // Only send WINDOW_STATE_CHANGE if the host has window focus. This
             // may change if support for multiple foreground windows (e.g. IME)
@@ -911,6 +925,7 @@
 
             updateChildrenImportantForAccessibility(drawerView, true);
             updateChildAccessibilityAction(drawerView);
+            updateBackInvokedCallbackState();
 
             // Only send WINDOW_STATE_CHANGE if the host has window focus.
             if (hasWindowFocus()) {
@@ -1045,12 +1060,16 @@
     protected void onDetachedFromWindow() {
         super.onDetachedFromWindow();
         mFirstLayout = true;
+
+        updateBackInvokedCallbackState();
     }
 
     @Override
     protected void onAttachedToWindow() {
         super.onAttachedToWindow();
         mFirstLayout = true;
+
+        updateBackInvokedCallbackState();
     }
 
     @SuppressLint("WrongConstant")
@@ -1731,6 +1750,7 @@
 
             updateChildrenImportantForAccessibility(drawerView, true);
             updateChildAccessibilityAction(drawerView);
+            updateBackInvokedCallbackState();
         } else if (animate) {
             lp.openState |= LayoutParams.FLAG_IS_OPENING;
 
@@ -2028,6 +2048,40 @@
         }
     }
 
+    /**
+     * Call this method whenever a property changes that affects whether the view will handle a
+     * back press, which is the combination of properties inspected in {@link #closeDrawers()} and
+     * properties that affect whether this view would normally receive key press events.
+     */
+    void updateBackInvokedCallbackState() {
+        if (Build.VERSION.SDK_INT >= 33) {
+            View visibleDrawer = findVisibleDrawer();
+            OnBackInvokedDispatcher currentDispatcher = Api33Impl.findOnBackInvokedDispatcher(this);
+            boolean shouldBeRegistered = visibleDrawer != null
+                    && currentDispatcher != null
+                    && getDrawerLockMode(visibleDrawer) == LOCK_MODE_UNLOCKED
+                    && ViewCompat.isAttachedToWindow(this);
+
+            if (shouldBeRegistered && mBackInvokedDispatcher == null) {
+                if (mBackInvokedCallback == null) {
+                    mBackInvokedCallback = Api33Impl.newOnBackInvokedCallback(this::closeDrawers);
+                }
+                Api33Impl.tryRegisterOnBackInvokedCallback(
+                        currentDispatcher, mBackInvokedCallback);
+                mBackInvokedDispatcher = currentDispatcher;
+            } else if (!shouldBeRegistered && mBackInvokedDispatcher != null) {
+                Api33Impl.tryUnregisterOnBackInvokedCallback(
+                        mBackInvokedDispatcher, mBackInvokedCallback);
+                mBackInvokedDispatcher = null;
+            }
+        }
+    }
+
+    @VisibleForTesting
+    boolean isBackInvokedCallbackRegistered() {
+        return mBackInvokedDispatcher != null;
+    }
+
     @Override
     public boolean onKeyDown(int keyCode, KeyEvent event) {
         if (keyCode == KeyEvent.KEYCODE_BACK && hasVisibleDrawer()) {
@@ -2527,4 +2581,38 @@
             }
         }
     }
+
+    @RequiresApi(33)
+    static class Api33Impl {
+        private Api33Impl() {
+            // This class is not instantiable.
+        }
+
+        @DoNotInline
+        static void tryRegisterOnBackInvokedCallback(@NonNull Object dispatcherObj,
+                @NonNull Object callback) {
+            OnBackInvokedDispatcher dispatcher = (OnBackInvokedDispatcher) dispatcherObj;
+            dispatcher.registerOnBackInvokedCallback(OnBackInvokedDispatcher.PRIORITY_OVERLAY,
+                    (OnBackInvokedCallback) callback);
+        }
+
+        @DoNotInline
+        static void tryUnregisterOnBackInvokedCallback(@NonNull Object dispatcherObj,
+                @NonNull Object callbackObj) {
+            OnBackInvokedDispatcher dispatcher = (OnBackInvokedDispatcher) dispatcherObj;
+            dispatcher.unregisterOnBackInvokedCallback((OnBackInvokedCallback) callbackObj);
+        }
+
+        @Nullable
+        @DoNotInline
+        static OnBackInvokedDispatcher findOnBackInvokedDispatcher(@NonNull DrawerLayout view) {
+            return view.findOnBackInvokedDispatcher();
+        }
+
+        @NonNull
+        @DoNotInline
+        static OnBackInvokedCallback newOnBackInvokedCallback(@NonNull Runnable action) {
+            return action::run;
+        }
+    }
 }