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.app.Activity; 22 import android.content.Context; 23 import android.content.ContextWrapper; 24 import android.content.pm.ActivityInfo; 25 import android.content.pm.ApplicationInfo; 26 import android.os.Handler; 27 import android.os.RemoteException; 28 import android.os.SystemProperties; 29 import android.text.TextUtils; 30 import android.util.Log; 31 import android.view.IWindow; 32 import android.view.IWindowSession; 33 34 import androidx.annotation.VisibleForTesting; 35 36 import java.io.PrintWriter; 37 import java.lang.ref.WeakReference; 38 import java.util.ArrayList; 39 import java.util.HashMap; 40 import java.util.Objects; 41 import java.util.TreeMap; 42 43 /** 44 * Provides window based implementation of {@link OnBackInvokedDispatcher}. 45 * <p> 46 * Callbacks with higher priorities receive back dispatching first. 47 * Within the same priority, callbacks receive back dispatching in the reverse order 48 * in which they are added. 49 * <p> 50 * When the top priority callback is updated, the new callback is propagated to the Window Manager 51 * if the window the instance is associated with has been attached. It is allowed to register / 52 * unregister {@link OnBackInvokedCallback}s before the window is attached, although 53 * callbacks will not receive dispatches until window attachment. 54 * 55 * @hide 56 */ 57 public class WindowOnBackInvokedDispatcher implements OnBackInvokedDispatcher { 58 private IWindowSession mWindowSession; 59 private IWindow mWindow; 60 private static final String TAG = "WindowOnBackDispatcher"; 61 private static final boolean ENABLE_PREDICTIVE_BACK = SystemProperties 62 .getInt("persist.wm.debug.predictive_back", 1) != 0; 63 private static final boolean ALWAYS_ENFORCE_PREDICTIVE_BACK = SystemProperties 64 .getInt("persist.wm.debug.predictive_back_always_enforce", 0) != 0; 65 @Nullable 66 private ImeOnBackInvokedDispatcher mImeDispatcher; 67 68 /** Convenience hashmap to quickly decide if a callback has been added. */ 69 private final HashMap<OnBackInvokedCallback, Integer> mAllCallbacks = new HashMap<>(); 70 /** Holds all callbacks by priorities. */ 71 72 @VisibleForTesting 73 public final TreeMap<Integer, ArrayList<OnBackInvokedCallback>> 74 mOnBackInvokedCallbacks = new TreeMap<>(); 75 private Checker mChecker; 76 WindowOnBackInvokedDispatcher(@onNull Context context)77 public WindowOnBackInvokedDispatcher(@NonNull Context context) { 78 mChecker = new Checker(context); 79 } 80 81 /** 82 * Sends the pending top callback (if one exists) to WM when the view root 83 * is attached a window. 84 */ attachToWindow(@onNull IWindowSession windowSession, @NonNull IWindow window)85 public void attachToWindow(@NonNull IWindowSession windowSession, @NonNull IWindow window) { 86 mWindowSession = windowSession; 87 mWindow = window; 88 if (!mAllCallbacks.isEmpty()) { 89 setTopOnBackInvokedCallback(getTopCallback()); 90 } 91 } 92 93 /** Detaches the dispatcher instance from its window. */ detachFromWindow()94 public void detachFromWindow() { 95 clear(); 96 mWindow = null; 97 mWindowSession = null; 98 } 99 100 // TODO: Take an Executor for the callback to run on. 101 @Override registerOnBackInvokedCallback( @riority int priority, @NonNull OnBackInvokedCallback callback)102 public void registerOnBackInvokedCallback( 103 @Priority int priority, @NonNull OnBackInvokedCallback callback) { 104 if (mChecker.checkApplicationCallbackRegistration(priority, callback)) { 105 registerOnBackInvokedCallbackUnchecked(callback, priority); 106 } 107 } 108 109 /** 110 * Register a callback bypassing platform checks. This is used to register compatibility 111 * callbacks. 112 */ registerOnBackInvokedCallbackUnchecked( @onNull OnBackInvokedCallback callback, @Priority int priority)113 public void registerOnBackInvokedCallbackUnchecked( 114 @NonNull OnBackInvokedCallback callback, @Priority int priority) { 115 if (mImeDispatcher != null) { 116 mImeDispatcher.registerOnBackInvokedCallback(priority, callback); 117 return; 118 } 119 if (!mOnBackInvokedCallbacks.containsKey(priority)) { 120 mOnBackInvokedCallbacks.put(priority, new ArrayList<>()); 121 } 122 ArrayList<OnBackInvokedCallback> callbacks = mOnBackInvokedCallbacks.get(priority); 123 124 // If callback has already been added, remove it and re-add it. 125 if (mAllCallbacks.containsKey(callback)) { 126 if (DEBUG) { 127 Log.i(TAG, "Callback already added. Removing and re-adding it."); 128 } 129 Integer prevPriority = mAllCallbacks.get(callback); 130 mOnBackInvokedCallbacks.get(prevPriority).remove(callback); 131 } 132 133 OnBackInvokedCallback previousTopCallback = getTopCallback(); 134 callbacks.add(callback); 135 mAllCallbacks.put(callback, priority); 136 if (previousTopCallback == null 137 || (previousTopCallback != callback 138 && mAllCallbacks.get(previousTopCallback) <= priority)) { 139 setTopOnBackInvokedCallback(callback); 140 } 141 } 142 143 @Override unregisterOnBackInvokedCallback(@onNull OnBackInvokedCallback callback)144 public void unregisterOnBackInvokedCallback(@NonNull OnBackInvokedCallback callback) { 145 if (mImeDispatcher != null) { 146 mImeDispatcher.unregisterOnBackInvokedCallback(callback); 147 return; 148 } 149 if (!mAllCallbacks.containsKey(callback)) { 150 if (DEBUG) { 151 Log.i(TAG, "Callback not found. returning..."); 152 } 153 return; 154 } 155 OnBackInvokedCallback previousTopCallback = getTopCallback(); 156 Integer priority = mAllCallbacks.get(callback); 157 ArrayList<OnBackInvokedCallback> callbacks = mOnBackInvokedCallbacks.get(priority); 158 callbacks.remove(callback); 159 if (callbacks.isEmpty()) { 160 mOnBackInvokedCallbacks.remove(priority); 161 } 162 mAllCallbacks.remove(callback); 163 // Re-populate the top callback to WM if the removed callback was previously the top one. 164 if (previousTopCallback == callback) { 165 // We should call onBackCancelled() when an active callback is removed from dispatcher. 166 sendCancelledIfInProgress(callback); 167 setTopOnBackInvokedCallback(getTopCallback()); 168 } 169 } 170 sendCancelledIfInProgress(@onNull OnBackInvokedCallback callback)171 private void sendCancelledIfInProgress(@NonNull OnBackInvokedCallback callback) { 172 boolean isInProgress = mProgressAnimator.isBackAnimationInProgress(); 173 if (isInProgress && callback instanceof OnBackAnimationCallback) { 174 OnBackAnimationCallback animatedCallback = (OnBackAnimationCallback) callback; 175 animatedCallback.onBackCancelled(); 176 if (DEBUG) { 177 Log.d(TAG, "sendCancelIfRunning: callback canceled"); 178 } 179 } else { 180 Log.w(TAG, "sendCancelIfRunning: isInProgress=" + isInProgress 181 + "callback=" + callback); 182 } 183 } 184 185 @Override registerSystemOnBackInvokedCallback(@onNull OnBackInvokedCallback callback)186 public void registerSystemOnBackInvokedCallback(@NonNull OnBackInvokedCallback callback) { 187 registerOnBackInvokedCallbackUnchecked(callback, OnBackInvokedDispatcher.PRIORITY_SYSTEM); 188 } 189 190 /** Clears all registered callbacks on the instance. */ clear()191 public void clear() { 192 if (mImeDispatcher != null) { 193 mImeDispatcher.clear(); 194 mImeDispatcher = null; 195 } 196 if (!mAllCallbacks.isEmpty()) { 197 OnBackInvokedCallback topCallback = getTopCallback(); 198 if (topCallback != null) { 199 sendCancelledIfInProgress(topCallback); 200 } else { 201 // Should not be possible 202 Log.e(TAG, "There is no topCallback, even if mAllCallbacks is not empty"); 203 } 204 // Clear binder references in WM. 205 setTopOnBackInvokedCallback(null); 206 } 207 208 // We should also stop running animations since all callbacks have been removed. 209 // note: mSpring.skipToEnd(), in ProgressAnimator.reset(), requires the main handler. 210 Handler.getMain().post(mProgressAnimator::reset); 211 mAllCallbacks.clear(); 212 mOnBackInvokedCallbacks.clear(); 213 } 214 setTopOnBackInvokedCallback(@ullable OnBackInvokedCallback callback)215 private void setTopOnBackInvokedCallback(@Nullable OnBackInvokedCallback callback) { 216 if (mWindowSession == null || mWindow == null) { 217 return; 218 } 219 try { 220 OnBackInvokedCallbackInfo callbackInfo = null; 221 if (callback != null) { 222 int priority = mAllCallbacks.get(callback); 223 final IOnBackInvokedCallback iCallback = 224 callback instanceof ImeOnBackInvokedDispatcher 225 .ImeOnBackInvokedCallback 226 ? ((ImeOnBackInvokedDispatcher.ImeOnBackInvokedCallback) 227 callback).getIOnBackInvokedCallback() 228 : new OnBackInvokedCallbackWrapper(callback); 229 callbackInfo = new OnBackInvokedCallbackInfo( 230 iCallback, 231 priority, 232 callback instanceof OnBackAnimationCallback); 233 } 234 mWindowSession.setOnBackInvokedCallbackInfo(mWindow, callbackInfo); 235 } catch (RemoteException e) { 236 Log.e(TAG, "Failed to set OnBackInvokedCallback to WM. Error: " + e); 237 } 238 } 239 getTopCallback()240 public OnBackInvokedCallback getTopCallback() { 241 if (mAllCallbacks.isEmpty()) { 242 return null; 243 } 244 for (Integer priority : mOnBackInvokedCallbacks.descendingKeySet()) { 245 ArrayList<OnBackInvokedCallback> callbacks = mOnBackInvokedCallbacks.get(priority); 246 if (!callbacks.isEmpty()) { 247 return callbacks.get(callbacks.size() - 1); 248 } 249 } 250 return null; 251 } 252 253 @NonNull 254 private static final BackProgressAnimator mProgressAnimator = new BackProgressAnimator(); 255 256 /** 257 * The {@link Context} in ViewRootImp and Activity could be different, this will make sure it 258 * could update the checker condition base on the real context when binding the proxy 259 * dispatcher in PhoneWindow. 260 */ updateContext(@onNull Context context)261 public void updateContext(@NonNull Context context) { 262 mChecker = new Checker(context); 263 } 264 265 /** 266 * Returns false if the legacy back behavior should be used. 267 */ isOnBackInvokedCallbackEnabled()268 public boolean isOnBackInvokedCallbackEnabled() { 269 return Checker.isOnBackInvokedCallbackEnabled(mChecker.getContext()); 270 } 271 272 /** 273 * Dump information about this WindowOnBackInvokedDispatcher 274 * @param prefix the prefix that will be prepended to each line of the produced output 275 * @param writer the writer that will receive the resulting text 276 */ dump(String prefix, PrintWriter writer)277 public void dump(String prefix, PrintWriter writer) { 278 String innerPrefix = prefix + " "; 279 writer.println(prefix + "WindowOnBackDispatcher:"); 280 if (mAllCallbacks.isEmpty()) { 281 writer.println(prefix + "<None>"); 282 return; 283 } 284 285 writer.println(innerPrefix + "Top Callback: " + getTopCallback()); 286 writer.println(innerPrefix + "Callbacks: "); 287 mAllCallbacks.forEach((callback, priority) -> { 288 writer.println(innerPrefix + " Callback: " + callback + " Priority=" + priority); 289 }); 290 } 291 292 static class OnBackInvokedCallbackWrapper extends IOnBackInvokedCallback.Stub { 293 static class CallbackRef { 294 final WeakReference<OnBackInvokedCallback> mWeakRef; 295 final OnBackInvokedCallback mStrongRef; CallbackRef(@onNull OnBackInvokedCallback callback, boolean useWeakRef)296 CallbackRef(@NonNull OnBackInvokedCallback callback, boolean useWeakRef) { 297 if (useWeakRef) { 298 mWeakRef = new WeakReference<>(callback); 299 mStrongRef = null; 300 } else { 301 mStrongRef = callback; 302 mWeakRef = null; 303 } 304 } 305 get()306 OnBackInvokedCallback get() { 307 if (mStrongRef != null) { 308 return mStrongRef; 309 } 310 return mWeakRef.get(); 311 } 312 } 313 final CallbackRef mCallbackRef; 314 OnBackInvokedCallbackWrapper(@onNull OnBackInvokedCallback callback)315 OnBackInvokedCallbackWrapper(@NonNull OnBackInvokedCallback callback) { 316 mCallbackRef = new CallbackRef(callback, true /* useWeakRef */); 317 } 318 OnBackInvokedCallbackWrapper(@onNull OnBackInvokedCallback callback, boolean useWeakRef)319 OnBackInvokedCallbackWrapper(@NonNull OnBackInvokedCallback callback, boolean useWeakRef) { 320 mCallbackRef = new CallbackRef(callback, useWeakRef); 321 } 322 323 @Override onBackStarted(BackMotionEvent backEvent)324 public void onBackStarted(BackMotionEvent backEvent) { 325 Handler.getMain().post(() -> { 326 final OnBackAnimationCallback callback = getBackAnimationCallback(); 327 if (callback != null) { 328 mProgressAnimator.onBackStarted(backEvent, event -> 329 callback.onBackProgressed(event)); 330 callback.onBackStarted(new BackEvent( 331 backEvent.getTouchX(), backEvent.getTouchY(), 332 backEvent.getProgress(), backEvent.getSwipeEdge())); 333 } 334 }); 335 } 336 337 @Override onBackProgressed(BackMotionEvent backEvent)338 public void onBackProgressed(BackMotionEvent backEvent) { 339 Handler.getMain().post(() -> { 340 final OnBackAnimationCallback callback = getBackAnimationCallback(); 341 if (callback != null) { 342 mProgressAnimator.onBackProgressed(backEvent); 343 } 344 }); 345 } 346 347 @Override onBackCancelled()348 public void onBackCancelled() { 349 Handler.getMain().post(() -> { 350 mProgressAnimator.onBackCancelled(() -> { 351 final OnBackAnimationCallback callback = getBackAnimationCallback(); 352 if (callback != null) { 353 callback.onBackCancelled(); 354 } 355 }); 356 }); 357 } 358 359 @Override onBackInvoked()360 public void onBackInvoked() throws RemoteException { 361 Handler.getMain().post(() -> { 362 boolean isInProgress = mProgressAnimator.isBackAnimationInProgress(); 363 mProgressAnimator.reset(); 364 final OnBackInvokedCallback callback = mCallbackRef.get(); 365 if (callback == null) { 366 Log.d(TAG, "Trying to call onBackInvoked() on a null callback reference."); 367 return; 368 } 369 if (callback instanceof OnBackAnimationCallback && !isInProgress) { 370 Log.w(TAG, "ProgressAnimator was not in progress, skip onBackInvoked()."); 371 return; 372 } 373 callback.onBackInvoked(); 374 }); 375 } 376 377 @Nullable getBackAnimationCallback()378 private OnBackAnimationCallback getBackAnimationCallback() { 379 OnBackInvokedCallback callback = mCallbackRef.get(); 380 return callback instanceof OnBackAnimationCallback ? (OnBackAnimationCallback) callback 381 : null; 382 } 383 } 384 385 /** 386 * Returns false if the legacy back behavior should be used. 387 * <p> 388 * Legacy back behavior dispatches KEYCODE_BACK instead of invoking the application registered 389 * {@link OnBackInvokedCallback}. 390 */ isOnBackInvokedCallbackEnabled(@onNull Context context)391 public static boolean isOnBackInvokedCallbackEnabled(@NonNull Context context) { 392 return Checker.isOnBackInvokedCallbackEnabled(context); 393 } 394 395 @Override setImeOnBackInvokedDispatcher( @onNull ImeOnBackInvokedDispatcher imeDispatcher)396 public void setImeOnBackInvokedDispatcher( 397 @NonNull ImeOnBackInvokedDispatcher imeDispatcher) { 398 mImeDispatcher = imeDispatcher; 399 } 400 401 /** Returns true if a non-null {@link ImeOnBackInvokedDispatcher} has been set. **/ hasImeOnBackInvokedDispatcher()402 public boolean hasImeOnBackInvokedDispatcher() { 403 return mImeDispatcher != null; 404 } 405 406 /** 407 * Class used to check whether a callback can be registered or not. This is meant to be 408 * shared with {@link ProxyOnBackInvokedDispatcher} which needs to do the same checks. 409 */ 410 public static class Checker { 411 private WeakReference<Context> mContext; 412 Checker(@onNull Context context)413 public Checker(@NonNull Context context) { 414 mContext = new WeakReference<>(context); 415 } 416 417 /** 418 * Checks whether the given callback can be registered with the given priority. 419 * @return true if the callback can be added. 420 * @throws IllegalArgumentException if the priority is negative. 421 */ checkApplicationCallbackRegistration(int priority, OnBackInvokedCallback callback)422 public boolean checkApplicationCallbackRegistration(int priority, 423 OnBackInvokedCallback callback) { 424 if (!isOnBackInvokedCallbackEnabled(getContext()) 425 && !(callback instanceof CompatOnBackInvokedCallback)) { 426 Log.w(TAG, 427 "OnBackInvokedCallback is not enabled for the application." 428 + "\nSet 'android:enableOnBackInvokedCallback=\"true\"' in the" 429 + " application manifest."); 430 return false; 431 } 432 if (priority < 0) { 433 throw new IllegalArgumentException("Application registered OnBackInvokedCallback " 434 + "cannot have negative priority. Priority: " + priority); 435 } 436 Objects.requireNonNull(callback); 437 return true; 438 } 439 getContext()440 private Context getContext() { 441 return mContext.get(); 442 } 443 isOnBackInvokedCallbackEnabled(@ullable Context context)444 private static boolean isOnBackInvokedCallbackEnabled(@Nullable Context context) { 445 // new back is enabled if the feature flag is enabled AND the app does not explicitly 446 // request legacy back. 447 boolean featureFlagEnabled = ENABLE_PREDICTIVE_BACK; 448 if (!featureFlagEnabled) { 449 return false; 450 } 451 452 if (ALWAYS_ENFORCE_PREDICTIVE_BACK) { 453 return true; 454 } 455 456 // If the context is null, return false to use legacy back. 457 if (context == null) { 458 Log.w(TAG, "OnBackInvokedCallback is not enabled because context is null."); 459 return false; 460 } 461 462 boolean requestsPredictiveBack = false; 463 464 // Check if the context is from an activity. 465 while ((context instanceof ContextWrapper) && !(context instanceof Activity)) { 466 context = ((ContextWrapper) context).getBaseContext(); 467 } 468 469 boolean shouldCheckActivity = false; 470 471 if (context instanceof Activity) { 472 final Activity activity = (Activity) context; 473 474 final ActivityInfo activityInfo = activity.getActivityInfo(); 475 if (activityInfo != null) { 476 if (activityInfo.hasOnBackInvokedCallbackEnabled()) { 477 shouldCheckActivity = true; 478 requestsPredictiveBack = activityInfo.isOnBackInvokedCallbackEnabled(); 479 480 if (DEBUG) { 481 Log.d(TAG, TextUtils.formatSimple( 482 "Activity: %s isPredictiveBackEnabled=%s", 483 activity.getComponentName(), 484 requestsPredictiveBack)); 485 } 486 } 487 } else { 488 Log.w(TAG, "The ActivityInfo is null, so we cannot verify if this Activity" 489 + " has the 'android:enableOnBackInvokedCallback' attribute." 490 + " The application attribute will be used as a fallback."); 491 } 492 } 493 494 if (!shouldCheckActivity) { 495 final ApplicationInfo applicationInfo = context.getApplicationInfo(); 496 requestsPredictiveBack = applicationInfo.isOnBackInvokedCallbackEnabled(); 497 498 if (DEBUG) { 499 Log.d(TAG, TextUtils.formatSimple("App: %s requestsPredictiveBack=%s", 500 applicationInfo.packageName, 501 requestsPredictiveBack)); 502 } 503 } 504 505 return requestsPredictiveBack; 506 } 507 } 508 } 509