| /* |
| * Copyright (C) 2021 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package android.window; |
| |
| import android.annotation.NonNull; |
| import android.annotation.Nullable; |
| import android.app.Activity; |
| import android.content.Context; |
| import android.content.ContextWrapper; |
| import android.content.pm.ActivityInfo; |
| import android.content.pm.ApplicationInfo; |
| import android.content.res.Configuration; |
| import android.content.res.Resources; |
| import android.content.res.TypedArray; |
| import android.os.Handler; |
| import android.os.Looper; |
| import android.os.RemoteException; |
| import android.os.SystemProperties; |
| import android.text.TextUtils; |
| import android.util.Log; |
| import android.util.TypedValue; |
| import android.view.IWindow; |
| import android.view.IWindowSession; |
| import android.view.ImeBackAnimationController; |
| import android.view.MotionEvent; |
| |
| import androidx.annotation.VisibleForTesting; |
| |
| import com.android.internal.R; |
| import com.android.internal.annotations.GuardedBy; |
| |
| import java.io.PrintWriter; |
| import java.lang.ref.WeakReference; |
| import java.util.ArrayList; |
| import java.util.HashMap; |
| import java.util.Objects; |
| import java.util.TreeMap; |
| import java.util.function.Supplier; |
| |
| /** |
| * Provides window based implementation of {@link OnBackInvokedDispatcher}. |
| * <p> |
| * Callbacks with higher priorities receive back dispatching first. |
| * Within the same priority, callbacks receive back dispatching in the reverse order |
| * in which they are added. |
| * <p> |
| * When the top priority callback is updated, the new callback is propagated to the Window Manager |
| * if the window the instance is associated with has been attached. It is allowed to register / |
| * unregister {@link OnBackInvokedCallback}s before the window is attached, although |
| * callbacks will not receive dispatches until window attachment. |
| * |
| * @hide |
| */ |
| public class WindowOnBackInvokedDispatcher implements OnBackInvokedDispatcher { |
| private IWindowSession mWindowSession; |
| private IWindow mWindow; |
| @VisibleForTesting |
| public final BackTouchTracker mTouchTracker = new BackTouchTracker(); |
| @VisibleForTesting |
| public final BackProgressAnimator mProgressAnimator = new BackProgressAnimator(); |
| // The handler to run callbacks on. |
| // This should be on the same thread the ViewRootImpl holding this instance is created on. |
| @NonNull |
| private final Handler mHandler; |
| private static final String TAG = "WindowOnBackDispatcher"; |
| private static final boolean ENABLE_PREDICTIVE_BACK = SystemProperties |
| .getInt("persist.wm.debug.predictive_back", 1) != 0; |
| private static final boolean ALWAYS_ENFORCE_PREDICTIVE_BACK = SystemProperties |
| .getInt("persist.wm.debug.predictive_back_always_enforce", 0) != 0; |
| private static final boolean PREDICTIVE_BACK_FALLBACK_WINDOW_ATTRIBUTE = |
| SystemProperties.getInt("persist.wm.debug.predictive_back_fallback_window_attribute", 0) |
| != 0; |
| @Nullable |
| private ImeOnBackInvokedDispatcher mImeDispatcher; |
| |
| @Nullable |
| private ImeBackAnimationController mImeBackAnimationController; |
| |
| @GuardedBy("mLock") |
| /** Convenience hashmap to quickly decide if a callback has been added. */ |
| private final HashMap<OnBackInvokedCallback, Integer> mAllCallbacks = new HashMap<>(); |
| /** Holds all callbacks by priorities. */ |
| |
| @VisibleForTesting |
| @GuardedBy("mLock") |
| public final TreeMap<Integer, ArrayList<OnBackInvokedCallback>> |
| mOnBackInvokedCallbacks = new TreeMap<>(); |
| |
| private Checker mChecker; |
| private final Object mLock = new Object(); |
| // The threshold for back swipe full progress. |
| private float mBackSwipeLinearThreshold; |
| private float mNonLinearProgressFactor; |
| private boolean mImeDispatchingActive; |
| |
| public WindowOnBackInvokedDispatcher(@NonNull Context context, Looper looper) { |
| mChecker = new Checker(context); |
| mHandler = new Handler(looper); |
| } |
| |
| /** Updates the dispatcher state on a new {@link MotionEvent}. */ |
| public void onMotionEvent(MotionEvent ev) { |
| if (!isBackGestureInProgress() || ev == null || ev.getAction() != MotionEvent.ACTION_MOVE) { |
| return; |
| } |
| mTouchTracker.update(ev.getX(), ev.getY(), Float.NaN, Float.NaN); |
| if (mTouchTracker.shouldUpdateStartLocation()) { |
| // Reset the start location on the first event after starting back, so that |
| // the beginning of the animation feels smooth. |
| mTouchTracker.updateStartLocation(); |
| } |
| if (!mProgressAnimator.isBackAnimationInProgress()) { |
| return; |
| } |
| final BackMotionEvent backEvent = mTouchTracker.createProgressEvent(); |
| mProgressAnimator.onBackProgressed(backEvent); |
| } |
| |
| /** |
| * Sends the pending top callback (if one exists) to WM when the view root |
| * is attached a window. |
| */ |
| public void attachToWindow(@NonNull IWindowSession windowSession, @NonNull IWindow window, |
| @Nullable ImeBackAnimationController imeBackAnimationController) { |
| synchronized (mLock) { |
| mWindowSession = windowSession; |
| mWindow = window; |
| mImeBackAnimationController = imeBackAnimationController; |
| if (!mAllCallbacks.isEmpty()) { |
| setTopOnBackInvokedCallback(getTopCallback()); |
| } |
| } |
| } |
| |
| /** Detaches the dispatcher instance from its window. */ |
| public void detachFromWindow() { |
| synchronized (mLock) { |
| clear(); |
| mWindow = null; |
| mWindowSession = null; |
| mImeBackAnimationController = null; |
| } |
| } |
| |
| // TODO: Take an Executor for the callback to run on. |
| @Override |
| public void registerOnBackInvokedCallback( |
| @Priority int priority, @NonNull OnBackInvokedCallback callback) { |
| if (mChecker.checkApplicationCallbackRegistration(priority, callback)) { |
| registerOnBackInvokedCallbackUnchecked(callback, priority); |
| } |
| } |
| |
| /** |
| * Register a callback bypassing platform checks. This is used to register compatibility |
| * callbacks. |
| */ |
| public void registerOnBackInvokedCallbackUnchecked( |
| @NonNull OnBackInvokedCallback callback, @Priority int priority) { |
| synchronized (mLock) { |
| if (mImeDispatcher != null) { |
| mImeDispatcher.registerOnBackInvokedCallback(priority, callback); |
| return; |
| } |
| if ((callback instanceof ImeOnBackInvokedDispatcher.DefaultImeOnBackAnimationCallback |
| || callback instanceof ImeOnBackInvokedDispatcher.ImeOnBackInvokedCallback) |
| && !isOnBackInvokedCallbackEnabled()) { |
| // Fall back to compat back key injection if legacy back behaviour should be used. |
| return; |
| } |
| if (!mOnBackInvokedCallbacks.containsKey(priority)) { |
| mOnBackInvokedCallbacks.put(priority, new ArrayList<>()); |
| } |
| if (callback instanceof ImeOnBackInvokedDispatcher.DefaultImeOnBackAnimationCallback) { |
| callback = mImeBackAnimationController; |
| } |
| ArrayList<OnBackInvokedCallback> callbacks = mOnBackInvokedCallbacks.get(priority); |
| |
| // If callback has already been added, remove it and re-add it. |
| if (mAllCallbacks.containsKey(callback)) { |
| if (DEBUG) { |
| Log.i(TAG, "Callback already added. Removing and re-adding it."); |
| } |
| Integer prevPriority = mAllCallbacks.get(callback); |
| mOnBackInvokedCallbacks.get(prevPriority).remove(callback); |
| } |
| |
| OnBackInvokedCallback previousTopCallback = getTopCallback(); |
| callbacks.add(callback); |
| mAllCallbacks.put(callback, priority); |
| if (previousTopCallback == null |
| || (previousTopCallback != callback |
| && mAllCallbacks.get(previousTopCallback) <= priority)) { |
| setTopOnBackInvokedCallback(callback); |
| } |
| } |
| } |
| |
| @Override |
| public void unregisterOnBackInvokedCallback(@NonNull OnBackInvokedCallback callback) { |
| synchronized (mLock) { |
| if (mImeDispatcher != null) { |
| mImeDispatcher.unregisterOnBackInvokedCallback(callback); |
| return; |
| } |
| if (callback instanceof ImeOnBackInvokedDispatcher.DefaultImeOnBackAnimationCallback) { |
| callback = mImeBackAnimationController; |
| } |
| if (!mAllCallbacks.containsKey(callback)) { |
| if (DEBUG) { |
| Log.i(TAG, "Callback not found. returning..."); |
| } |
| return; |
| } |
| OnBackInvokedCallback previousTopCallback = getTopCallback(); |
| Integer priority = mAllCallbacks.get(callback); |
| ArrayList<OnBackInvokedCallback> callbacks = mOnBackInvokedCallbacks.get(priority); |
| callbacks.remove(callback); |
| if (callbacks.isEmpty()) { |
| mOnBackInvokedCallbacks.remove(priority); |
| } |
| mAllCallbacks.remove(callback); |
| // Re-populate the top callback to WM if the removed callback was previously the top |
| // one. |
| if (previousTopCallback == callback) { |
| // We should call onBackCancelled() when an active callback is removed from |
| // dispatcher. |
| sendCancelledIfInProgress(callback); |
| setTopOnBackInvokedCallback(getTopCallback()); |
| } |
| } |
| } |
| |
| /** |
| * Indicates if a user gesture is currently in progress. |
| */ |
| public boolean isBackGestureInProgress() { |
| synchronized (mLock) { |
| return mTouchTracker.isActive() || mImeDispatchingActive; |
| } |
| } |
| |
| private void sendCancelledIfInProgress(@NonNull OnBackInvokedCallback callback) { |
| boolean isInProgress = mProgressAnimator.isBackAnimationInProgress(); |
| if (isInProgress && callback instanceof OnBackAnimationCallback) { |
| OnBackAnimationCallback animatedCallback = (OnBackAnimationCallback) callback; |
| animatedCallback.onBackCancelled(); |
| if (DEBUG) { |
| Log.d(TAG, "sendCancelIfRunning: callback canceled"); |
| } |
| } else { |
| Log.w(TAG, "sendCancelIfRunning: isInProgress=" + isInProgress |
| + " callback=" + callback); |
| } |
| } |
| |
| @Override |
| public void registerSystemOnBackInvokedCallback(@NonNull OnBackInvokedCallback callback) { |
| registerOnBackInvokedCallbackUnchecked(callback, OnBackInvokedDispatcher.PRIORITY_SYSTEM); |
| } |
| |
| /** Clears all registered callbacks on the instance. */ |
| public void clear() { |
| synchronized (mLock) { |
| if (mImeDispatcher != null) { |
| mImeDispatcher.clear(); |
| mImeDispatcher = null; |
| } |
| if (!mAllCallbacks.isEmpty()) { |
| OnBackInvokedCallback topCallback = getTopCallback(); |
| if (topCallback != null) { |
| sendCancelledIfInProgress(topCallback); |
| } else { |
| // Should not be possible |
| Log.e(TAG, "There is no topCallback, even if mAllCallbacks is not empty"); |
| } |
| // Clear binder references in WM. |
| setTopOnBackInvokedCallback(null); |
| } |
| |
| // We should also stop running animations since all callbacks have been removed. |
| // note: mSpring.skipToEnd(), in ProgressAnimator.reset(), requires the main handler. |
| mHandler.post(mProgressAnimator::reset); |
| mAllCallbacks.clear(); |
| mOnBackInvokedCallbacks.clear(); |
| } |
| } |
| |
| private void setTopOnBackInvokedCallback(@Nullable OnBackInvokedCallback callback) { |
| if (mWindowSession == null || mWindow == null) { |
| return; |
| } |
| try { |
| OnBackInvokedCallbackInfo callbackInfo = null; |
| if (callback != null) { |
| int priority = mAllCallbacks.get(callback); |
| final IOnBackInvokedCallback iCallback = |
| callback instanceof ImeOnBackInvokedDispatcher |
| .ImeOnBackInvokedCallback |
| ? ((ImeOnBackInvokedDispatcher.ImeOnBackInvokedCallback) |
| callback).getIOnBackInvokedCallback() |
| : new OnBackInvokedCallbackWrapper( |
| callback, |
| mTouchTracker, |
| mProgressAnimator, |
| mHandler); |
| callbackInfo = new OnBackInvokedCallbackInfo( |
| iCallback, |
| priority, |
| callback instanceof OnBackAnimationCallback); |
| } |
| mWindowSession.setOnBackInvokedCallbackInfo(mWindow, callbackInfo); |
| } catch (RemoteException e) { |
| Log.e(TAG, "Failed to set OnBackInvokedCallback to WM. Error: " + e); |
| } |
| } |
| |
| public OnBackInvokedCallback getTopCallback() { |
| synchronized (mLock) { |
| if (mAllCallbacks.isEmpty()) { |
| return null; |
| } |
| for (Integer priority : mOnBackInvokedCallbacks.descendingKeySet()) { |
| ArrayList<OnBackInvokedCallback> callbacks = mOnBackInvokedCallbacks.get(priority); |
| if (!callbacks.isEmpty()) { |
| return callbacks.get(callbacks.size() - 1); |
| } |
| } |
| } |
| return null; |
| } |
| |
| /** |
| * The {@link Context} in ViewRootImp and Activity could be different, this will make sure it |
| * could update the checker condition base on the real context when binding the proxy |
| * dispatcher in PhoneWindow. |
| */ |
| public void updateContext(@NonNull Context context) { |
| mChecker = new Checker(context); |
| // Set swipe threshold values. |
| Resources res = context.getResources(); |
| mBackSwipeLinearThreshold = |
| res.getDimension(R.dimen.navigation_edge_action_progress_threshold); |
| TypedValue typedValue = new TypedValue(); |
| res.getValue(R.dimen.back_progress_non_linear_factor, typedValue, true); |
| mNonLinearProgressFactor = typedValue.getFloat(); |
| onConfigurationChanged(context.getResources().getConfiguration()); |
| } |
| |
| /** Updates the threshold values for computing progress. */ |
| public void onConfigurationChanged(Configuration configuration) { |
| float maxDistance = configuration.windowConfiguration.getMaxBounds().width(); |
| float linearDistance = Math.min(maxDistance, mBackSwipeLinearThreshold); |
| mTouchTracker.setProgressThresholds( |
| linearDistance, maxDistance, mNonLinearProgressFactor); |
| if (mImeDispatcher != null) { |
| mImeDispatcher.setProgressThresholds( |
| linearDistance, maxDistance, mNonLinearProgressFactor); |
| } |
| } |
| |
| /** |
| * Returns false if the legacy back behavior should be used. |
| */ |
| public boolean isOnBackInvokedCallbackEnabled() { |
| return isOnBackInvokedCallbackEnabled(mChecker.getContext()); |
| } |
| |
| /** |
| * Dump information about this WindowOnBackInvokedDispatcher |
| * @param prefix the prefix that will be prepended to each line of the produced output |
| * @param writer the writer that will receive the resulting text |
| */ |
| public void dump(String prefix, PrintWriter writer) { |
| String innerPrefix = prefix + " "; |
| writer.println(prefix + "WindowOnBackDispatcher:"); |
| synchronized (mLock) { |
| if (mAllCallbacks.isEmpty()) { |
| writer.println(prefix + "<None>"); |
| return; |
| } |
| |
| writer.println(innerPrefix + "Top Callback: " + getTopCallback()); |
| writer.println(innerPrefix + "Callbacks: "); |
| mAllCallbacks.forEach((callback, priority) -> { |
| writer.println(innerPrefix + " Callback: " + callback + " Priority=" + priority); |
| }); |
| } |
| } |
| |
| /** |
| * Called when we start dispatching to a callback registered from IME. |
| */ |
| public void onStartImeDispatching() { |
| synchronized (mLock) { |
| mImeDispatchingActive = true; |
| } |
| } |
| |
| /** |
| * Called when we stop dispatching to a callback registered from IME. |
| */ |
| public void onStopImeDispatching() { |
| synchronized (mLock) { |
| mImeDispatchingActive = false; |
| } |
| } |
| |
| static class OnBackInvokedCallbackWrapper extends IOnBackInvokedCallback.Stub { |
| static class CallbackRef { |
| final WeakReference<OnBackInvokedCallback> mWeakRef; |
| final OnBackInvokedCallback mStrongRef; |
| CallbackRef(@NonNull OnBackInvokedCallback callback, boolean useWeakRef) { |
| if (useWeakRef) { |
| mWeakRef = new WeakReference<>(callback); |
| mStrongRef = null; |
| } else { |
| mStrongRef = callback; |
| mWeakRef = null; |
| } |
| } |
| |
| OnBackInvokedCallback get() { |
| if (mStrongRef != null) { |
| return mStrongRef; |
| } |
| return mWeakRef.get(); |
| } |
| } |
| final CallbackRef mCallbackRef; |
| @NonNull |
| private final BackProgressAnimator mProgressAnimator; |
| @NonNull |
| private final BackTouchTracker mTouchTracker; |
| @NonNull |
| private final Handler mHandler; |
| |
| OnBackInvokedCallbackWrapper( |
| @NonNull OnBackInvokedCallback callback, |
| @NonNull BackTouchTracker touchTracker, |
| @NonNull BackProgressAnimator progressAnimator, |
| @NonNull Handler handler) { |
| mCallbackRef = new CallbackRef(callback, true /* useWeakRef */); |
| mTouchTracker = touchTracker; |
| mProgressAnimator = progressAnimator; |
| mHandler = handler; |
| } |
| |
| OnBackInvokedCallbackWrapper( |
| @NonNull OnBackInvokedCallback callback, |
| @NonNull BackTouchTracker touchTracker, |
| @NonNull BackProgressAnimator progressAnimator, |
| @NonNull Handler handler, |
| boolean useWeakRef) { |
| mCallbackRef = new CallbackRef(callback, useWeakRef); |
| mTouchTracker = touchTracker; |
| mProgressAnimator = progressAnimator; |
| mHandler = handler; |
| } |
| |
| @Override |
| public void onBackStarted(BackMotionEvent backEvent) { |
| mHandler.post(() -> { |
| final OnBackAnimationCallback callback = getBackAnimationCallback(); |
| |
| // reset progress animator before dispatching onBackStarted to callback. This |
| // ensures that onBackCancelled (of a previous gesture) is always dispatched |
| // before onBackStarted |
| if (callback != null) mProgressAnimator.reset(); |
| mTouchTracker.setState(BackTouchTracker.TouchTrackerState.ACTIVE); |
| mTouchTracker.setShouldUpdateStartLocation(true); |
| mTouchTracker.setGestureStartLocation( |
| backEvent.getTouchX(), backEvent.getTouchY(), backEvent.getSwipeEdge()); |
| |
| if (callback != null) { |
| callback.onBackStarted(new BackEvent( |
| backEvent.getTouchX(), |
| backEvent.getTouchY(), |
| backEvent.getProgress(), |
| backEvent.getSwipeEdge())); |
| mProgressAnimator.onBackStarted(backEvent, callback::onBackProgressed); |
| } |
| }); |
| } |
| |
| @Override |
| public void onBackProgressed(BackMotionEvent backEvent) { } |
| |
| @Override |
| public void onBackCancelled() { |
| mHandler.post(() -> { |
| final OnBackAnimationCallback callback = getBackAnimationCallback(); |
| mTouchTracker.reset(); |
| if (callback == null) return; |
| mProgressAnimator.onBackCancelled(callback::onBackCancelled); |
| }); |
| } |
| |
| @Override |
| public void onBackInvoked() throws RemoteException { |
| mHandler.post(() -> { |
| mTouchTracker.reset(); |
| boolean isInProgress = mProgressAnimator.isBackAnimationInProgress(); |
| mProgressAnimator.reset(); |
| // TODO(b/333957271): Re-introduce auto fling progress generation. |
| final OnBackInvokedCallback callback = mCallbackRef.get(); |
| if (callback == null) { |
| Log.d(TAG, "Trying to call onBackInvoked() on a null callback reference."); |
| return; |
| } |
| if (callback instanceof OnBackAnimationCallback && !isInProgress) { |
| Log.w(TAG, "ProgressAnimator was not in progress, skip onBackInvoked()."); |
| return; |
| } |
| callback.onBackInvoked(); |
| }); |
| } |
| |
| @Override |
| public void setTriggerBack(boolean triggerBack) throws RemoteException { |
| mTouchTracker.setTriggerBack(triggerBack); |
| } |
| |
| @Nullable |
| private OnBackAnimationCallback getBackAnimationCallback() { |
| OnBackInvokedCallback callback = mCallbackRef.get(); |
| return callback instanceof OnBackAnimationCallback ? (OnBackAnimationCallback) callback |
| : null; |
| } |
| } |
| |
| /** |
| * Returns false if the legacy back behavior should be used. |
| * <p> |
| * Legacy back behavior dispatches KEYCODE_BACK instead of invoking the application registered |
| * {@link OnBackInvokedCallback}. |
| */ |
| public static boolean isOnBackInvokedCallbackEnabled(@NonNull Context context) { |
| final Context originalContext = context; |
| while ((context instanceof ContextWrapper) && !(context instanceof Activity)) { |
| context = ((ContextWrapper) context).getBaseContext(); |
| } |
| final ActivityInfo activityInfo = (context instanceof Activity) |
| ? ((Activity) context).getActivityInfo() : null; |
| final ApplicationInfo applicationInfo = context.getApplicationInfo(); |
| |
| return WindowOnBackInvokedDispatcher |
| .isOnBackInvokedCallbackEnabled(activityInfo, applicationInfo, |
| () -> originalContext); |
| } |
| |
| @Override |
| public void setImeOnBackInvokedDispatcher( |
| @NonNull ImeOnBackInvokedDispatcher imeDispatcher) { |
| mImeDispatcher = imeDispatcher; |
| mImeDispatcher.setHandler(mHandler); |
| mImeDispatcher.setProgressThresholds( |
| mTouchTracker.getLinearDistance(), |
| mTouchTracker.getMaxDistance(), |
| mTouchTracker.getNonLinearFactor()); |
| } |
| |
| /** Returns true if a non-null {@link ImeOnBackInvokedDispatcher} has been set. **/ |
| public boolean hasImeOnBackInvokedDispatcher() { |
| return mImeDispatcher != null; |
| } |
| |
| /** |
| * Class used to check whether a callback can be registered or not. This is meant to be |
| * shared with {@link ProxyOnBackInvokedDispatcher} which needs to do the same checks. |
| */ |
| public static class Checker { |
| private WeakReference<Context> mContext; |
| |
| public Checker(@NonNull Context context) { |
| mContext = new WeakReference<>(context); |
| } |
| |
| /** |
| * Checks whether the given callback can be registered with the given priority. |
| * @return true if the callback can be added. |
| * @throws IllegalArgumentException if the priority is negative. |
| */ |
| public boolean checkApplicationCallbackRegistration(int priority, |
| OnBackInvokedCallback callback) { |
| if (!WindowOnBackInvokedDispatcher.isOnBackInvokedCallbackEnabled(getContext()) |
| && !(callback instanceof CompatOnBackInvokedCallback)) { |
| Log.w(TAG, |
| "OnBackInvokedCallback is not enabled for the application." |
| + "\nSet 'android:enableOnBackInvokedCallback=\"true\"' in the" |
| + " application manifest."); |
| return false; |
| } |
| if (priority < 0) { |
| throw new IllegalArgumentException("Application registered OnBackInvokedCallback " |
| + "cannot have negative priority. Priority: " + priority); |
| } |
| Objects.requireNonNull(callback); |
| return true; |
| } |
| |
| private Context getContext() { |
| return mContext.get(); |
| } |
| } |
| |
| /** |
| * @hide |
| */ |
| public static boolean isOnBackInvokedCallbackEnabled(@Nullable ActivityInfo activityInfo, |
| @NonNull ApplicationInfo applicationInfo, |
| @NonNull Supplier<Context> contextSupplier) { |
| // new back is enabled if the feature flag is enabled AND the app does not explicitly |
| // request legacy back. |
| if (!ENABLE_PREDICTIVE_BACK) { |
| return false; |
| } |
| |
| if (ALWAYS_ENFORCE_PREDICTIVE_BACK) { |
| return true; |
| } |
| |
| boolean requestsPredictiveBack; |
| // Activity |
| if (activityInfo != null && activityInfo.hasOnBackInvokedCallbackEnabled()) { |
| requestsPredictiveBack = activityInfo.isOnBackInvokedCallbackEnabled(); |
| if (DEBUG) { |
| Log.d(TAG, TextUtils.formatSimple( |
| "Activity: %s isPredictiveBackEnabled=%s", |
| activityInfo.getComponentName(), |
| requestsPredictiveBack)); |
| } |
| return requestsPredictiveBack; |
| } |
| |
| // Application |
| requestsPredictiveBack = applicationInfo.isOnBackInvokedCallbackEnabled(); |
| if (DEBUG) { |
| Log.d(TAG, TextUtils.formatSimple("App: %s requestsPredictiveBack=%s", |
| applicationInfo.packageName, |
| requestsPredictiveBack)); |
| } |
| if (requestsPredictiveBack) { |
| return true; |
| } |
| |
| if (PREDICTIVE_BACK_FALLBACK_WINDOW_ATTRIBUTE) { |
| // Compatibility check for legacy window style flag used by Wear OS. |
| // Note on compatibility behavior: |
| // 1. windowSwipeToDismiss should be respected for all apps not opted in. |
| // 2. windowSwipeToDismiss should be true for all apps not opted in, which |
| // enables the PB animation for them. |
| // 3. windowSwipeToDismiss=false should be respected for apps not opted in, |
| // which disables PB & onBackPressed caused by BackAnimController's |
| // setTrigger(true) |
| // Use the original context to resolve the styled attribute so that they stay |
| // true to the window. |
| final Context context = contextSupplier.get(); |
| boolean windowSwipeToDismiss = true; |
| if (context != null) { |
| final TypedArray array = context.obtainStyledAttributes( |
| new int[]{android.R.attr.windowSwipeToDismiss}); |
| if (array.getIndexCount() > 0) { |
| windowSwipeToDismiss = array.getBoolean(0, true); |
| } |
| array.recycle(); |
| } |
| |
| if (DEBUG) { |
| Log.i(TAG, "falling back to windowSwipeToDismiss: " + windowSwipeToDismiss); |
| } |
| |
| requestsPredictiveBack = windowSwipeToDismiss; |
| } |
| return requestsPredictiveBack; |
| } |
| } |