/* * 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 android.annotation.NonNull; import android.annotation.Nullable; import android.app.Activity; import android.content.Context; import android.content.ContextWrapper; import android.content.pm.ActivityInfo; import android.content.pm.ApplicationInfo; import android.os.Handler; import android.os.RemoteException; import android.os.SystemProperties; import android.text.TextUtils; import android.util.Log; import android.view.IWindow; import android.view.IWindowSession; import androidx.annotation.VisibleForTesting; import java.io.PrintWriter; import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.HashMap; import java.util.Objects; import java.util.TreeMap; /** * Provides window based implementation of {@link OnBackInvokedDispatcher}. *

* Callbacks with higher priorities receive back dispatching first. * Within the same priority, callbacks receive back dispatching in the reverse order * in which they are added. *

* 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. * * @hide */ public class WindowOnBackInvokedDispatcher implements OnBackInvokedDispatcher { private IWindowSession mWindowSession; private IWindow mWindow; private static final String TAG = "WindowOnBackDispatcher"; private static final boolean ENABLE_PREDICTIVE_BACK = SystemProperties .getInt("persist.wm.debug.predictive_back", 1) != 0; private static final boolean ALWAYS_ENFORCE_PREDICTIVE_BACK = SystemProperties .getInt("persist.wm.debug.predictive_back_always_enforce", 0) != 0; @Nullable private ImeOnBackInvokedDispatcher mImeDispatcher; /** Convenience hashmap to quickly decide if a callback has been added. */ private final HashMap mAllCallbacks = new HashMap<>(); /** Holds all callbacks by priorities. */ @VisibleForTesting public final TreeMap> mOnBackInvokedCallbacks = new TreeMap<>(); private Checker mChecker; public WindowOnBackInvokedDispatcher(@NonNull Context context) { mChecker = new Checker(context); } /** * Sends the pending top callback (if one exists) to WM when the view root * is attached a window. */ public void attachToWindow(@NonNull IWindowSession windowSession, @NonNull IWindow window) { mWindowSession = windowSession; mWindow = window; if (!mAllCallbacks.isEmpty()) { setTopOnBackInvokedCallback(getTopCallback()); } } /** Detaches the dispatcher instance from its window. */ public void detachFromWindow() { clear(); mWindow = null; mWindowSession = null; } // TODO: Take an Executor for the callback to run on. @Override public void registerOnBackInvokedCallback( @Priority int priority, @NonNull OnBackInvokedCallback callback) { if (mChecker.checkApplicationCallbackRegistration(priority, callback)) { registerOnBackInvokedCallbackUnchecked(callback, priority); } } /** * Register a callback bypassing platform checks. This is used to register compatibility * callbacks. */ public void registerOnBackInvokedCallbackUnchecked( @NonNull OnBackInvokedCallback callback, @Priority int priority) { if (mImeDispatcher != null) { mImeDispatcher.registerOnBackInvokedCallback(priority, callback); return; } 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); } OnBackInvokedCallback previousTopCallback = getTopCallback(); callbacks.add(callback); mAllCallbacks.put(callback, priority); if (previousTopCallback == null || (previousTopCallback != callback && mAllCallbacks.get(previousTopCallback) <= priority)) { setTopOnBackInvokedCallback(callback); } } @Override public void unregisterOnBackInvokedCallback(@NonNull OnBackInvokedCallback callback) { if (mImeDispatcher != null) { mImeDispatcher.unregisterOnBackInvokedCallback(callback); return; } if (!mAllCallbacks.containsKey(callback)) { if (DEBUG) { Log.i(TAG, "Callback not found. returning..."); } return; } OnBackInvokedCallback previousTopCallback = getTopCallback(); Integer priority = mAllCallbacks.get(callback); ArrayList callbacks = mOnBackInvokedCallbacks.get(priority); callbacks.remove(callback); if (callbacks.isEmpty()) { mOnBackInvokedCallbacks.remove(priority); } mAllCallbacks.remove(callback); // Re-populate the top callback to WM if the removed callback was previously the top one. if (previousTopCallback == callback) { // We should call onBackCancelled() when an active callback is removed from dispatcher. sendCancelledIfInProgress(callback); setTopOnBackInvokedCallback(getTopCallback()); } } private void sendCancelledIfInProgress(@NonNull OnBackInvokedCallback callback) { boolean isInProgress = mProgressAnimator.isBackAnimationInProgress(); if (isInProgress && callback instanceof OnBackAnimationCallback) { OnBackAnimationCallback animatedCallback = (OnBackAnimationCallback) callback; animatedCallback.onBackCancelled(); if (DEBUG) { Log.d(TAG, "sendCancelIfRunning: callback canceled"); } } else { Log.w(TAG, "sendCancelIfRunning: isInProgress=" + isInProgress + "callback=" + callback); } } @Override public void registerSystemOnBackInvokedCallback(@NonNull OnBackInvokedCallback callback) { registerOnBackInvokedCallbackUnchecked(callback, OnBackInvokedDispatcher.PRIORITY_SYSTEM); } /** Clears all registered callbacks on the instance. */ public void clear() { if (mImeDispatcher != null) { mImeDispatcher.clear(); mImeDispatcher = null; } if (!mAllCallbacks.isEmpty()) { OnBackInvokedCallback topCallback = getTopCallback(); if (topCallback != null) { sendCancelledIfInProgress(topCallback); } else { // Should not be possible Log.e(TAG, "There is no topCallback, even if mAllCallbacks is not empty"); } // Clear binder references in WM. setTopOnBackInvokedCallback(null); } // We should also stop running animations since all callbacks have been removed. // note: mSpring.skipToEnd(), in ProgressAnimator.reset(), requires the main handler. Handler.getMain().post(mProgressAnimator::reset); mAllCallbacks.clear(); mOnBackInvokedCallbacks.clear(); } private void setTopOnBackInvokedCallback(@Nullable OnBackInvokedCallback callback) { if (mWindowSession == null || mWindow == null) { return; } try { OnBackInvokedCallbackInfo callbackInfo = null; if (callback != null) { int priority = mAllCallbacks.get(callback); final IOnBackInvokedCallback iCallback = callback instanceof ImeOnBackInvokedDispatcher .ImeOnBackInvokedCallback ? ((ImeOnBackInvokedDispatcher.ImeOnBackInvokedCallback) callback).getIOnBackInvokedCallback() : new OnBackInvokedCallbackWrapper(callback); callbackInfo = new OnBackInvokedCallbackInfo( iCallback, priority, callback instanceof OnBackAnimationCallback); } mWindowSession.setOnBackInvokedCallbackInfo(mWindow, callbackInfo); } catch (RemoteException e) { Log.e(TAG, "Failed to set OnBackInvokedCallback to WM. Error: " + e); } } public OnBackInvokedCallback getTopCallback() { if (mAllCallbacks.isEmpty()) { return null; } for (Integer priority : mOnBackInvokedCallbacks.descendingKeySet()) { ArrayList callbacks = mOnBackInvokedCallbacks.get(priority); if (!callbacks.isEmpty()) { return callbacks.get(callbacks.size() - 1); } } return null; } @NonNull private static final BackProgressAnimator mProgressAnimator = new BackProgressAnimator(); /** * The {@link Context} in ViewRootImp and Activity could be different, this will make sure it * could update the checker condition base on the real context when binding the proxy * dispatcher in PhoneWindow. */ public void updateContext(@NonNull Context context) { mChecker = new Checker(context); } /** * Returns false if the legacy back behavior should be used. */ public boolean isOnBackInvokedCallbackEnabled() { return Checker.isOnBackInvokedCallbackEnabled(mChecker.getContext()); } /** * Dump information about this WindowOnBackInvokedDispatcher * @param prefix the prefix that will be prepended to each line of the produced output * @param writer the writer that will receive the resulting text */ public void dump(String prefix, PrintWriter writer) { String innerPrefix = prefix + " "; writer.println(prefix + "WindowOnBackDispatcher:"); if (mAllCallbacks.isEmpty()) { writer.println(prefix + ""); return; } writer.println(innerPrefix + "Top Callback: " + getTopCallback()); writer.println(innerPrefix + "Callbacks: "); mAllCallbacks.forEach((callback, priority) -> { writer.println(innerPrefix + " Callback: " + callback + " Priority=" + priority); }); } static class OnBackInvokedCallbackWrapper extends IOnBackInvokedCallback.Stub { static class CallbackRef { final WeakReference mWeakRef; final OnBackInvokedCallback mStrongRef; CallbackRef(@NonNull OnBackInvokedCallback callback, boolean useWeakRef) { if (useWeakRef) { mWeakRef = new WeakReference<>(callback); mStrongRef = null; } else { mStrongRef = callback; mWeakRef = null; } } OnBackInvokedCallback get() { if (mStrongRef != null) { return mStrongRef; } return mWeakRef.get(); } } final CallbackRef mCallbackRef; OnBackInvokedCallbackWrapper(@NonNull OnBackInvokedCallback callback) { mCallbackRef = new CallbackRef(callback, true /* useWeakRef */); } OnBackInvokedCallbackWrapper(@NonNull OnBackInvokedCallback callback, boolean useWeakRef) { mCallbackRef = new CallbackRef(callback, useWeakRef); } @Override public void onBackStarted(BackMotionEvent backEvent) { Handler.getMain().post(() -> { final OnBackAnimationCallback callback = getBackAnimationCallback(); if (callback != null) { mProgressAnimator.onBackStarted(backEvent, event -> callback.onBackProgressed(event)); callback.onBackStarted(new BackEvent( backEvent.getTouchX(), backEvent.getTouchY(), backEvent.getProgress(), backEvent.getSwipeEdge())); } }); } @Override public void onBackProgressed(BackMotionEvent backEvent) { Handler.getMain().post(() -> { final OnBackAnimationCallback callback = getBackAnimationCallback(); if (callback != null) { mProgressAnimator.onBackProgressed(backEvent); } }); } @Override public void onBackCancelled() { Handler.getMain().post(() -> { mProgressAnimator.onBackCancelled(() -> { final OnBackAnimationCallback callback = getBackAnimationCallback(); if (callback != null) { callback.onBackCancelled(); } }); }); } @Override public void onBackInvoked() throws RemoteException { Handler.getMain().post(() -> { boolean isInProgress = mProgressAnimator.isBackAnimationInProgress(); mProgressAnimator.reset(); final OnBackInvokedCallback callback = mCallbackRef.get(); if (callback == null) { Log.d(TAG, "Trying to call onBackInvoked() on a null callback reference."); return; } if (callback instanceof OnBackAnimationCallback && !isInProgress) { Log.w(TAG, "ProgressAnimator was not in progress, skip onBackInvoked()."); return; } callback.onBackInvoked(); }); } @Nullable private OnBackAnimationCallback getBackAnimationCallback() { OnBackInvokedCallback callback = mCallbackRef.get(); return callback instanceof OnBackAnimationCallback ? (OnBackAnimationCallback) callback : null; } } /** * Returns false if the legacy back behavior should be used. *

* Legacy back behavior dispatches KEYCODE_BACK instead of invoking the application registered * {@link OnBackInvokedCallback}. */ public static boolean isOnBackInvokedCallbackEnabled(@NonNull Context context) { return Checker.isOnBackInvokedCallbackEnabled(context); } @Override public void setImeOnBackInvokedDispatcher( @NonNull ImeOnBackInvokedDispatcher imeDispatcher) { mImeDispatcher = imeDispatcher; } /** Returns true if a non-null {@link ImeOnBackInvokedDispatcher} has been set. **/ public boolean hasImeOnBackInvokedDispatcher() { return mImeDispatcher != null; } /** * Class used to check whether a callback can be registered or not. This is meant to be * shared with {@link ProxyOnBackInvokedDispatcher} which needs to do the same checks. */ public static class Checker { private WeakReference mContext; public Checker(@NonNull Context context) { mContext = new WeakReference<>(context); } /** * Checks whether the given callback can be registered with the given priority. * @return true if the callback can be added. * @throws IllegalArgumentException if the priority is negative. */ public boolean checkApplicationCallbackRegistration(int priority, OnBackInvokedCallback callback) { if (!isOnBackInvokedCallbackEnabled(getContext()) && !(callback instanceof CompatOnBackInvokedCallback)) { Log.w(TAG, "OnBackInvokedCallback is not enabled for the application." + "\nSet 'android:enableOnBackInvokedCallback=\"true\"' in the" + " application manifest."); return false; } if (priority < 0) { throw new IllegalArgumentException("Application registered OnBackInvokedCallback " + "cannot have negative priority. Priority: " + priority); } Objects.requireNonNull(callback); return true; } private Context getContext() { return mContext.get(); } private static boolean isOnBackInvokedCallbackEnabled(@Nullable Context context) { // new back is enabled if the feature flag is enabled AND the app does not explicitly // request legacy back. boolean featureFlagEnabled = ENABLE_PREDICTIVE_BACK; if (!featureFlagEnabled) { return false; } if (ALWAYS_ENFORCE_PREDICTIVE_BACK) { return true; } // If the context is null, return false to use legacy back. if (context == null) { Log.w(TAG, "OnBackInvokedCallback is not enabled because context is null."); return false; } boolean requestsPredictiveBack = false; // Check if the context is from an activity. while ((context instanceof ContextWrapper) && !(context instanceof Activity)) { context = ((ContextWrapper) context).getBaseContext(); } boolean shouldCheckActivity = false; if (context instanceof Activity) { final Activity activity = (Activity) context; final ActivityInfo activityInfo = activity.getActivityInfo(); if (activityInfo != null) { if (activityInfo.hasOnBackInvokedCallbackEnabled()) { shouldCheckActivity = true; requestsPredictiveBack = activityInfo.isOnBackInvokedCallbackEnabled(); if (DEBUG) { Log.d(TAG, TextUtils.formatSimple( "Activity: %s isPredictiveBackEnabled=%s", activity.getComponentName(), requestsPredictiveBack)); } } } else { Log.w(TAG, "The ActivityInfo is null, so we cannot verify if this Activity" + " has the 'android:enableOnBackInvokedCallback' attribute." + " The application attribute will be used as a fallback."); } } if (!shouldCheckActivity) { final ApplicationInfo applicationInfo = context.getApplicationInfo(); requestsPredictiveBack = applicationInfo.isOnBackInvokedCallbackEnabled(); if (DEBUG) { Log.d(TAG, TextUtils.formatSimple("App: %s requestsPredictiveBack=%s", applicationInfo.packageName, requestsPredictiveBack)); } } return requestsPredictiveBack; } } }