| /* |
| * Copyright (C) 2020 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 com.android.server.autofill.ui; |
| |
| import static com.android.server.autofill.Helper.sDebug; |
| import static com.android.server.autofill.Helper.sVerbose; |
| |
| import android.annotation.NonNull; |
| import android.annotation.Nullable; |
| import android.content.IntentSender; |
| import android.os.Handler; |
| import android.os.IBinder; |
| import android.os.RemoteException; |
| import android.service.autofill.IInlineSuggestionUi; |
| import android.service.autofill.IInlineSuggestionUiCallback; |
| import android.service.autofill.ISurfacePackageResultCallback; |
| import android.util.Slog; |
| import android.view.SurfaceControlViewHost; |
| |
| import com.android.internal.view.inline.IInlineContentCallback; |
| |
| /** |
| * The instance of this class lives in the system server, orchestrating the communication between |
| * the remote process owning embedded view (i.e. ExtServices) and the remote process hosting the |
| * embedded view (i.e. IME). It's also responsible for releasing the embedded view from the owning |
| * process when it's not longer needed in the hosting process. |
| * |
| * <p>An instance of this class may be reused to associate with multiple instances of |
| * {@link InlineContentProviderImpl}s, each of which wraps a callback from the IME. But at any |
| * given time, there is only one active IME callback which this class will callback into. |
| * |
| * <p>This class is thread safe, because all the outside calls are piped into a single handler |
| * thread to be processed. |
| */ |
| final class RemoteInlineSuggestionUi { |
| |
| private static final String TAG = RemoteInlineSuggestionUi.class.getSimpleName(); |
| |
| // The delay time to release the remote inline suggestion view (in the renderer |
| // process) after receiving a signal about the surface package being released due to being |
| // detached from the window in the host app (in the IME process). The release will be |
| // canceled if the host app reattaches the view to a window within this delay time. |
| // TODO(b/154683107): try out using the Chroreographer to schedule the release right at the |
| // next frame. Basically if the view is not re-attached to the window immediately in the next |
| // frame after it was detached, then it will be released. |
| private static final long RELEASE_REMOTE_VIEW_HOST_DELAY_MS = 200; |
| |
| @NonNull |
| private final Handler mHandler; |
| @NonNull |
| private final RemoteInlineSuggestionViewConnector mRemoteInlineSuggestionViewConnector; |
| private final int mWidth; |
| private final int mHeight; |
| @NonNull |
| private final InlineSuggestionUiCallbackImpl mInlineSuggestionUiCallback; |
| |
| @Nullable |
| private IInlineContentCallback mInlineContentCallback; // from IME |
| |
| /** |
| * Remote inline suggestion view, backed by an instance of {@link SurfaceControlViewHost} in |
| * the render service process. We takes care of releasing it when there is no remote |
| * reference to it (from IME), and we will create a new instance of the view when it's needed |
| * by IME again. |
| */ |
| @Nullable |
| private IInlineSuggestionUi mInlineSuggestionUi; |
| private int mRefCount = 0; |
| private boolean mWaitingForUiCreation = false; |
| private int mActualWidth; |
| private int mActualHeight; |
| |
| @Nullable |
| private Runnable mDelayedReleaseViewRunnable; |
| |
| RemoteInlineSuggestionUi( |
| @NonNull RemoteInlineSuggestionViewConnector remoteInlineSuggestionViewConnector, |
| int width, int height, Handler handler) { |
| mHandler = handler; |
| mRemoteInlineSuggestionViewConnector = remoteInlineSuggestionViewConnector; |
| mWidth = width; |
| mHeight = height; |
| mInlineSuggestionUiCallback = new InlineSuggestionUiCallbackImpl(); |
| } |
| |
| /** |
| * Updates the callback from the IME process. It'll swap out the previous IME callback, and |
| * all the subsequent callback events (onClick, onLongClick, touch event transfer, etc) will |
| * be directed to the new callback. |
| */ |
| void setInlineContentCallback(@NonNull IInlineContentCallback inlineContentCallback) { |
| mHandler.post(() -> { |
| mInlineContentCallback = inlineContentCallback; |
| }); |
| } |
| |
| /** |
| * Handles the request from the IME process to get a new surface package. May create a new |
| * view in the renderer process if the existing view is already released. |
| */ |
| void requestSurfacePackage() { |
| mHandler.post(this::handleRequestSurfacePackage); |
| } |
| |
| /** |
| * Handles the signal from the IME process that the previously sent surface package has been |
| * released. |
| */ |
| void surfacePackageReleased() { |
| mHandler.post(() -> handleUpdateRefCount(-1)); |
| } |
| |
| /** |
| * Returns true if the provided size matches the remote view's size. |
| */ |
| boolean match(int width, int height) { |
| return mWidth == width && mHeight == height; |
| } |
| |
| private void handleRequestSurfacePackage() { |
| cancelPendingReleaseViewRequest(); |
| |
| if (mInlineSuggestionUi == null) { |
| if (mWaitingForUiCreation) { |
| // This could happen in the following case: the remote embedded view was released |
| // when previously detached from window. An event after that to re-attached to |
| // the window will cause us calling the renderSuggestion again. Now, before the |
| // render call returns a new surface package, if the view is detached and |
| // re-attached to the window, causing this method to be called again, we will get |
| // to this state. This request will be ignored and the surface package will still |
| // be sent back once the view is rendered. |
| if (sDebug) Slog.d(TAG, "Inline suggestion ui is not ready"); |
| } else { |
| mRemoteInlineSuggestionViewConnector.renderSuggestion(mWidth, mHeight, |
| mInlineSuggestionUiCallback); |
| mWaitingForUiCreation = true; |
| } |
| } else { |
| try { |
| mInlineSuggestionUi.getSurfacePackage(new ISurfacePackageResultCallback.Stub() { |
| @Override |
| public void onResult(SurfaceControlViewHost.SurfacePackage result) { |
| mHandler.post(() -> { |
| if (sVerbose) Slog.v(TAG, "Sending refreshed SurfacePackage to IME"); |
| try { |
| mInlineContentCallback.onContent(result, mActualWidth, |
| mActualHeight); |
| handleUpdateRefCount(1); |
| } catch (RemoteException e) { |
| Slog.w(TAG, "RemoteException calling onContent"); |
| } |
| }); |
| } |
| }); |
| } catch (RemoteException e) { |
| Slog.w(TAG, "RemoteException calling getSurfacePackage."); |
| } |
| } |
| } |
| |
| private void handleUpdateRefCount(int delta) { |
| cancelPendingReleaseViewRequest(); |
| mRefCount += delta; |
| if (mRefCount <= 0) { |
| mDelayedReleaseViewRunnable = () -> { |
| if (mInlineSuggestionUi != null) { |
| try { |
| if (sVerbose) Slog.v(TAG, "releasing the host"); |
| mInlineSuggestionUi.releaseSurfaceControlViewHost(); |
| mInlineSuggestionUi = null; |
| } catch (RemoteException e) { |
| Slog.w(TAG, "RemoteException calling releaseSurfaceControlViewHost"); |
| } |
| } |
| mDelayedReleaseViewRunnable = null; |
| }; |
| mHandler.postDelayed(mDelayedReleaseViewRunnable, RELEASE_REMOTE_VIEW_HOST_DELAY_MS); |
| } |
| } |
| |
| private void cancelPendingReleaseViewRequest() { |
| if (mDelayedReleaseViewRunnable != null) { |
| mHandler.removeCallbacks(mDelayedReleaseViewRunnable); |
| mDelayedReleaseViewRunnable = null; |
| } |
| } |
| |
| /** |
| * This is called when a new inline suggestion UI is inflated from the ext services. |
| */ |
| private void handleInlineSuggestionUiReady(IInlineSuggestionUi content, |
| SurfaceControlViewHost.SurfacePackage surfacePackage, int width, int height) { |
| mInlineSuggestionUi = content; |
| mRefCount = 0; |
| mWaitingForUiCreation = false; |
| mActualWidth = width; |
| mActualHeight = height; |
| if (mInlineContentCallback != null) { |
| try { |
| if (sVerbose) Slog.v(TAG, "Sending new UI content to IME"); |
| handleUpdateRefCount(1); |
| mInlineContentCallback.onContent(surfacePackage, mActualWidth, mActualHeight); |
| } catch (RemoteException e) { |
| Slog.w(TAG, "RemoteException calling onContent"); |
| } |
| } |
| if (surfacePackage != null) { |
| surfacePackage.release(); |
| } |
| } |
| |
| private void handleOnClick() { |
| // Autofill the value |
| mRemoteInlineSuggestionViewConnector.onClick(); |
| |
| // Notify the remote process (IME) that hosts the embedded UI that it's clicked |
| if (mInlineContentCallback != null) { |
| try { |
| mInlineContentCallback.onClick(); |
| } catch (RemoteException e) { |
| Slog.w(TAG, "RemoteException calling onClick"); |
| } |
| } |
| } |
| |
| private void handleOnLongClick() { |
| // Notify the remote process (IME) that hosts the embedded UI that it's long clicked |
| if (mInlineContentCallback != null) { |
| try { |
| mInlineContentCallback.onLongClick(); |
| } catch (RemoteException e) { |
| Slog.w(TAG, "RemoteException calling onLongClick"); |
| } |
| } |
| } |
| |
| private void handleOnError() { |
| mRemoteInlineSuggestionViewConnector.onError(); |
| } |
| |
| private void handleOnTransferTouchFocusToImeWindow(IBinder sourceInputToken, int displayId) { |
| mRemoteInlineSuggestionViewConnector.onTransferTouchFocusToImeWindow(sourceInputToken, |
| displayId); |
| } |
| |
| private void handleOnStartIntentSender(IntentSender intentSender) { |
| mRemoteInlineSuggestionViewConnector.onStartIntentSender(intentSender); |
| } |
| |
| /** |
| * Responsible for communicating with the inline suggestion view owning process. |
| */ |
| private class InlineSuggestionUiCallbackImpl extends IInlineSuggestionUiCallback.Stub { |
| |
| @Override |
| public void onClick() { |
| mHandler.post(RemoteInlineSuggestionUi.this::handleOnClick); |
| } |
| |
| @Override |
| public void onLongClick() { |
| mHandler.post(RemoteInlineSuggestionUi.this::handleOnLongClick); |
| } |
| |
| @Override |
| public void onContent(IInlineSuggestionUi content, |
| SurfaceControlViewHost.SurfacePackage surface, int width, int height) { |
| mHandler.post(() -> handleInlineSuggestionUiReady(content, surface, width, height)); |
| } |
| |
| @Override |
| public void onError() { |
| mHandler.post(RemoteInlineSuggestionUi.this::handleOnError); |
| } |
| |
| @Override |
| public void onTransferTouchFocusToImeWindow(IBinder sourceInputToken, int displayId) { |
| mHandler.post(() -> handleOnTransferTouchFocusToImeWindow(sourceInputToken, displayId)); |
| } |
| |
| @Override |
| public void onStartIntentSender(IntentSender intentSender) { |
| mHandler.post(() -> handleOnStartIntentSender(intentSender)); |
| } |
| } |
| } |