692 lines
28 KiB
Java
692 lines
28 KiB
Java
/*
|
|
* 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;
|
|
}
|
|
}
|