diff --git a/core/api/current.txt b/core/api/current.txt index 3db168566870f..67efc7d56c425 100644 --- a/core/api/current.txt +++ b/core/api/current.txt @@ -48898,10 +48898,9 @@ package android.view { method public default void onBackInvoked(); } - public abstract class OnBackInvokedDispatcher { - ctor public OnBackInvokedDispatcher(); - method public abstract void registerOnBackInvokedCallback(@NonNull android.view.OnBackInvokedCallback, int); - method public abstract void unregisterOnBackInvokedCallback(@NonNull android.view.OnBackInvokedCallback); + public interface OnBackInvokedDispatcher { + method public void registerOnBackInvokedCallback(@NonNull android.view.OnBackInvokedCallback, @IntRange(from=0) int); + method public void unregisterOnBackInvokedCallback(@NonNull android.view.OnBackInvokedCallback); field public static final int PRIORITY_DEFAULT = 0; // 0x0 field public static final int PRIORITY_OVERLAY = 1000000; // 0xf4240 } diff --git a/core/api/test-current.txt b/core/api/test-current.txt index 9d6fc0e67f031..d5f45bc74d5c6 100644 --- a/core/api/test-current.txt +++ b/core/api/test-current.txt @@ -2800,6 +2800,10 @@ package android.view { field public static final int FLAG_IS_ACCESSIBILITY_EVENT = 2048; // 0x800 } + public interface OnBackInvokedDispatcher { + field public static final long DISPATCH_BACK_INVOCATION_AHEAD_OF_TIME = 195946584L; // 0xbade858L + } + @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.RUNTIME) @java.lang.annotation.Target({java.lang.annotation.ElementType.METHOD}) public @interface RemotableViewMethod { method public abstract String asyncImpl() default ""; } diff --git a/core/java/android/app/Activity.java b/core/java/android/app/Activity.java index 90c56ae633743..6c69c1f27ea26 100644 --- a/core/java/android/app/Activity.java +++ b/core/java/android/app/Activity.java @@ -115,6 +115,7 @@ import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; import android.view.MotionEvent; +import android.view.OnBackInvokedCallback; import android.view.OnBackInvokedDispatcher; import android.view.OnBackInvokedDispatcherOwner; import android.view.RemoteAnimationDefinition; @@ -144,6 +145,7 @@ import android.widget.AdapterView; import android.widget.Toast; import android.widget.Toolbar; import android.window.SplashScreen; +import android.window.WindowOnBackInvokedDispatcher; import com.android.internal.R; import com.android.internal.annotations.GuardedBy; @@ -791,6 +793,7 @@ public class Activity extends ContextThemeWrapper private static final int LOG_AM_ON_ACTIVITY_RESULT_CALLED = 30062; private static final int LOG_AM_ON_TOP_RESUMED_GAINED_CALLED = 30064; private static final int LOG_AM_ON_TOP_RESUMED_LOST_CALLED = 30065; + private OnBackInvokedCallback mDefaultBackCallback; /** * After {@link Build.VERSION_CODES#TIRAMISU}, @@ -1617,7 +1620,16 @@ public class Activity extends ContextThemeWrapper } mRestoredFromBundle = savedInstanceState != null; mCalled = true; - + if (!WindowOnBackInvokedDispatcher.shouldUseLegacyBack()) { + // Add onBackPressed as default back behavior. + mDefaultBackCallback = new OnBackInvokedCallback() { + @Override + public void onBackInvoked() { + navigateBack(); + } + }; + getOnBackInvokedDispatcher().registerSystemOnBackInvokedCallback(mDefaultBackCallback); + } } /** @@ -2653,6 +2665,10 @@ public class Activity extends ContextThemeWrapper if (mUiTranslationController != null) { mUiTranslationController.onActivityDestroyed(); } + + if (mDefaultBackCallback != null) { + getOnBackInvokedDispatcher().unregisterOnBackInvokedCallback(mDefaultBackCallback); + } } /** @@ -3773,10 +3789,13 @@ public class Activity extends ContextThemeWrapper * @see KeyEvent */ public boolean onKeyUp(int keyCode, KeyEvent event) { - if (getApplicationInfo().targetSdkVersion - >= Build.VERSION_CODES.ECLAIR) { - if (keyCode == KeyEvent.KEYCODE_BACK && event.isTracking() - && !event.isCanceled()) { + int sdkVersion = getApplicationInfo().targetSdkVersion; + if (sdkVersion >= Build.VERSION_CODES.ECLAIR) { + if (keyCode == KeyEvent.KEYCODE_BACK + && event.isTracking() + && !event.isCanceled() + && mDefaultBackCallback == null) { + // Using legacy back handling. onBackPressed(); return true; } @@ -3841,6 +3860,10 @@ public class Activity extends ContextThemeWrapper if (!fragmentManager.isStateSaved() && fragmentManager.popBackStackImmediate()) { return; } + navigateBack(); + } + + private void navigateBack() { if (!isTaskRoot()) { // If the activity is not the root of the task, allow finish to proceed normally. finishAfterTransition(); diff --git a/core/java/android/app/Dialog.java b/core/java/android/app/Dialog.java index 4b42ddc383b2a..208477588885c 100644 --- a/core/java/android/app/Dialog.java +++ b/core/java/android/app/Dialog.java @@ -52,6 +52,7 @@ import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuItem; import android.view.MotionEvent; +import android.view.OnBackInvokedCallback; import android.view.OnBackInvokedDispatcher; import android.view.OnBackInvokedDispatcherOwner; import android.view.SearchEvent; @@ -62,6 +63,7 @@ import android.view.ViewGroup.LayoutParams; import android.view.Window; import android.view.WindowManager; import android.view.accessibility.AccessibilityEvent; +import android.window.WindowOnBackInvokedDispatcher; import com.android.internal.R; import com.android.internal.app.WindowDecorActionBar; @@ -156,6 +158,7 @@ public class Dialog implements DialogInterface, Window.Callback, /** A {@link Runnable} to run instead of dismissing when {@link #dismiss()} is called. */ private Runnable mDismissOverride; + private OnBackInvokedCallback mDefaultBackCallback; /** * Creates a dialog window that uses the default dialog theme. @@ -453,6 +456,16 @@ public class Dialog implements DialogInterface, Window.Callback, */ protected void onStart() { if (mActionBar != null) mActionBar.setShowHideAnimationEnabled(true); + if (mContext != null && !WindowOnBackInvokedDispatcher.shouldUseLegacyBack()) { + // Add onBackPressed as default back behavior. + mDefaultBackCallback = new OnBackInvokedCallback() { + @Override + public void onBackInvoked() { + onBackPressed(); + } + }; + getOnBackInvokedDispatcher().registerSystemOnBackInvokedCallback(mDefaultBackCallback); + } } /** @@ -460,6 +473,9 @@ public class Dialog implements DialogInterface, Window.Callback, */ protected void onStop() { if (mActionBar != null) mActionBar.setShowHideAnimationEnabled(false); + if (mDefaultBackCallback != null) { + getOnBackInvokedDispatcher().unregisterOnBackInvokedCallback(mDefaultBackCallback); + } } private static final String DIALOG_SHOWING_TAG = "android:dialogShowing"; @@ -685,7 +701,8 @@ public class Dialog implements DialogInterface, Window.Callback, public boolean onKeyUp(int keyCode, @NonNull KeyEvent event) { if ((keyCode == KeyEvent.KEYCODE_BACK || keyCode == KeyEvent.KEYCODE_ESCAPE) && event.isTracking() - && !event.isCanceled()) { + && !event.isCanceled() + && WindowOnBackInvokedDispatcher.shouldUseLegacyBack()) { onBackPressed(); return true; } diff --git a/core/java/android/view/OnBackInvokedDispatcher.java b/core/java/android/view/OnBackInvokedDispatcher.java index f3ca531f2a428..5c4ee89bfbd3e 100644 --- a/core/java/android/view/OnBackInvokedDispatcher.java +++ b/core/java/android/view/OnBackInvokedDispatcher.java @@ -17,8 +17,13 @@ package android.view; import android.annotation.IntDef; +import android.annotation.IntRange; import android.annotation.NonNull; +import android.annotation.Nullable; import android.annotation.SuppressLint; +import android.annotation.TestApi; +import android.compat.annotation.ChangeId; +import android.compat.annotation.EnabledSince; import android.os.Build; import java.lang.annotation.Retention; @@ -32,13 +37,29 @@ import java.lang.annotation.RetentionPolicy; * Attribute updates are proactively pushed to the window manager if they change the dispatch * target (a.k.a. the callback to be invoked next), or its behavior. */ -public abstract class OnBackInvokedDispatcher { +public interface OnBackInvokedDispatcher { + /** + * Enables dispatching the "back" action via {@link android.view.OnBackInvokedDispatcher}. + * + * When enabled, the following APIs are no longer invoked: + * + * + * @hide + */ + @TestApi + @ChangeId + @EnabledSince(targetSdkVersion = Build.VERSION_CODES.TIRAMISU) + long DISPATCH_BACK_INVOCATION_AHEAD_OF_TIME = 195946584L; /** @hide */ - public static final String TAG = "OnBackInvokedDispatcher"; + String TAG = "OnBackInvokedDispatcher"; /** @hide */ - public static final boolean DEBUG = Build.isDebuggable(); + boolean DEBUG = Build.isDebuggable(); /** @hide */ @IntDef({ @@ -46,18 +67,26 @@ public abstract class OnBackInvokedDispatcher { PRIORITY_OVERLAY, }) @Retention(RetentionPolicy.SOURCE) - public @interface Priority{} + @interface Priority{} /** * Priority level of {@link OnBackInvokedCallback}s for overlays such as menus and * navigation drawers that should receive back dispatch before non-overlays. */ - public static final int PRIORITY_OVERLAY = 1000000; + int PRIORITY_OVERLAY = 1000000; /** * Default priority level of {@link OnBackInvokedCallback}s. */ - public static final int PRIORITY_DEFAULT = 0; + int PRIORITY_DEFAULT = 0; + + /** + * Priority level of {@link OnBackInvokedCallback}s registered by the system. + * + * System back animation will play when the callback to receive dispatch has this priority. + * @hide + */ + int PRIORITY_SYSTEM = -1; /** * Registers a {@link OnBackInvokedCallback}. @@ -69,10 +98,11 @@ public abstract class OnBackInvokedDispatcher { * registered, the existing instance (no matter its priority) will be * unregistered and registered again. * @param priority The priority of the callback. + * @throws {@link IllegalArgumentException} if the priority is negative. */ @SuppressLint("SamShouldBeLast") - public abstract void registerOnBackInvokedCallback( - @NonNull OnBackInvokedCallback callback, @Priority int priority); + void registerOnBackInvokedCallback( + @NonNull OnBackInvokedCallback callback, @Priority @IntRange(from = 0) int priority); /** * Unregisters a {@link OnBackInvokedCallback}. @@ -80,5 +110,20 @@ public abstract class OnBackInvokedDispatcher { * @param callback The callback to be unregistered. Does nothing if the callback has not been * registered. */ - public abstract void unregisterOnBackInvokedCallback(@NonNull OnBackInvokedCallback callback); + void unregisterOnBackInvokedCallback(@NonNull OnBackInvokedCallback callback); + + /** + * Returns the most prioritized callback to receive back dispatch next. + * @hide + */ + @Nullable + default OnBackInvokedCallback getTopCallback() { + return null; + } + + /** + * Registers a {@link OnBackInvokedCallback} with system priority. + * @hide + */ + default void registerSystemOnBackInvokedCallback(@NonNull OnBackInvokedCallback callback) { } } diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java index 3d8653554efd5..ee4a934482dbd 100644 --- a/core/java/android/view/ViewRootImpl.java +++ b/core/java/android/view/ViewRootImpl.java @@ -317,6 +317,11 @@ public final class ViewRootImpl implements ViewParent, */ private final WindowOnBackInvokedDispatcher mOnBackInvokedDispatcher = new WindowOnBackInvokedDispatcher(); + /** + * Compatibility {@link OnBackInvokedCallback} that dispatches KEYCODE_BACK events + * to view root for apps using legacy back behavior. + */ + private OnBackInvokedCallback mCompatOnBackInvokedCallback; /** * Callback for notifying about global configuration changes. @@ -1190,6 +1195,14 @@ public final class ViewRootImpl implements ViewParent, mTmpFrames.displayFrame, mTempRect2, mTmpFrames.frame); setFrame(mTmpFrames.frame); registerBackCallbackOnWindow(); + if (WindowOnBackInvokedDispatcher.shouldUseLegacyBack()) { + // For apps requesting legacy back behavior, we add a compat callback that + // dispatches {@link KeyEvent#KEYCODE_BACK} to their root views. + // This way from system point of view, these apps are providing custom + // {@link OnBackInvokedCallback}s, and will not play system back animations + // for them. + registerCompatOnBackInvokedCallback(); + } if (DEBUG_LAYOUT) Log.v(mTag, "Added window " + mWindow); if (res < WindowManagerGlobal.ADD_OKAY) { mAttachInfo.mRootView = null; @@ -5931,7 +5944,7 @@ public final class ViewRootImpl implements ViewParent, } } - private boolean isBack(InputEvent event) { + boolean isBack(InputEvent event) { if (event instanceof KeyEvent) { return ((KeyEvent) event).getKeyCode() == KeyEvent.KEYCODE_BACK; } else { @@ -6471,6 +6484,19 @@ public final class ViewRootImpl implements ViewParent, return FINISH_NOT_HANDLED; } + if (isBack(event) + && mContext != null + && !WindowOnBackInvokedDispatcher.shouldUseLegacyBack()) { + // Invoke the appropriate {@link OnBackInvokedCallback} if the new back + // navigation should be used, and the key event is not handled by anything else. + OnBackInvokedCallback topCallback = + getOnBackInvokedDispatcher().getTopCallback(); + if (topCallback != null) { + topCallback.onBackInvoked(); + return FINISH_HANDLED; + } + } + // This dispatch is for windows that don't have a Window.Callback. Otherwise, // the Window.Callback usually will have already called this (see // DecorView.superDispatchKeyEvent) leaving this call a no-op. @@ -8414,6 +8440,7 @@ public final class ViewRootImpl implements ViewParent, mAdded = false; } + unregisterCompatOnBackInvokedCallback(); mOnBackInvokedDispatcher.detachFromWindow(); WindowManagerGlobal.getInstance().doRemoveView(this); } @@ -10782,6 +10809,38 @@ public final class ViewRootImpl implements ViewParent, mOnBackInvokedDispatcher.attachToWindow(mWindowSession, mWindow); } + private void sendBackKeyEvent(int action) { + long when = SystemClock.uptimeMillis(); + final KeyEvent ev = new KeyEvent(when, when, action, + KeyEvent.KEYCODE_BACK, 0 /* repeat */, 0 /* metaState */, + KeyCharacterMap.VIRTUAL_KEYBOARD, 0 /* scancode */, + KeyEvent.FLAG_FROM_SYSTEM | KeyEvent.FLAG_VIRTUAL_HARD_KEY, + InputDevice.SOURCE_KEYBOARD); + + ev.setDisplayId(mContext.getDisplay().getDisplayId()); + if (mView != null) { + mView.dispatchKeyEvent(ev); + } + } + + private void registerCompatOnBackInvokedCallback() { + mCompatOnBackInvokedCallback = new OnBackInvokedCallback() { + @Override + public void onBackInvoked() { + sendBackKeyEvent(KeyEvent.ACTION_DOWN); + sendBackKeyEvent(KeyEvent.ACTION_UP); + } + }; + mOnBackInvokedDispatcher.registerOnBackInvokedCallback( + mCompatOnBackInvokedCallback, OnBackInvokedDispatcher.PRIORITY_DEFAULT); + } + + private void unregisterCompatOnBackInvokedCallback() { + if (mCompatOnBackInvokedCallback != null) { + mOnBackInvokedDispatcher.unregisterOnBackInvokedCallback(mCompatOnBackInvokedCallback); + } + } + @Override public void setTouchableRegion(Region r) { if (r != null) { diff --git a/core/java/android/window/ProxyOnBackInvokedDispatcher.java b/core/java/android/window/ProxyOnBackInvokedDispatcher.java index 509bbd4de389d..4de977a91d05b 100644 --- a/core/java/android/window/ProxyOnBackInvokedDispatcher.java +++ b/core/java/android/window/ProxyOnBackInvokedDispatcher.java @@ -42,7 +42,7 @@ import java.util.List; * * @hide */ -public class ProxyOnBackInvokedDispatcher extends OnBackInvokedDispatcher { +public class ProxyOnBackInvokedDispatcher implements OnBackInvokedDispatcher { /** * List of pair representing an {@link OnBackInvokedCallback} and its associated priority. @@ -60,14 +60,16 @@ public class ProxyOnBackInvokedDispatcher extends OnBackInvokedDispatcher { Log.v(TAG, String.format("Pending register %s. Actual=%s", callback, mActualDispatcherOwner)); } - synchronized (mLock) { - mCallbacks.add(Pair.create(callback, priority)); - if (mActualDispatcherOwner != null) { - mActualDispatcherOwner.getOnBackInvokedDispatcher().registerOnBackInvokedCallback( - callback, priority); - } - + if (priority < 0) { + throw new IllegalArgumentException("Application registered OnBackInvokedCallback " + + "cannot have negative priority. Priority: " + priority); } + registerOnBackInvokedCallbackUnchecked(callback, priority); + } + + @Override + public void registerSystemOnBackInvokedCallback(@NonNull OnBackInvokedCallback callback) { + registerOnBackInvokedCallbackUnchecked(callback, PRIORITY_SYSTEM); } @Override @@ -82,6 +84,17 @@ public class ProxyOnBackInvokedDispatcher extends OnBackInvokedDispatcher { } } + private void registerOnBackInvokedCallbackUnchecked( + @NonNull OnBackInvokedCallback callback, int priority) { + synchronized (mLock) { + mCallbacks.add(Pair.create(callback, priority)); + if (mActualDispatcherOwner != null) { + mActualDispatcherOwner.getOnBackInvokedDispatcher().registerOnBackInvokedCallback( + callback, priority); + } + } + } + /** * Transfers all the pending callbacks to the provided dispatcher. *

@@ -99,8 +112,12 @@ public class ProxyOnBackInvokedDispatcher extends OnBackInvokedDispatcher { dispatcher)); } for (Pair callbackPair : mCallbacks) { - dispatcher.registerOnBackInvokedCallback(callbackPair.first, - callbackPair.second); + int priority = callbackPair.second; + if (priority >= 0) { + dispatcher.registerOnBackInvokedCallback(callbackPair.first, priority); + } else { + dispatcher.registerSystemOnBackInvokedCallback(callbackPair.first); + } } mCallbacks.clear(); } diff --git a/core/java/android/window/WindowOnBackInvokedDispatcher.java b/core/java/android/window/WindowOnBackInvokedDispatcher.java index 29786046e49cb..d37d3b42872f7 100644 --- a/core/java/android/window/WindowOnBackInvokedDispatcher.java +++ b/core/java/android/window/WindowOnBackInvokedDispatcher.java @@ -18,8 +18,10 @@ package android.window; import android.annotation.NonNull; import android.annotation.Nullable; +import android.app.compat.CompatChanges; import android.os.Handler; import android.os.RemoteException; +import android.os.SystemProperties; import android.util.Log; import android.view.IWindow; import android.view.IWindowSession; @@ -31,7 +33,7 @@ import java.util.HashMap; import java.util.TreeMap; /** - * Provides window based implementation of {@link OnBackInvokedDispatcher}. + * Provides window based implementation of {@link android.view.OnBackInvokedDispatcher}. * * Callbacks with higher priorities receive back dispatching first. * Within the same priority, callbacks receive back dispatching in the reverse order @@ -39,16 +41,19 @@ import java.util.TreeMap; * * When the top priority callback is updated, the new callback is propagated to the Window Manager * if the window the instance is associated with has been attached. It is allowed to register / - * unregister {@link OnBackInvokedCallback}s before the window is attached, although callbacks - * will not receive dispatches until window attachment. + * unregister {@link android.view.OnBackInvokedCallback}s before the window is attached, although + * callbacks will not receive dispatches until window attachment. * * @hide */ -public class WindowOnBackInvokedDispatcher extends OnBackInvokedDispatcher { +public class WindowOnBackInvokedDispatcher implements OnBackInvokedDispatcher { private IWindowSession mWindowSession; private IWindow mWindow; private static final String TAG = "WindowOnBackDispatcher"; private static final boolean DEBUG = false; + private static final String BACK_PREDICTABILITY_PROP = "persist.debug.back_predictability"; + private static final boolean IS_BACK_PREDICTABILITY_ENABLED = SystemProperties + .getInt(BACK_PREDICTABILITY_PROP, 0) > 0; /** The currently most prioritized callback. */ @Nullable @@ -82,6 +87,15 @@ public class WindowOnBackInvokedDispatcher extends OnBackInvokedDispatcher { @Override public void registerOnBackInvokedCallback( @NonNull OnBackInvokedCallback callback, @Priority int priority) { + if (priority < 0) { + throw new IllegalArgumentException("Application registered OnBackInvokedCallback " + + "cannot have negative priority. Priority: " + priority); + } + registerOnBackInvokedCallbackUnchecked(callback, priority); + } + + private void registerOnBackInvokedCallbackUnchecked( + @NonNull OnBackInvokedCallback callback, @Priority int priority) { if (!mOnBackInvokedCallbacks.containsKey(priority)) { mOnBackInvokedCallbacks.put(priority, new ArrayList<>()); } @@ -120,6 +134,11 @@ public class WindowOnBackInvokedDispatcher extends OnBackInvokedDispatcher { } } + @Override + public void registerSystemOnBackInvokedCallback(@NonNull OnBackInvokedCallback callback) { + registerOnBackInvokedCallbackUnchecked(callback, OnBackInvokedDispatcher.PRIORITY_SYSTEM); + } + /** Clears all registered callbacks on the instance. */ public void clear() { mAllCallbacks.clear(); @@ -198,4 +217,21 @@ public class WindowOnBackInvokedDispatcher extends OnBackInvokedDispatcher { Handler.getMain().post(() -> mCallback.onBackInvoked()); } } + + @Override + public OnBackInvokedCallback getTopCallback() { + return mTopCallback == null ? null : mTopCallback.getCallback(); + } + + /** + * Returns if the legacy back behavior should be used. + * + * Legacy back behavior dispatches KEYCODE_BACK instead of invoking the application registered + * {@link android.view.OnBackInvokedCallback}. + * + */ + public static boolean shouldUseLegacyBack() { + return !CompatChanges.isChangeEnabled(DISPATCH_BACK_INVOCATION_AHEAD_OF_TIME) + || !IS_BACK_PREDICTABILITY_ENABLED; + } }