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.content.res.Configuration;
27 import android.content.res.Resources;
28 import android.content.res.TypedArray;
29 import android.os.Handler;
30 import android.os.Looper;
31 import android.os.RemoteException;
32 import android.os.SystemProperties;
33 import android.text.TextUtils;
34 import android.util.Log;
35 import android.util.TypedValue;
36 import android.view.IWindow;
37 import android.view.IWindowSession;
38 import android.view.ImeBackAnimationController;
39 import android.view.MotionEvent;
40 
41 import androidx.annotation.VisibleForTesting;
42 
43 import com.android.internal.R;
44 import com.android.internal.annotations.GuardedBy;
45 
46 import java.io.PrintWriter;
47 import java.lang.ref.WeakReference;
48 import java.util.ArrayList;
49 import java.util.HashMap;
50 import java.util.Objects;
51 import java.util.TreeMap;
52 import java.util.function.Supplier;
53 
54 /**
55  * Provides window based implementation of {@link OnBackInvokedDispatcher}.
56  * <p>
57  * Callbacks with higher priorities receive back dispatching first.
58  * Within the same priority, callbacks receive back dispatching in the reverse order
59  * in which they are added.
60  * <p>
61  * When the top priority callback is updated, the new callback is propagated to the Window Manager
62  * if the window the instance is associated with has been attached. It is allowed to register /
63  * unregister {@link OnBackInvokedCallback}s before the window is attached, although
64  * callbacks will not receive dispatches until window attachment.
65  *
66  * @hide
67  */
68 public class WindowOnBackInvokedDispatcher implements OnBackInvokedDispatcher {
69     private IWindowSession mWindowSession;
70     private IWindow mWindow;
71     @VisibleForTesting
72     public final BackTouchTracker mTouchTracker = new BackTouchTracker();
73     @VisibleForTesting
74     public final BackProgressAnimator mProgressAnimator = new BackProgressAnimator();
75     // The handler to run callbacks on.
76     // This should be on the same thread the ViewRootImpl holding this instance is created on.
77     @NonNull
78     private final Handler mHandler;
79     private static final String TAG = "WindowOnBackDispatcher";
80     private static final boolean ENABLE_PREDICTIVE_BACK = SystemProperties
81             .getInt("persist.wm.debug.predictive_back", 1) != 0;
82     private static final boolean ALWAYS_ENFORCE_PREDICTIVE_BACK = SystemProperties
83             .getInt("persist.wm.debug.predictive_back_always_enforce", 0) != 0;
84     private static final boolean PREDICTIVE_BACK_FALLBACK_WINDOW_ATTRIBUTE =
85             SystemProperties.getInt("persist.wm.debug.predictive_back_fallback_window_attribute", 0)
86                     != 0;
87     @Nullable
88     private ImeOnBackInvokedDispatcher mImeDispatcher;
89 
90     @Nullable
91     private ImeBackAnimationController mImeBackAnimationController;
92 
93     @GuardedBy("mLock")
94     /** Convenience hashmap to quickly decide if a callback has been added. */
95     private final HashMap<OnBackInvokedCallback, Integer> mAllCallbacks = new HashMap<>();
96     /** Holds all callbacks by priorities. */
97 
98     @VisibleForTesting
99     @GuardedBy("mLock")
100     public final TreeMap<Integer, ArrayList<OnBackInvokedCallback>>
101             mOnBackInvokedCallbacks = new TreeMap<>();
102 
103     private Checker mChecker;
104     private final Object mLock = new Object();
105     // The threshold for back swipe full progress.
106     private float mBackSwipeLinearThreshold;
107     private float mNonLinearProgressFactor;
108 
WindowOnBackInvokedDispatcher(@onNull Context context, Looper looper)109     public WindowOnBackInvokedDispatcher(@NonNull Context context, Looper looper) {
110         mChecker = new Checker(context);
111         mHandler = new Handler(looper);
112     }
113 
114     /** Updates the dispatcher state on a new {@link MotionEvent}. */
onMotionEvent(MotionEvent ev)115     public void onMotionEvent(MotionEvent ev) {
116         if (!isBackGestureInProgress() || ev == null || ev.getAction() != MotionEvent.ACTION_MOVE) {
117             return;
118         }
119         mTouchTracker.update(ev.getX(), ev.getY(), Float.NaN, Float.NaN);
120         if (mTouchTracker.shouldUpdateStartLocation()) {
121             // Reset the start location on the first event after starting back, so that
122             // the beginning of the animation feels smooth.
123             mTouchTracker.updateStartLocation();
124         }
125         if (!mProgressAnimator.isBackAnimationInProgress()) {
126             return;
127         }
128         final BackMotionEvent backEvent = mTouchTracker.createProgressEvent();
129         mProgressAnimator.onBackProgressed(backEvent);
130     }
131 
132     /**
133      * Sends the pending top callback (if one exists) to WM when the view root
134      * is attached a window.
135      */
attachToWindow(@onNull IWindowSession windowSession, @NonNull IWindow window, @Nullable ImeBackAnimationController imeBackAnimationController)136     public void attachToWindow(@NonNull IWindowSession windowSession, @NonNull IWindow window,
137             @Nullable ImeBackAnimationController imeBackAnimationController) {
138         synchronized (mLock) {
139             mWindowSession = windowSession;
140             mWindow = window;
141             mImeBackAnimationController = imeBackAnimationController;
142             if (!mAllCallbacks.isEmpty()) {
143                 setTopOnBackInvokedCallback(getTopCallback());
144             }
145         }
146     }
147 
148     /** Detaches the dispatcher instance from its window. */
detachFromWindow()149     public void detachFromWindow() {
150         synchronized (mLock) {
151             clear();
152             mWindow = null;
153             mWindowSession = null;
154             mImeBackAnimationController = null;
155         }
156     }
157 
158     // TODO: Take an Executor for the callback to run on.
159     @Override
registerOnBackInvokedCallback( @riority int priority, @NonNull OnBackInvokedCallback callback)160     public void registerOnBackInvokedCallback(
161             @Priority int priority, @NonNull OnBackInvokedCallback callback) {
162         if (mChecker.checkApplicationCallbackRegistration(priority, callback)) {
163             registerOnBackInvokedCallbackUnchecked(callback, priority);
164         }
165     }
166 
167     /**
168      * Register a callback bypassing platform checks. This is used to register compatibility
169      * callbacks.
170      */
registerOnBackInvokedCallbackUnchecked( @onNull OnBackInvokedCallback callback, @Priority int priority)171     public void registerOnBackInvokedCallbackUnchecked(
172             @NonNull OnBackInvokedCallback callback, @Priority int priority) {
173         synchronized (mLock) {
174             if (mImeDispatcher != null) {
175                 mImeDispatcher.registerOnBackInvokedCallback(priority, callback);
176                 return;
177             }
178             if (callback instanceof ImeOnBackInvokedDispatcher.ImeOnBackInvokedCallback) {
179                 // Fall back to compat back key injection if legacy back behaviour should be used.
180                 if (!isOnBackInvokedCallbackEnabled()) return;
181                 if (callback instanceof ImeOnBackInvokedDispatcher.DefaultImeOnBackAnimationCallback
182                         && mImeBackAnimationController != null) {
183                     // register ImeBackAnimationController instead to play predictive back animation
184                     callback = mImeBackAnimationController;
185                 }
186             }
187 
188             if (!mOnBackInvokedCallbacks.containsKey(priority)) {
189                 mOnBackInvokedCallbacks.put(priority, new ArrayList<>());
190             }
191             ArrayList<OnBackInvokedCallback> callbacks = mOnBackInvokedCallbacks.get(priority);
192 
193             // If callback has already been added, remove it and re-add it.
194             if (mAllCallbacks.containsKey(callback)) {
195                 if (DEBUG) {
196                     Log.i(TAG, "Callback already added. Removing and re-adding it.");
197                 }
198                 Integer prevPriority = mAllCallbacks.get(callback);
199                 mOnBackInvokedCallbacks.get(prevPriority).remove(callback);
200             }
201 
202             OnBackInvokedCallback previousTopCallback = getTopCallback();
203             callbacks.add(callback);
204             mAllCallbacks.put(callback, priority);
205             if (previousTopCallback == null
206                     || (previousTopCallback != callback
207                     && mAllCallbacks.get(previousTopCallback) <= priority)) {
208                 setTopOnBackInvokedCallback(callback);
209             }
210         }
211     }
212 
213     @Override
unregisterOnBackInvokedCallback(@onNull OnBackInvokedCallback callback)214     public void unregisterOnBackInvokedCallback(@NonNull OnBackInvokedCallback callback) {
215         synchronized (mLock) {
216             if (mImeDispatcher != null) {
217                 mImeDispatcher.unregisterOnBackInvokedCallback(callback);
218                 return;
219             }
220             if (callback instanceof ImeOnBackInvokedDispatcher.DefaultImeOnBackAnimationCallback) {
221                 callback = mImeBackAnimationController;
222             }
223             if (!mAllCallbacks.containsKey(callback)) {
224                 if (DEBUG) {
225                     Log.i(TAG, "Callback not found. returning...");
226                 }
227                 return;
228             }
229             OnBackInvokedCallback previousTopCallback = getTopCallback();
230             Integer priority = mAllCallbacks.get(callback);
231             ArrayList<OnBackInvokedCallback> callbacks = mOnBackInvokedCallbacks.get(priority);
232             callbacks.remove(callback);
233             if (callbacks.isEmpty()) {
234                 mOnBackInvokedCallbacks.remove(priority);
235             }
236             mAllCallbacks.remove(callback);
237             // Re-populate the top callback to WM if the removed callback was previously the top
238             // one.
239             if (previousTopCallback == callback) {
240                 // We should call onBackCancelled() when an active callback is removed from
241                 // dispatcher.
242                 sendCancelledIfInProgress(callback);
243                 setTopOnBackInvokedCallback(getTopCallback());
244             }
245         }
246     }
247 
248     /**
249      * Indicates if a user gesture is currently in progress.
250      */
isBackGestureInProgress()251     public boolean isBackGestureInProgress() {
252         synchronized (mLock) {
253             return mTouchTracker.isActive();
254         }
255     }
256 
sendCancelledIfInProgress(@onNull OnBackInvokedCallback callback)257     private void sendCancelledIfInProgress(@NonNull OnBackInvokedCallback callback) {
258         boolean isInProgress = mProgressAnimator.isBackAnimationInProgress();
259         if (isInProgress && callback instanceof OnBackAnimationCallback) {
260             OnBackAnimationCallback animatedCallback = (OnBackAnimationCallback) callback;
261             animatedCallback.onBackCancelled();
262             if (DEBUG) {
263                 Log.d(TAG, "sendCancelIfRunning: callback canceled");
264             }
265         } else {
266             Log.w(TAG, "sendCancelIfRunning: isInProgress=" + isInProgress
267                     + " callback=" + callback);
268         }
269     }
270 
271     @Override
registerSystemOnBackInvokedCallback(@onNull OnBackInvokedCallback callback)272     public void registerSystemOnBackInvokedCallback(@NonNull OnBackInvokedCallback callback) {
273         registerOnBackInvokedCallbackUnchecked(callback, OnBackInvokedDispatcher.PRIORITY_SYSTEM);
274     }
275 
276     /** Clears all registered callbacks on the instance. */
clear()277     public void clear() {
278         synchronized (mLock) {
279             if (mImeDispatcher != null) {
280                 mImeDispatcher.clear();
281                 mImeDispatcher = null;
282             }
283             if (!mAllCallbacks.isEmpty()) {
284                 OnBackInvokedCallback topCallback = getTopCallback();
285                 if (topCallback != null) {
286                     sendCancelledIfInProgress(topCallback);
287                 } else {
288                     // Should not be possible
289                     Log.e(TAG, "There is no topCallback, even if mAllCallbacks is not empty");
290                 }
291                 // Clear binder references in WM.
292                 setTopOnBackInvokedCallback(null);
293             }
294 
295             // We should also stop running animations since all callbacks have been removed.
296             // note: mSpring.skipToEnd(), in ProgressAnimator.reset(), requires the main handler.
297             mHandler.post(mProgressAnimator::reset);
298             mAllCallbacks.clear();
299             mOnBackInvokedCallbacks.clear();
300         }
301     }
302 
setTopOnBackInvokedCallback(@ullable OnBackInvokedCallback callback)303     private void setTopOnBackInvokedCallback(@Nullable OnBackInvokedCallback callback) {
304         if (mWindowSession == null || mWindow == null) {
305             return;
306         }
307         try {
308             OnBackInvokedCallbackInfo callbackInfo = null;
309             if (callback != null) {
310                 int priority = mAllCallbacks.get(callback);
311                 final IOnBackInvokedCallback iCallback = new OnBackInvokedCallbackWrapper(
312                         callback, mTouchTracker, mProgressAnimator, mHandler);
313                 callbackInfo = new OnBackInvokedCallbackInfo(
314                         iCallback,
315                         priority,
316                         callback instanceof OnBackAnimationCallback);
317             }
318             mWindowSession.setOnBackInvokedCallbackInfo(mWindow, callbackInfo);
319         } catch (RemoteException e) {
320             Log.e(TAG, "Failed to set OnBackInvokedCallback to WM. Error: " + e);
321         }
322     }
323 
getTopCallback()324     public OnBackInvokedCallback getTopCallback() {
325         synchronized (mLock) {
326             if (mAllCallbacks.isEmpty()) {
327                 return null;
328             }
329             for (Integer priority : mOnBackInvokedCallbacks.descendingKeySet()) {
330                 ArrayList<OnBackInvokedCallback> callbacks = mOnBackInvokedCallbacks.get(priority);
331                 if (!callbacks.isEmpty()) {
332                     return callbacks.get(callbacks.size() - 1);
333                 }
334             }
335         }
336         return null;
337     }
338 
339     /**
340      * The {@link Context} in ViewRootImp and Activity could be different, this will make sure it
341      * could update the checker condition base on the real context when binding the proxy
342      * dispatcher in PhoneWindow.
343      */
updateContext(@onNull Context context)344     public void updateContext(@NonNull Context context) {
345         mChecker = new Checker(context);
346         // Set swipe threshold values.
347         Resources res = context.getResources();
348         mBackSwipeLinearThreshold =
349                 res.getDimension(R.dimen.navigation_edge_action_progress_threshold);
350         TypedValue typedValue = new TypedValue();
351         res.getValue(R.dimen.back_progress_non_linear_factor, typedValue, true);
352         mNonLinearProgressFactor = typedValue.getFloat();
353         onConfigurationChanged(context.getResources().getConfiguration());
354     }
355 
356     /** Updates the threshold values for computing progress. */
onConfigurationChanged(Configuration configuration)357     public void onConfigurationChanged(Configuration configuration) {
358         float maxDistance = configuration.windowConfiguration.getMaxBounds().width();
359         float linearDistance = Math.min(maxDistance, mBackSwipeLinearThreshold);
360         mTouchTracker.setProgressThresholds(
361                 linearDistance, maxDistance, mNonLinearProgressFactor);
362     }
363 
364     /**
365      * Returns false if the legacy back behavior should be used.
366      */
isOnBackInvokedCallbackEnabled()367     public boolean isOnBackInvokedCallbackEnabled() {
368         return isOnBackInvokedCallbackEnabled(mChecker.getContext());
369     }
370 
371     /**
372      * Dump information about this WindowOnBackInvokedDispatcher
373      * @param prefix the prefix that will be prepended to each line of the produced output
374      * @param writer the writer that will receive the resulting text
375      */
dump(String prefix, PrintWriter writer)376     public void dump(String prefix, PrintWriter writer) {
377         String innerPrefix = prefix + "    ";
378         writer.println(prefix + "WindowOnBackDispatcher:");
379         synchronized (mLock) {
380             if (mAllCallbacks.isEmpty()) {
381                 writer.println(prefix + "<None>");
382                 return;
383             }
384 
385             writer.println(innerPrefix + "Top Callback: " + getTopCallback());
386             writer.println(innerPrefix + "Callbacks: ");
387             mAllCallbacks.forEach((callback, priority) -> {
388                 writer.println(innerPrefix + "  Callback: " + callback + " Priority=" + priority);
389             });
390         }
391     }
392 
393     private static class OnBackInvokedCallbackWrapper extends IOnBackInvokedCallback.Stub {
394         @NonNull
395         private final WeakReference<OnBackInvokedCallback> mCallback;
396         @NonNull
397         private final BackProgressAnimator mProgressAnimator;
398         @NonNull
399         private final BackTouchTracker mTouchTracker;
400         @NonNull
401         private final Handler mHandler;
402 
OnBackInvokedCallbackWrapper( @onNull OnBackInvokedCallback callback, @NonNull BackTouchTracker touchTracker, @NonNull BackProgressAnimator progressAnimator, @NonNull Handler handler)403         OnBackInvokedCallbackWrapper(
404                 @NonNull OnBackInvokedCallback callback,
405                 @NonNull BackTouchTracker touchTracker,
406                 @NonNull BackProgressAnimator progressAnimator,
407                 @NonNull Handler handler) {
408             mCallback = new WeakReference<>(callback);
409             mTouchTracker = touchTracker;
410             mProgressAnimator = progressAnimator;
411             mHandler = handler;
412         }
413 
414         @Override
onBackStarted(BackMotionEvent backEvent)415         public void onBackStarted(BackMotionEvent backEvent) {
416             mHandler.post(() -> {
417                 final OnBackAnimationCallback callback = getBackAnimationCallback();
418 
419                 // reset progress animator before dispatching onBackStarted to callback. This
420                 // ensures that onBackCancelled (of a previous gesture) is always dispatched
421                 // before onBackStarted
422                 if (callback != null && mProgressAnimator.isBackAnimationInProgress()) {
423                     mProgressAnimator.reset();
424                 }
425                 mTouchTracker.setState(BackTouchTracker.TouchTrackerState.ACTIVE);
426                 mTouchTracker.setShouldUpdateStartLocation(true);
427                 mTouchTracker.setGestureStartLocation(
428                         backEvent.getTouchX(), backEvent.getTouchY(), backEvent.getSwipeEdge());
429 
430                 if (callback != null) {
431                     callback.onBackStarted(BackEvent.fromBackMotionEvent(backEvent));
432                     mProgressAnimator.onBackStarted(backEvent, callback::onBackProgressed);
433                 }
434             });
435         }
436 
437         @Override
onBackProgressed(BackMotionEvent backEvent)438         public void onBackProgressed(BackMotionEvent backEvent) {
439             // This is only called in some special cases such as when activity embedding is active
440             // or when the activity is letterboxed. Otherwise mProgressAnimator#onBackProgressed is
441             // called from WindowOnBackInvokedDispatcher#onMotionEvent
442             mHandler.post(() -> {
443                 if (getBackAnimationCallback() != null) {
444                     mProgressAnimator.onBackProgressed(backEvent);
445                 }
446             });
447         }
448 
449         @Override
onBackCancelled()450         public void onBackCancelled() {
451             mHandler.post(() -> {
452                 final OnBackAnimationCallback callback = getBackAnimationCallback();
453                 mTouchTracker.reset();
454                 if (callback == null) return;
455                 mProgressAnimator.onBackCancelled(callback::onBackCancelled);
456             });
457         }
458 
459         @Override
onBackInvoked()460         public void onBackInvoked() throws RemoteException {
461             mHandler.post(() -> {
462                 mTouchTracker.reset();
463                 boolean isInProgress = mProgressAnimator.isBackAnimationInProgress();
464                 final OnBackInvokedCallback callback = mCallback.get();
465                 if (callback == null) {
466                     mProgressAnimator.reset();
467                     Log.d(TAG, "Trying to call onBackInvoked() on a null callback reference.");
468                     return;
469                 }
470                 if (callback instanceof OnBackAnimationCallback && !isInProgress) {
471                     Log.w(TAG, "ProgressAnimator was not in progress, skip onBackInvoked().");
472                     return;
473                 }
474                 OnBackAnimationCallback animationCallback = getBackAnimationCallback();
475                 if (animationCallback != null) {
476                     mProgressAnimator.onBackInvoked(callback::onBackInvoked);
477                 } else {
478                     mProgressAnimator.reset();
479                     callback.onBackInvoked();
480                 }
481             });
482         }
483 
484         @Override
setTriggerBack(boolean triggerBack)485         public void setTriggerBack(boolean triggerBack) throws RemoteException {
486             mTouchTracker.setTriggerBack(triggerBack);
487         }
488 
489         @Nullable
getBackAnimationCallback()490         private OnBackAnimationCallback getBackAnimationCallback() {
491             OnBackInvokedCallback callback = mCallback.get();
492             return callback instanceof OnBackAnimationCallback ? (OnBackAnimationCallback) callback
493                     : null;
494         }
495     }
496 
497     /**
498      * Returns false if the legacy back behavior should be used.
499      * <p>
500      * Legacy back behavior dispatches KEYCODE_BACK instead of invoking the application registered
501      * {@link OnBackInvokedCallback}.
502      */
isOnBackInvokedCallbackEnabled(@onNull Context context)503     public static boolean isOnBackInvokedCallbackEnabled(@NonNull Context context) {
504         final Context originalContext = context;
505         while ((context instanceof ContextWrapper) && !(context instanceof Activity)) {
506             context = ((ContextWrapper) context).getBaseContext();
507         }
508         final ActivityInfo activityInfo = (context instanceof Activity)
509                 ? ((Activity) context).getActivityInfo() : null;
510         final ApplicationInfo applicationInfo = context.getApplicationInfo();
511 
512         return WindowOnBackInvokedDispatcher
513                 .isOnBackInvokedCallbackEnabled(activityInfo, applicationInfo,
514                         () -> originalContext);
515     }
516 
517     @Override
setImeOnBackInvokedDispatcher( @onNull ImeOnBackInvokedDispatcher imeDispatcher)518     public void setImeOnBackInvokedDispatcher(
519             @NonNull ImeOnBackInvokedDispatcher imeDispatcher) {
520         mImeDispatcher = imeDispatcher;
521         mImeDispatcher.setHandler(mHandler);
522     }
523 
524     /** Returns true if a non-null {@link ImeOnBackInvokedDispatcher} has been set. **/
hasImeOnBackInvokedDispatcher()525     public boolean hasImeOnBackInvokedDispatcher() {
526         return mImeDispatcher != null;
527     }
528 
529     /**
530      * Class used to check whether a callback can be registered or not. This is meant to be
531      * shared with {@link ProxyOnBackInvokedDispatcher} which needs to do the same checks.
532      */
533     public static class Checker {
534         private WeakReference<Context> mContext;
535 
Checker(@onNull Context context)536         public Checker(@NonNull Context context) {
537             mContext = new WeakReference<>(context);
538         }
539 
540         /**
541          * Checks whether the given callback can be registered with the given priority.
542          * @return true if the callback can be added.
543          * @throws IllegalArgumentException if the priority is negative.
544          */
checkApplicationCallbackRegistration(int priority, OnBackInvokedCallback callback)545         public boolean checkApplicationCallbackRegistration(int priority,
546                 OnBackInvokedCallback callback) {
547             if (!WindowOnBackInvokedDispatcher.isOnBackInvokedCallbackEnabled(getContext())
548                     && !(callback instanceof CompatOnBackInvokedCallback)) {
549                 Log.w(TAG,
550                         "OnBackInvokedCallback is not enabled for the application."
551                                 + "\nSet 'android:enableOnBackInvokedCallback=\"true\"' in the"
552                                 + " application manifest.");
553                 return false;
554             }
555             if (priority < 0) {
556                 throw new IllegalArgumentException("Application registered OnBackInvokedCallback "
557                         + "cannot have negative priority. Priority: " + priority);
558             }
559             Objects.requireNonNull(callback);
560             return true;
561         }
562 
getContext()563         private Context getContext() {
564             return mContext.get();
565         }
566     }
567 
568     /**
569      * @hide
570      */
isOnBackInvokedCallbackEnabled(@ullable ActivityInfo activityInfo, @NonNull ApplicationInfo applicationInfo, @NonNull Supplier<Context> contextSupplier)571     public static boolean isOnBackInvokedCallbackEnabled(@Nullable ActivityInfo activityInfo,
572             @NonNull ApplicationInfo applicationInfo,
573             @NonNull Supplier<Context> contextSupplier) {
574         // new back is enabled if the feature flag is enabled AND the app does not explicitly
575         // request legacy back.
576         if (!ENABLE_PREDICTIVE_BACK) {
577             return false;
578         }
579 
580         if (ALWAYS_ENFORCE_PREDICTIVE_BACK) {
581             return true;
582         }
583 
584         boolean requestsPredictiveBack;
585         // Activity
586         if (activityInfo != null && activityInfo.hasOnBackInvokedCallbackEnabled()) {
587             requestsPredictiveBack = activityInfo.isOnBackInvokedCallbackEnabled();
588             if (DEBUG) {
589                 Log.d(TAG, TextUtils.formatSimple(
590                         "Activity: %s isPredictiveBackEnabled=%s",
591                         activityInfo.getComponentName(),
592                         requestsPredictiveBack));
593             }
594             return requestsPredictiveBack;
595         }
596 
597         // Application
598         requestsPredictiveBack = applicationInfo.isOnBackInvokedCallbackEnabled();
599         if (DEBUG) {
600             Log.d(TAG, TextUtils.formatSimple("App: %s requestsPredictiveBack=%s",
601                     applicationInfo.packageName,
602                     requestsPredictiveBack));
603         }
604         if (requestsPredictiveBack) {
605             return true;
606         }
607 
608         if (PREDICTIVE_BACK_FALLBACK_WINDOW_ATTRIBUTE) {
609             // Compatibility check for legacy window style flag used by Wear OS.
610             // Note on compatibility behavior:
611             // 1. windowSwipeToDismiss should be respected for all apps not opted in.
612             // 2. windowSwipeToDismiss should be true for all apps not opted in, which
613             //    enables the PB animation for them.
614             // 3. windowSwipeToDismiss=false should be respected for apps not opted in,
615             //    which disables PB & onBackPressed caused by BackAnimController's
616             //    setTrigger(true)
617             // Use the original context to resolve the styled attribute so that they stay
618             // true to the window.
619             final Context context = contextSupplier.get();
620             boolean windowSwipeToDismiss = true;
621             if (context != null) {
622                 final TypedArray array = context.obtainStyledAttributes(
623                             new int[]{android.R.attr.windowSwipeToDismiss});
624                 if (array.getIndexCount() > 0) {
625                     windowSwipeToDismiss = array.getBoolean(0, true);
626                 }
627                 array.recycle();
628             }
629 
630             if (DEBUG) {
631                 Log.i(TAG, "falling back to windowSwipeToDismiss: " + windowSwipeToDismiss);
632             }
633 
634             requestsPredictiveBack = windowSwipeToDismiss;
635         }
636         return requestsPredictiveBack;
637     }
638 }
639