Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 7f10e298 authored by Shan Huang's avatar Shan Huang Committed by Android (Google) Code Review
Browse files

Merge "Implement OnBackInvokedDispatcher"

parents 06a55ff1 54b9c9d6
Loading
Loading
Loading
Loading
+9 −0
Original line number Diff line number Diff line
@@ -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;

@@ -333,4 +334,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);
}
+5 −0
Original line number Diff line number Diff line
@@ -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;
@@ -500,6 +501,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;
+55 −0
Original line number Diff line number Diff line
/*
 * 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();
}
+132 −6
Original line number Diff line number Diff line
@@ -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<OnBackInvokedCallback, Integer> mAllCallbacks = new HashMap<>();
    /** Holds all callbacks by priorities. */
    private final TreeMap<Integer, ArrayList<OnBackInvokedCallback>>
            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<OnBackInvokedCallback> 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<OnBackInvokedCallback> 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());
        }
    }
}
+154 −0
Original line number Diff line number Diff line
/*
 * 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}
 *
 * <p>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<IOnBackInvokedCallback> 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<IOnBackInvokedCallback> 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<IOnBackInvokedCallback> 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();
    }
}
Loading