/*
* 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;
import java.util.function.Consumer;
/**
* 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;
@NonNull
private final ResultReceiver mResultReceiver;
// 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);
}
}
};
}
void setHandler(@NonNull Handler handler) {
mHandler = handler;
}
/**
* 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);
}
@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);
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);
}
@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);
}
}
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);
}
/** 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();
}
@VisibleForTesting(visibility = PACKAGE)
public static class ImeOnBackInvokedCallback implements OnBackAnimationCallback {
@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 onBackStarted(@NonNull BackEvent backEvent) {
try {
mIOnBackInvokedCallback.onBackStarted(
new BackMotionEvent(backEvent.getTouchX(), backEvent.getTouchY(),
backEvent.getProgress(), 0f, 0f, false, backEvent.getSwipeEdge(),
null));
} catch (RemoteException e) {
Log.e(TAG, "Exception when invoking forwarded callback. e: ", e);
}
}
@Override
public void onBackProgressed(@NonNull BackEvent backEvent) {
try {
mIOnBackInvokedCallback.onBackProgressed(
new BackMotionEvent(backEvent.getTouchX(), backEvent.getTouchY(),
backEvent.getProgress(), 0f, 0f, false, backEvent.getSwipeEdge(),
null));
} catch (RemoteException e) {
Log.e(TAG, "Exception when invoking forwarded callback. e: ", e);
}
}
@Override
public void onBackInvoked() {
try {
mIOnBackInvokedCallback.onBackInvoked();
} catch (RemoteException e) {
Log.e(TAG, "Exception when invoking forwarded callback. e: ", e);
}
}
@Override
public void onBackCancelled() {
try {
mIOnBackInvokedCallback.onBackCancelled();
} catch (RemoteException e) {
Log.e(TAG, "Exception when invoking forwarded callback. e: ", e);
}
}
private int getId() {
return mId;
}
@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);
}
}
}
/**
* Wrapper class that wraps an OnBackInvokedCallback. This is used when a callback is sent from
* the IME process to the app process.
*/
private class ImeOnBackInvokedCallbackWrapper extends IOnBackInvokedCallback.Stub {
private final OnBackInvokedCallback mCallback;
ImeOnBackInvokedCallbackWrapper(@NonNull OnBackInvokedCallback callback) {
mCallback = callback;
}
@Override
public void onBackStarted(BackMotionEvent backMotionEvent) {
maybeRunOnAnimationCallback((animationCallback) -> animationCallback.onBackStarted(
BackEvent.fromBackMotionEvent(backMotionEvent)));
}
@Override
public void onBackProgressed(BackMotionEvent backMotionEvent) {
maybeRunOnAnimationCallback((animationCallback) -> animationCallback.onBackProgressed(
BackEvent.fromBackMotionEvent(backMotionEvent)));
}
@Override
public void onBackCancelled() {
maybeRunOnAnimationCallback(OnBackAnimationCallback::onBackCancelled);
}
@Override
public void onBackInvoked() {
mHandler.post(mCallback::onBackInvoked);
}
@Override
public void setTriggerBack(boolean triggerBack) {
// no-op
}
private void maybeRunOnAnimationCallback(Consumer block) {
if (mCallback instanceof OnBackAnimationCallback) {
mHandler.post(() -> block.accept((OnBackAnimationCallback) mCallback));
}
}
}
}