• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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