1 /* 2 * Copyright (C) 2021 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package android.window; 18 19 import android.annotation.NonNull; 20 import android.annotation.Nullable; 21 import android.content.Context; 22 import android.os.Handler; 23 import android.os.RemoteException; 24 import android.os.SystemProperties; 25 import android.text.TextUtils; 26 import android.util.Log; 27 import android.view.IWindow; 28 import android.view.IWindowSession; 29 30 import java.io.PrintWriter; 31 import java.lang.ref.WeakReference; 32 import java.util.ArrayList; 33 import java.util.HashMap; 34 import java.util.Objects; 35 import java.util.TreeMap; 36 37 /** 38 * Provides window based implementation of {@link OnBackInvokedDispatcher}. 39 * <p> 40 * Callbacks with higher priorities receive back dispatching first. 41 * Within the same priority, callbacks receive back dispatching in the reverse order 42 * in which they are added. 43 * <p> 44 * When the top priority callback is updated, the new callback is propagated to the Window Manager 45 * if the window the instance is associated with has been attached. It is allowed to register / 46 * unregister {@link OnBackInvokedCallback}s before the window is attached, although 47 * callbacks will not receive dispatches until window attachment. 48 * 49 * @hide 50 */ 51 public class WindowOnBackInvokedDispatcher implements OnBackInvokedDispatcher { 52 private IWindowSession mWindowSession; 53 private IWindow mWindow; 54 private static final String TAG = "WindowOnBackDispatcher"; 55 private static final boolean ENABLE_PREDICTIVE_BACK = SystemProperties 56 .getInt("persist.wm.debug.predictive_back", 1) != 0; 57 private static final boolean ALWAYS_ENFORCE_PREDICTIVE_BACK = SystemProperties 58 .getInt("persist.wm.debug.predictive_back_always_enforce", 0) != 0; 59 @Nullable 60 private ImeOnBackInvokedDispatcher mImeDispatcher; 61 62 /** Convenience hashmap to quickly decide if a callback has been added. */ 63 private final HashMap<OnBackInvokedCallback, Integer> mAllCallbacks = new HashMap<>(); 64 /** Holds all callbacks by priorities. */ 65 private final TreeMap<Integer, ArrayList<OnBackInvokedCallback>> 66 mOnBackInvokedCallbacks = new TreeMap<>(); 67 private final Checker mChecker; 68 WindowOnBackInvokedDispatcher(boolean applicationCallBackEnabled)69 public WindowOnBackInvokedDispatcher(boolean applicationCallBackEnabled) { 70 mChecker = new Checker(applicationCallBackEnabled); 71 } 72 73 /** 74 * Sends the pending top callback (if one exists) to WM when the view root 75 * is attached a window. 76 */ attachToWindow(@onNull IWindowSession windowSession, @NonNull IWindow window)77 public void attachToWindow(@NonNull IWindowSession windowSession, @NonNull IWindow window) { 78 mWindowSession = windowSession; 79 mWindow = window; 80 if (!mAllCallbacks.isEmpty()) { 81 setTopOnBackInvokedCallback(getTopCallback()); 82 } 83 } 84 85 /** Detaches the dispatcher instance from its window. */ detachFromWindow()86 public void detachFromWindow() { 87 clear(); 88 mWindow = null; 89 mWindowSession = null; 90 } 91 92 // TODO: Take an Executor for the callback to run on. 93 @Override registerOnBackInvokedCallback( @riority int priority, @NonNull OnBackInvokedCallback callback)94 public void registerOnBackInvokedCallback( 95 @Priority int priority, @NonNull OnBackInvokedCallback callback) { 96 if (mChecker.checkApplicationCallbackRegistration(priority, callback)) { 97 registerOnBackInvokedCallbackUnchecked(callback, priority); 98 } 99 } 100 101 /** 102 * Register a callback bypassing platform checks. This is used to register compatibility 103 * callbacks. 104 */ registerOnBackInvokedCallbackUnchecked( @onNull OnBackInvokedCallback callback, @Priority int priority)105 public void registerOnBackInvokedCallbackUnchecked( 106 @NonNull OnBackInvokedCallback callback, @Priority int priority) { 107 if (mImeDispatcher != null) { 108 mImeDispatcher.registerOnBackInvokedCallback(priority, callback); 109 return; 110 } 111 if (!mOnBackInvokedCallbacks.containsKey(priority)) { 112 mOnBackInvokedCallbacks.put(priority, new ArrayList<>()); 113 } 114 ArrayList<OnBackInvokedCallback> callbacks = mOnBackInvokedCallbacks.get(priority); 115 116 // If callback has already been added, remove it and re-add it. 117 if (mAllCallbacks.containsKey(callback)) { 118 if (DEBUG) { 119 Log.i(TAG, "Callback already added. Removing and re-adding it."); 120 } 121 Integer prevPriority = mAllCallbacks.get(callback); 122 mOnBackInvokedCallbacks.get(prevPriority).remove(callback); 123 } 124 125 OnBackInvokedCallback previousTopCallback = getTopCallback(); 126 callbacks.add(callback); 127 mAllCallbacks.put(callback, priority); 128 if (previousTopCallback == null 129 || (previousTopCallback != callback 130 && mAllCallbacks.get(previousTopCallback) <= priority)) { 131 setTopOnBackInvokedCallback(callback); 132 } 133 } 134 135 @Override unregisterOnBackInvokedCallback(@onNull OnBackInvokedCallback callback)136 public void unregisterOnBackInvokedCallback(@NonNull OnBackInvokedCallback callback) { 137 if (mImeDispatcher != null) { 138 mImeDispatcher.unregisterOnBackInvokedCallback(callback); 139 return; 140 } 141 if (!mAllCallbacks.containsKey(callback)) { 142 if (DEBUG) { 143 Log.i(TAG, "Callback not found. returning..."); 144 } 145 return; 146 } 147 OnBackInvokedCallback previousTopCallback = getTopCallback(); 148 Integer priority = mAllCallbacks.get(callback); 149 ArrayList<OnBackInvokedCallback> callbacks = mOnBackInvokedCallbacks.get(priority); 150 callbacks.remove(callback); 151 if (callbacks.isEmpty()) { 152 mOnBackInvokedCallbacks.remove(priority); 153 } 154 mAllCallbacks.remove(callback); 155 // Re-populate the top callback to WM if the removed callback was previously the top one. 156 if (previousTopCallback == callback) { 157 setTopOnBackInvokedCallback(getTopCallback()); 158 } 159 } 160 161 @Override registerSystemOnBackInvokedCallback(@onNull OnBackInvokedCallback callback)162 public void registerSystemOnBackInvokedCallback(@NonNull OnBackInvokedCallback callback) { 163 registerOnBackInvokedCallbackUnchecked(callback, OnBackInvokedDispatcher.PRIORITY_SYSTEM); 164 } 165 166 /** Clears all registered callbacks on the instance. */ clear()167 public void clear() { 168 if (mImeDispatcher != null) { 169 mImeDispatcher.clear(); 170 mImeDispatcher = null; 171 } 172 if (!mAllCallbacks.isEmpty()) { 173 // Clear binder references in WM. 174 setTopOnBackInvokedCallback(null); 175 } 176 mAllCallbacks.clear(); 177 mOnBackInvokedCallbacks.clear(); 178 } 179 setTopOnBackInvokedCallback(@ullable OnBackInvokedCallback callback)180 private void setTopOnBackInvokedCallback(@Nullable OnBackInvokedCallback callback) { 181 if (mWindowSession == null || mWindow == null) { 182 return; 183 } 184 try { 185 OnBackInvokedCallbackInfo callbackInfo = null; 186 if (callback != null) { 187 int priority = mAllCallbacks.get(callback); 188 final IOnBackInvokedCallback iCallback = 189 callback instanceof ImeOnBackInvokedDispatcher 190 .ImeOnBackInvokedCallback 191 ? ((ImeOnBackInvokedDispatcher.ImeOnBackInvokedCallback) 192 callback).getIOnBackInvokedCallback() 193 : new OnBackInvokedCallbackWrapper(callback); 194 callbackInfo = new OnBackInvokedCallbackInfo(iCallback, priority); 195 } 196 mWindowSession.setOnBackInvokedCallbackInfo(mWindow, callbackInfo); 197 } catch (RemoteException e) { 198 Log.e(TAG, "Failed to set OnBackInvokedCallback to WM. Error: " + e); 199 } 200 } 201 getTopCallback()202 public OnBackInvokedCallback getTopCallback() { 203 if (mAllCallbacks.isEmpty()) { 204 return null; 205 } 206 for (Integer priority : mOnBackInvokedCallbacks.descendingKeySet()) { 207 ArrayList<OnBackInvokedCallback> callbacks = mOnBackInvokedCallbacks.get(priority); 208 if (!callbacks.isEmpty()) { 209 return callbacks.get(callbacks.size() - 1); 210 } 211 } 212 return null; 213 } 214 215 /** 216 * Returns the checker used to check whether a callback can be registered 217 */ 218 @NonNull getChecker()219 public Checker getChecker() { 220 return mChecker; 221 } 222 @NonNull 223 private static final BackProgressAnimator mProgressAnimator = new BackProgressAnimator(); 224 225 /** 226 * Dump information about this WindowOnBackInvokedDispatcher 227 * @param prefix the prefix that will be prepended to each line of the produced output 228 * @param writer the writer that will receive the resulting text 229 */ dump(String prefix, PrintWriter writer)230 public void dump(String prefix, PrintWriter writer) { 231 String innerPrefix = prefix + " "; 232 writer.println(prefix + "WindowOnBackDispatcher:"); 233 if (mAllCallbacks.isEmpty()) { 234 writer.println(prefix + "<None>"); 235 return; 236 } 237 238 writer.println(innerPrefix + "Top Callback: " + getTopCallback()); 239 writer.println(innerPrefix + "Callbacks: "); 240 mAllCallbacks.forEach((callback, priority) -> { 241 writer.println(innerPrefix + " Callback: " + callback + " Priority=" + priority); 242 }); 243 } 244 245 static class OnBackInvokedCallbackWrapper extends IOnBackInvokedCallback.Stub { 246 private final WeakReference<OnBackInvokedCallback> mCallback; 247 OnBackInvokedCallbackWrapper(@onNull OnBackInvokedCallback callback)248 OnBackInvokedCallbackWrapper(@NonNull OnBackInvokedCallback callback) { 249 mCallback = new WeakReference<>(callback); 250 } 251 252 @Override onBackStarted(BackMotionEvent backEvent)253 public void onBackStarted(BackMotionEvent backEvent) { 254 Handler.getMain().post(() -> { 255 final OnBackAnimationCallback callback = getBackAnimationCallback(); 256 if (callback != null) { 257 mProgressAnimator.onBackStarted(backEvent, event -> 258 callback.onBackProgressed(event)); 259 callback.onBackStarted(new BackEvent( 260 backEvent.getTouchX(), backEvent.getTouchY(), 261 backEvent.getProgress(), backEvent.getSwipeEdge())); 262 } 263 }); 264 } 265 266 @Override onBackProgressed(BackMotionEvent backEvent)267 public void onBackProgressed(BackMotionEvent backEvent) { 268 Handler.getMain().post(() -> { 269 final OnBackAnimationCallback callback = getBackAnimationCallback(); 270 if (callback != null) { 271 mProgressAnimator.onBackProgressed(backEvent); 272 } 273 }); 274 } 275 276 @Override onBackCancelled()277 public void onBackCancelled() { 278 Handler.getMain().post(() -> { 279 mProgressAnimator.onBackCancelled(() -> { 280 final OnBackAnimationCallback callback = getBackAnimationCallback(); 281 if (callback != null) { 282 callback.onBackCancelled(); 283 } 284 }); 285 }); 286 } 287 288 @Override onBackInvoked()289 public void onBackInvoked() throws RemoteException { 290 Handler.getMain().post(() -> { 291 mProgressAnimator.reset(); 292 final OnBackInvokedCallback callback = mCallback.get(); 293 if (callback == null) { 294 Log.d(TAG, "Trying to call onBackInvoked() on a null callback reference."); 295 return; 296 } 297 callback.onBackInvoked(); 298 }); 299 } 300 301 @Nullable getBackAnimationCallback()302 private OnBackAnimationCallback getBackAnimationCallback() { 303 OnBackInvokedCallback callback = mCallback.get(); 304 return callback instanceof OnBackAnimationCallback ? (OnBackAnimationCallback) callback 305 : null; 306 } 307 } 308 309 /** 310 * Returns if the legacy back behavior should be used. 311 * <p> 312 * Legacy back behavior dispatches KEYCODE_BACK instead of invoking the application registered 313 * {@link OnBackInvokedCallback}. 314 */ isOnBackInvokedCallbackEnabled(@ullable Context context)315 public static boolean isOnBackInvokedCallbackEnabled(@Nullable Context context) { 316 // new back is enabled if the feature flag is enabled AND the app does not explicitly 317 // request legacy back. 318 boolean featureFlagEnabled = ENABLE_PREDICTIVE_BACK; 319 // If the context is null, we assume true and fallback on the two other conditions. 320 boolean appRequestsPredictiveBack = 321 context != null && context.getApplicationInfo().isOnBackInvokedCallbackEnabled(); 322 323 if (DEBUG) { 324 Log.d(TAG, TextUtils.formatSimple("App: %s featureFlagEnabled=%s " 325 + "appRequestsPredictiveBack=%s alwaysEnforce=%s", 326 context != null ? context.getApplicationInfo().packageName : "null context", 327 featureFlagEnabled, appRequestsPredictiveBack, ALWAYS_ENFORCE_PREDICTIVE_BACK)); 328 } 329 330 return featureFlagEnabled && (appRequestsPredictiveBack || ALWAYS_ENFORCE_PREDICTIVE_BACK); 331 } 332 333 @Override setImeOnBackInvokedDispatcher( @onNull ImeOnBackInvokedDispatcher imeDispatcher)334 public void setImeOnBackInvokedDispatcher( 335 @NonNull ImeOnBackInvokedDispatcher imeDispatcher) { 336 mImeDispatcher = imeDispatcher; 337 } 338 339 340 /** 341 * Class used to check whether a callback can be registered or not. This is meant to be 342 * shared with {@link ProxyOnBackInvokedDispatcher} which needs to do the same checks. 343 */ 344 public static class Checker { 345 346 private final boolean mApplicationCallBackEnabled; 347 Checker(boolean applicationCallBackEnabled)348 public Checker(boolean applicationCallBackEnabled) { 349 mApplicationCallBackEnabled = applicationCallBackEnabled; 350 } 351 352 /** 353 * Checks whether the given callback can be registered with the given priority. 354 * @return true if the callback can be added. 355 * @throws IllegalArgumentException if the priority is negative. 356 */ checkApplicationCallbackRegistration(int priority, OnBackInvokedCallback callback)357 public boolean checkApplicationCallbackRegistration(int priority, 358 OnBackInvokedCallback callback) { 359 if (!mApplicationCallBackEnabled 360 && !(callback instanceof CompatOnBackInvokedCallback) 361 && !ALWAYS_ENFORCE_PREDICTIVE_BACK) { 362 Log.w("OnBackInvokedCallback", 363 "OnBackInvokedCallback is not enabled for the application." 364 + "\nSet 'android:enableOnBackInvokedCallback=\"true\"' in the" 365 + " application manifest."); 366 return false; 367 } 368 if (priority < 0) { 369 throw new IllegalArgumentException("Application registered OnBackInvokedCallback " 370 + "cannot have negative priority. Priority: " + priority); 371 } 372 Objects.requireNonNull(callback); 373 return true; 374 } 375 } 376 } 377