From 54b9c9d66eb6a31fd045a6a5d87ef2c22165ab6d Mon Sep 17 00:00:00 2001 From: Shan Huang Date: Tue, 11 Jan 2022 03:17:12 +0000 Subject: [PATCH] Implement OnBackInvokedDispatcher Bug:209867448 Test: mp core services sysuig. atest FrameworksCoreTests:WindowOnBackInvokedDispatcherTest Change-Id: Ib74250e93926854f07ebba6b9f680040aa98f9a4 --- core/java/android/view/IWindowSession.aidl | 9 + .../android/view/WindowlessWindowManager.java | 5 + .../window/IOnBackInvokedCallback.aidl | 55 +++++++ .../window/WindowOnBackInvokedDispatcher.java | 138 +++++++++++++++- .../WindowOnBackInvokedDispatcherTest.java | 154 ++++++++++++++++++ .../java/com/android/server/wm/Session.java | 7 + 6 files changed, 362 insertions(+), 6 deletions(-) create mode 100644 core/java/android/window/IOnBackInvokedCallback.aidl create mode 100644 core/tests/coretests/src/android/window/WindowOnBackInvokedDispatcherTest.java diff --git a/core/java/android/view/IWindowSession.aidl b/core/java/android/view/IWindowSession.aidl index 921ce53b3bda9..d84cca99c8f15 100644 --- a/core/java/android/view/IWindowSession.aidl +++ b/core/java/android/view/IWindowSession.aidl @@ -37,6 +37,7 @@ import android.view.Surface; import android.view.SurfaceControl; import android.view.SurfaceControl.Transaction; import android.window.ClientWindowFrames; +import android.window.IOnBackInvokedCallback; import java.util.List; @@ -328,4 +329,12 @@ interface IWindowSession { */ oneway void generateDisplayHash(IWindow window, in Rect boundsInWindow, in String hashAlgorithm, in RemoteCallback callback); + + /** + * Sets the {@link IOnBackInvokedCallback} to be invoked for a window when back is triggered. + * + * @param window The token for the window to set the callback to. + * @param callback The {@link IOnBackInvokedCallback} to set. + */ + oneway void setOnBackInvokedCallback(IWindow window, IOnBackInvokedCallback callback); } diff --git a/core/java/android/view/WindowlessWindowManager.java b/core/java/android/view/WindowlessWindowManager.java index 5176f9bb0f939..6c74da5025a95 100644 --- a/core/java/android/view/WindowlessWindowManager.java +++ b/core/java/android/view/WindowlessWindowManager.java @@ -28,6 +28,7 @@ import android.os.RemoteException; import android.util.Log; import android.util.MergedConfiguration; import android.window.ClientWindowFrames; +import android.window.IOnBackInvokedCallback; import java.util.HashMap; import java.util.Objects; @@ -495,6 +496,10 @@ public class WindowlessWindowManager implements IWindowSession { RemoteCallback callback) { } + @Override + public void setOnBackInvokedCallback(IWindow iWindow, + IOnBackInvokedCallback iOnBackInvokedCallback) throws RemoteException { } + @Override public boolean dropForAccessibility(IWindow window, int x, int y) { return false; diff --git a/core/java/android/window/IOnBackInvokedCallback.aidl b/core/java/android/window/IOnBackInvokedCallback.aidl new file mode 100644 index 0000000000000..a42863c3126fe --- /dev/null +++ b/core/java/android/window/IOnBackInvokedCallback.aidl @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2021 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 android.window; + +/** + * Interface that wraps a {@link OnBackInvokedCallback} object, to be stored in window manager + * and called from back handling process when back is invoked. + * + * @hide + */ +oneway interface IOnBackInvokedCallback { + /** + * Called when a back gesture has been started, or back button has been pressed down. + * Wraps {@link OnBackInvokedCallback#onBackStarted()}. + */ + void onBackStarted(); + + /** + * Called on back gesture progress. + * Wraps {@link OnBackInvokedCallback#onBackProgressed()}. + * + * @param touchX Absolute X location of the touch point. + * @param touchY Absolute Y location of the touch point. + * @param progress Value between 0 and 1 on how far along the back gesture is. + */ + void onBackProgressed(int touchX, int touchY, float progress); + + /** + * Called when a back gesture or back button press has been cancelled. + * Wraps {@link OnBackInvokedCallback#onBackCancelled()}. + */ + void onBackCancelled(); + + /** + * Called when a back gesture has been completed and committed, or back button pressed + * has been released and committed. + * Wraps {@link OnBackInvokedCallback#onBackInvoked()}. + */ + void onBackInvoked(); +} diff --git a/core/java/android/window/WindowOnBackInvokedDispatcher.java b/core/java/android/window/WindowOnBackInvokedDispatcher.java index 53734eb887204..29786046e49cb 100644 --- a/core/java/android/window/WindowOnBackInvokedDispatcher.java +++ b/core/java/android/window/WindowOnBackInvokedDispatcher.java @@ -17,11 +17,19 @@ package android.window; import android.annotation.NonNull; +import android.annotation.Nullable; +import android.os.Handler; +import android.os.RemoteException; +import android.util.Log; import android.view.IWindow; import android.view.IWindowSession; import android.view.OnBackInvokedCallback; import android.view.OnBackInvokedDispatcher; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.TreeMap; + /** * Provides window based implementation of {@link OnBackInvokedDispatcher}. * @@ -39,6 +47,18 @@ import android.view.OnBackInvokedDispatcher; public class WindowOnBackInvokedDispatcher extends OnBackInvokedDispatcher { private IWindowSession mWindowSession; private IWindow mWindow; + private static final String TAG = "WindowOnBackDispatcher"; + private static final boolean DEBUG = false; + + /** The currently most prioritized callback. */ + @Nullable + private OnBackInvokedCallbackWrapper mTopCallback; + + /** Convenience hashmap to quickly decide if a callback has been added. */ + private final HashMap mAllCallbacks = new HashMap<>(); + /** Holds all callbacks by priorities. */ + private final TreeMap> + mOnBackInvokedCallbacks = new TreeMap<>(); /** * Sends the pending top callback (if one exists) to WM when the view root @@ -47,7 +67,9 @@ public class WindowOnBackInvokedDispatcher extends OnBackInvokedDispatcher { public void attachToWindow(@NonNull IWindowSession windowSession, @NonNull IWindow window) { mWindowSession = windowSession; mWindow = window; - // TODO(b/209867448): Send the top callback to WM (if one exists). + if (mTopCallback != null) { + setTopOnBackInvokedCallback(mTopCallback); + } } /** Detaches the dispatcher instance from its window. */ @@ -56,20 +78,124 @@ public class WindowOnBackInvokedDispatcher extends OnBackInvokedDispatcher { mWindowSession = null; } + // TODO: Take an Executor for the callback to run on. @Override public void registerOnBackInvokedCallback( @NonNull OnBackInvokedCallback callback, @Priority int priority) { - // TODO(b/209867448): To be implemented. + if (!mOnBackInvokedCallbacks.containsKey(priority)) { + mOnBackInvokedCallbacks.put(priority, new ArrayList<>()); + } + ArrayList callbacks = mOnBackInvokedCallbacks.get(priority); + + // If callback has already been added, remove it and re-add it. + if (mAllCallbacks.containsKey(callback)) { + if (DEBUG) { + Log.i(TAG, "Callback already added. Removing and re-adding it."); + } + Integer prevPriority = mAllCallbacks.get(callback); + mOnBackInvokedCallbacks.get(prevPriority).remove(callback); + } + + callbacks.add(callback); + mAllCallbacks.put(callback, priority); + if (mTopCallback == null || (mTopCallback.getCallback() != callback + && mAllCallbacks.get(mTopCallback.getCallback()) <= priority)) { + setTopOnBackInvokedCallback(new OnBackInvokedCallbackWrapper(callback, priority)); + } } @Override - public void unregisterOnBackInvokedCallback( - @NonNull OnBackInvokedCallback callback) { - // TODO(b/209867448): To be implemented. + public void unregisterOnBackInvokedCallback(@NonNull OnBackInvokedCallback callback) { + if (!mAllCallbacks.containsKey(callback)) { + if (DEBUG) { + Log.i(TAG, "Callback not found. returning..."); + } + return; + } + Integer priority = mAllCallbacks.get(callback); + mOnBackInvokedCallbacks.get(priority).remove(callback); + mAllCallbacks.remove(callback); + if (mTopCallback != null && mTopCallback.getCallback() == callback) { + findAndSetTopOnBackInvokedCallback(); + } } /** Clears all registered callbacks on the instance. */ public void clear() { - // TODO(b/209867448): To be implemented. + mAllCallbacks.clear(); + mTopCallback = null; + mOnBackInvokedCallbacks.clear(); + } + + /** + * Iterates through all callbacks to find the most prioritized one and pushes it to + * window manager. + */ + private void findAndSetTopOnBackInvokedCallback() { + if (mAllCallbacks.isEmpty()) { + setTopOnBackInvokedCallback(null); + return; + } + + for (Integer priority : mOnBackInvokedCallbacks.descendingKeySet()) { + ArrayList callbacks = mOnBackInvokedCallbacks.get(priority); + if (!callbacks.isEmpty()) { + OnBackInvokedCallbackWrapper callback = new OnBackInvokedCallbackWrapper( + callbacks.get(callbacks.size() - 1), priority); + setTopOnBackInvokedCallback(callback); + return; + } + } + setTopOnBackInvokedCallback(null); + } + + // Pushes the top priority callback to window manager. + private void setTopOnBackInvokedCallback(@Nullable OnBackInvokedCallbackWrapper callback) { + mTopCallback = callback; + if (mWindowSession == null || mWindow == null) { + return; + } + try { + mWindowSession.setOnBackInvokedCallback(mWindow, mTopCallback); + } catch (RemoteException e) { + Log.e(TAG, "Failed to set OnBackInvokedCallback to WM. Error: " + e); + } + } + + private class OnBackInvokedCallbackWrapper extends IOnBackInvokedCallback.Stub { + private final OnBackInvokedCallback mCallback; + private final @Priority int mPriority; + + OnBackInvokedCallbackWrapper( + @NonNull OnBackInvokedCallback callback, @Priority int priority) { + mCallback = callback; + mPriority = priority; + } + + @NonNull + public OnBackInvokedCallback getCallback() { + return mCallback; + } + + @Override + public void onBackStarted() throws RemoteException { + Handler.getMain().post(() -> mCallback.onBackStarted()); + } + + @Override + public void onBackProgressed(int touchX, int touchY, float progress) + throws RemoteException { + Handler.getMain().post(() -> mCallback.onBackProgressed(touchX, touchY, progress)); + } + + @Override + public void onBackCancelled() throws RemoteException { + Handler.getMain().post(() -> mCallback.onBackCancelled()); + } + + @Override + public void onBackInvoked() throws RemoteException { + Handler.getMain().post(() -> mCallback.onBackInvoked()); + } } } diff --git a/core/tests/coretests/src/android/window/WindowOnBackInvokedDispatcherTest.java b/core/tests/coretests/src/android/window/WindowOnBackInvokedDispatcherTest.java new file mode 100644 index 0000000000000..a1a1e20d6982c --- /dev/null +++ b/core/tests/coretests/src/android/window/WindowOnBackInvokedDispatcherTest.java @@ -0,0 +1,154 @@ +/* + * Copyright (C) 2021 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 android.window; + +import static org.mockito.ArgumentMatchers.isNull; +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.verifyZeroInteractions; + +import android.os.RemoteException; +import android.platform.test.annotations.Presubmit; +import android.view.IWindow; +import android.view.IWindowSession; +import android.view.OnBackInvokedCallback; +import android.view.OnBackInvokedDispatcher; + +import androidx.test.filters.SmallTest; +import androidx.test.platform.app.InstrumentationRegistry; +import androidx.test.runner.AndroidJUnit4; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; + +/** + * Tests for {@link WindowOnBackInvokedDispatcherTest} + * + *

Build/Install/Run: + * atest FrameworksCoreTests:WindowOnBackInvokedDispatcherTest + */ +@RunWith(AndroidJUnit4.class) +@SmallTest +@Presubmit +public class WindowOnBackInvokedDispatcherTest { + @Mock + private IWindowSession mWindowSession; + @Mock + private IWindow mWindow; + private WindowOnBackInvokedDispatcher mDispatcher; + @Mock + private OnBackInvokedCallback mCallback1; + @Mock + private OnBackInvokedCallback mCallback2; + + @Before + public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); + mDispatcher = new WindowOnBackInvokedDispatcher(); + mDispatcher.attachToWindow(mWindowSession, mWindow); + } + + private void waitForIdle() { + InstrumentationRegistry.getInstrumentation().waitForIdleSync(); + } + + @Test + public void propagatesTopCallback_samePriority() throws RemoteException { + ArgumentCaptor captor = + ArgumentCaptor.forClass(IOnBackInvokedCallback.class); + + mDispatcher.registerOnBackInvokedCallback( + mCallback1, OnBackInvokedDispatcher.PRIORITY_DEFAULT); + mDispatcher.registerOnBackInvokedCallback( + mCallback2, OnBackInvokedDispatcher.PRIORITY_DEFAULT); + + verify(mWindowSession, times(2)) + .setOnBackInvokedCallback(Mockito.eq(mWindow), captor.capture()); + captor.getAllValues().get(0).onBackStarted(); + waitForIdle(); + verify(mCallback1).onBackStarted(); + verifyZeroInteractions(mCallback2); + + captor.getAllValues().get(1).onBackStarted(); + waitForIdle(); + verify(mCallback2).onBackStarted(); + verifyNoMoreInteractions(mCallback1); + } + + @Test + public void propagatesTopCallback_differentPriority() throws RemoteException { + ArgumentCaptor captor = + ArgumentCaptor.forClass(IOnBackInvokedCallback.class); + + mDispatcher.registerOnBackInvokedCallback( + mCallback1, OnBackInvokedDispatcher.PRIORITY_OVERLAY); + mDispatcher.registerOnBackInvokedCallback( + mCallback2, OnBackInvokedDispatcher.PRIORITY_DEFAULT); + + verify(mWindowSession) + .setOnBackInvokedCallback(Mockito.eq(mWindow), captor.capture()); + verifyNoMoreInteractions(mWindowSession); + captor.getValue().onBackStarted(); + waitForIdle(); + verify(mCallback1).onBackStarted(); + } + + @Test + public void propagatesTopCallback_withRemoval() throws RemoteException { + mDispatcher.registerOnBackInvokedCallback( + mCallback1, OnBackInvokedDispatcher.PRIORITY_DEFAULT); + mDispatcher.registerOnBackInvokedCallback( + mCallback2, OnBackInvokedDispatcher.PRIORITY_DEFAULT); + + reset(mWindowSession); + mDispatcher.unregisterOnBackInvokedCallback(mCallback1); + verifyZeroInteractions(mWindowSession); + + mDispatcher.unregisterOnBackInvokedCallback(mCallback2); + verify(mWindowSession).setOnBackInvokedCallback(Mockito.eq(mWindow), isNull()); + } + + + @Test + public void propagatesTopCallback_sameInstanceAddedTwice() throws RemoteException { + ArgumentCaptor captor = + ArgumentCaptor.forClass(IOnBackInvokedCallback.class); + + mDispatcher.registerOnBackInvokedCallback(mCallback1, + OnBackInvokedDispatcher.PRIORITY_OVERLAY); + mDispatcher.registerOnBackInvokedCallback( + mCallback2, OnBackInvokedDispatcher.PRIORITY_DEFAULT); + mDispatcher.registerOnBackInvokedCallback( + mCallback1, OnBackInvokedDispatcher.PRIORITY_DEFAULT); + + reset(mWindowSession); + mDispatcher.registerOnBackInvokedCallback( + mCallback2, OnBackInvokedDispatcher.PRIORITY_OVERLAY); + verify(mWindowSession) + .setOnBackInvokedCallback(Mockito.eq(mWindow), captor.capture()); + captor.getValue().onBackStarted(); + waitForIdle(); + verify(mCallback2).onBackStarted(); + } +} diff --git a/services/core/java/com/android/server/wm/Session.java b/services/core/java/com/android/server/wm/Session.java index 005544b961c32..340053ac16216 100644 --- a/services/core/java/com/android/server/wm/Session.java +++ b/services/core/java/com/android/server/wm/Session.java @@ -73,6 +73,7 @@ import android.view.SurfaceControl; import android.view.SurfaceSession; import android.view.WindowManager; import android.window.ClientWindowFrames; +import android.window.IOnBackInvokedCallback; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.os.logging.MetricsLoggerWrapper; @@ -863,4 +864,10 @@ class Session extends IWindowSession.Stub implements IBinder.DeathRecipient { Binder.restoreCallingIdentity(origId); } } + + @Override + public void setOnBackInvokedCallback(IWindow iWindow, + IOnBackInvokedCallback iOnBackInvokedCallback) throws RemoteException { + // TODO: Set the callback to the WindowState of the window. + } }