/* * Copyright (C) 2022 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 static com.android.internal.annotations.VisibleForTesting.Visibility.PACKAGE; import android.annotation.NonNull; import android.annotation.Nullable; import android.os.Bundle; import android.os.Handler; import android.os.Parcel; import android.os.Parcelable; import android.os.RemoteException; import android.os.ResultReceiver; import android.util.Log; import android.view.ViewRootImpl; import com.android.internal.annotations.VisibleForTesting; import java.util.ArrayList; /** * A {@link OnBackInvokedDispatcher} for IME that forwards {@link OnBackInvokedCallback} * registrations from the IME process to the app process to be registered on the app window. *

* The app process creates and propagates an instance of {@link ImeOnBackInvokedDispatcher} * to the IME to be set on the IME window's {@link WindowOnBackInvokedDispatcher}. *

* @see WindowOnBackInvokedDispatcher#setImeOnBackInvokedDispatcher * * @hide */ public class ImeOnBackInvokedDispatcher implements OnBackInvokedDispatcher, Parcelable { private static final String TAG = "ImeBackDispatcher"; static final String RESULT_KEY_ID = "id"; static final String RESULT_KEY_CALLBACK = "callback"; static final String RESULT_KEY_PRIORITY = "priority"; static final int RESULT_CODE_REGISTER = 0; static final int RESULT_CODE_UNREGISTER = 1; static final int RESULT_CODE_START_DISPATCHING = 2; static final int RESULT_CODE_STOP_DISPATCHING = 3; @NonNull private final ResultReceiver mResultReceiver; @NonNull private final BackProgressAnimator mProgressAnimator = new BackProgressAnimator(); @NonNull private final BackTouchTracker mTouchTracker = new BackTouchTracker(); // The handler to run callbacks on. This should be on the same thread // the ViewRootImpl holding IME's WindowOnBackInvokedDispatcher is created on. private Handler mHandler; public ImeOnBackInvokedDispatcher(Handler handler) { mResultReceiver = new ResultReceiver(handler) { @Override public void onReceiveResult(int resultCode, Bundle resultData) { WindowOnBackInvokedDispatcher dispatcher = getReceivingDispatcher(); if (dispatcher != null) { receive(resultCode, resultData, dispatcher); } } }; } /** * Override this method to return the {@link WindowOnBackInvokedDispatcher} of the window * that should receive the forwarded callback. */ @Nullable protected WindowOnBackInvokedDispatcher getReceivingDispatcher() { return null; } ImeOnBackInvokedDispatcher(Parcel in) { mResultReceiver = in.readTypedObject(ResultReceiver.CREATOR); } void setHandler(@NonNull Handler handler) { mHandler = handler; } @Override public void registerOnBackInvokedCallback( @OnBackInvokedDispatcher.Priority int priority, @NonNull OnBackInvokedCallback callback) { final Bundle bundle = new Bundle(); // Always invoke back for ime without checking the window focus. // We use strong reference in the binder wrapper to avoid accidentally GC the callback. // This is necessary because the callback is sent to and registered from // the app process, which may treat the IME callback as weakly referenced. This will not // cause a memory leak because the app side already clears the reference correctly. final IOnBackInvokedCallback iCallback = new ImeOnBackInvokedCallbackWrapper( callback, mTouchTracker, mProgressAnimator, this, mHandler != null ? mHandler : Handler.getMain(), false /* useWeakRef */); bundle.putBinder(RESULT_KEY_CALLBACK, iCallback.asBinder()); bundle.putInt(RESULT_KEY_PRIORITY, priority); bundle.putInt(RESULT_KEY_ID, callback.hashCode()); mResultReceiver.send(RESULT_CODE_REGISTER, bundle); } @Override public void unregisterOnBackInvokedCallback( @NonNull OnBackInvokedCallback callback) { Bundle bundle = new Bundle(); bundle.putInt(RESULT_KEY_ID, callback.hashCode()); mResultReceiver.send(RESULT_CODE_UNREGISTER, bundle); } @Override public int describeContents() { return 0; } @Override public void writeToParcel(@NonNull Parcel dest, int flags) { dest.writeTypedObject(mResultReceiver, flags); } /** Sets the progress thresholds for touch tracking */ public void setProgressThresholds(float linearDistance, float maxDistance, float nonLinearFactor) { mTouchTracker.setProgressThresholds(linearDistance, maxDistance, nonLinearFactor); } @NonNull public static final Parcelable.Creator CREATOR = new Parcelable.Creator() { public ImeOnBackInvokedDispatcher createFromParcel(Parcel in) { return new ImeOnBackInvokedDispatcher(in); } public ImeOnBackInvokedDispatcher[] newArray(int size) { return new ImeOnBackInvokedDispatcher[size]; } }; private final ArrayList mImeCallbacks = new ArrayList<>(); private void receive( int resultCode, Bundle resultData, @NonNull WindowOnBackInvokedDispatcher receivingDispatcher) { if (resultCode == RESULT_CODE_REGISTER) { final int callbackId = resultData.getInt(RESULT_KEY_ID); int priority = resultData.getInt(RESULT_KEY_PRIORITY); final IOnBackInvokedCallback callback = IOnBackInvokedCallback.Stub.asInterface( resultData.getBinder(RESULT_KEY_CALLBACK)); registerReceivedCallback( callback, priority, callbackId, receivingDispatcher); } else if (resultCode == RESULT_CODE_UNREGISTER) { final int callbackId = resultData.getInt(RESULT_KEY_ID); unregisterReceivedCallback(callbackId, receivingDispatcher); } else if (resultCode == RESULT_CODE_START_DISPATCHING) { receiveStartDispatching(receivingDispatcher); } else if (resultCode == RESULT_CODE_STOP_DISPATCHING) { receiveStopDispatching(receivingDispatcher); } } private void registerReceivedCallback( @NonNull IOnBackInvokedCallback iCallback, @OnBackInvokedDispatcher.Priority int priority, int callbackId, @NonNull WindowOnBackInvokedDispatcher receivingDispatcher) { final ImeOnBackInvokedCallback imeCallback; if (priority == PRIORITY_SYSTEM) { // A callback registration with PRIORITY_SYSTEM indicates that a predictive back // animation can be played on the IME. Therefore register the // DefaultImeOnBackInvokedCallback with the receiving dispatcher and override the // priority to PRIORITY_DEFAULT. priority = PRIORITY_DEFAULT; imeCallback = new DefaultImeOnBackAnimationCallback(iCallback, callbackId, priority); } else { imeCallback = new ImeOnBackInvokedCallback(iCallback, callbackId, priority); } mImeCallbacks.add(imeCallback); receivingDispatcher.registerOnBackInvokedCallbackUnchecked(imeCallback, priority); } private void unregisterReceivedCallback( int callbackId, OnBackInvokedDispatcher receivingDispatcher) { ImeOnBackInvokedCallback callback = null; for (ImeOnBackInvokedCallback imeCallback : mImeCallbacks) { if (imeCallback.getId() == callbackId) { callback = imeCallback; break; } } if (callback == null) { Log.e(TAG, "Ime callback not found. Ignoring unregisterReceivedCallback. " + "callbackId: " + callbackId); return; } receivingDispatcher.unregisterOnBackInvokedCallback(callback); mImeCallbacks.remove(callback); } static class ImeOnBackInvokedCallbackWrapper extends WindowOnBackInvokedDispatcher.OnBackInvokedCallbackWrapper { @NonNull private final ImeOnBackInvokedDispatcher mDispatcher; ImeOnBackInvokedCallbackWrapper( @NonNull OnBackInvokedCallback callback, @NonNull BackTouchTracker touchTracker, @NonNull BackProgressAnimator progressAnimator, @NonNull ImeOnBackInvokedDispatcher dispatcher, @NonNull Handler handler, boolean useWeakRef) { super(callback, touchTracker, progressAnimator, handler, useWeakRef); mDispatcher = dispatcher; } @Override public void onBackStarted(BackMotionEvent backEvent) { super.onBackStarted(backEvent); mDispatcher.sendStartDispatching(); } @Override public void onBackCancelled() { super.onBackCancelled(); mDispatcher.sendStopDispatching(); } @Override public void onBackInvoked() throws RemoteException { super.onBackInvoked(); mDispatcher.sendStopDispatching(); } } /** Notifies the app process that we've stopped dispatching to an IME callback */ private void sendStopDispatching() { mResultReceiver.send(RESULT_CODE_STOP_DISPATCHING, null /* unused bundle */); } /** Notifies the app process that we've started dispatching to an IME callback */ private void sendStartDispatching() { mResultReceiver.send(RESULT_CODE_START_DISPATCHING, null /* unused bundle */); } /** Receives IME's message that dispatching has started. */ private void receiveStopDispatching( @NonNull WindowOnBackInvokedDispatcher receivingDispatcher) { receivingDispatcher.onStopImeDispatching(); } /** Receives IME's message that dispatching has stopped. */ private void receiveStartDispatching( @NonNull WindowOnBackInvokedDispatcher receivingDispatcher) { receivingDispatcher.onStartImeDispatching(); } /** Clears all registered callbacks on the instance. */ public void clear() { // Unregister previously registered callbacks if there's any. if (getReceivingDispatcher() != null) { for (ImeOnBackInvokedCallback callback : mImeCallbacks) { getReceivingDispatcher().unregisterOnBackInvokedCallback(callback); } } mImeCallbacks.clear(); // We should also stop running animations since all callbacks have been removed. // note: mSpring.skipToEnd(), in ProgressAnimator.reset(), requires the main handler. Handler.getMain().post(mProgressAnimator::reset); sendStopDispatching(); } @VisibleForTesting(visibility = PACKAGE) public static class ImeOnBackInvokedCallback implements OnBackInvokedCallback { @NonNull private final IOnBackInvokedCallback mIOnBackInvokedCallback; /** * The hashcode of the callback instance in the IME process, used as a unique id to * identify the callback when it's passed between processes. */ private final int mId; private final int mPriority; ImeOnBackInvokedCallback(@NonNull IOnBackInvokedCallback iCallback, int id, @Priority int priority) { mIOnBackInvokedCallback = iCallback; mId = id; mPriority = priority; } @Override public void onBackInvoked() { try { if (mIOnBackInvokedCallback != null) { mIOnBackInvokedCallback.onBackInvoked(); } } catch (RemoteException e) { Log.e(TAG, "Exception when invoking forwarded callback. e: ", e); } } private int getId() { return mId; } IOnBackInvokedCallback getIOnBackInvokedCallback() { return mIOnBackInvokedCallback; } @Override public String toString() { return "ImeCallback=ImeOnBackInvokedCallback@" + mId + " Callback=" + mIOnBackInvokedCallback; } } /** * Subclass of ImeOnBackInvokedCallback indicating that a predictive IME back animation may be * played instead of invoking the callback. */ @VisibleForTesting(visibility = PACKAGE) public static class DefaultImeOnBackAnimationCallback extends ImeOnBackInvokedCallback { DefaultImeOnBackAnimationCallback(@NonNull IOnBackInvokedCallback iCallback, int id, int priority) { super(iCallback, id, priority); } } /** * Transfers {@link ImeOnBackInvokedCallback}s registered on one {@link ViewRootImpl} to * another {@link ViewRootImpl} on focus change. * * @param previous the previously focused {@link ViewRootImpl}. * @param current the currently focused {@link ViewRootImpl}. */ public void switchRootView(ViewRootImpl previous, ViewRootImpl current) { for (ImeOnBackInvokedCallback imeCallback : mImeCallbacks) { if (previous != null) { previous.getOnBackInvokedDispatcher().unregisterOnBackInvokedCallback(imeCallback); } if (current != null) { current.getOnBackInvokedDispatcher().registerOnBackInvokedCallbackUnchecked( imeCallback, imeCallback.mPriority); } } } }