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;
+ }
+ }
}