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