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

Commit 54b9c9d6 authored by Shan Huang's avatar Shan Huang
Browse files

Implement OnBackInvokedDispatcher

Bug:209867448
Test: mp core services sysuig. atest FrameworksCoreTests:WindowOnBackInvokedDispatcherTest

Change-Id: Ib74250e93926854f07ebba6b9f680040aa98f9a4
parent da433c60
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;

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