/* * Copyright (C) 2019 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.view; import android.annotation.AnyThread; import android.annotation.NonNull; import android.annotation.UiThread; import android.util.Log; import android.view.inputmethod.InputMethodManager; import com.android.internal.inputmethod.InputMethodDebug; import com.android.internal.inputmethod.StartInputFlags; import com.android.internal.inputmethod.StartInputReason; /** * Responsible for IME focus handling inside {@link ViewRootImpl}. * @hide */ public final class ImeFocusController { private static final boolean DEBUG = false; private static final String TAG = "ImeFocusController"; private final ViewRootImpl mViewRootImpl; private boolean mHasImeFocus = false; private View mServedView; private View mNextServedView; private InputMethodManagerDelegate mDelegate; @UiThread ImeFocusController(@NonNull ViewRootImpl viewRootImpl) { mViewRootImpl = viewRootImpl; } @NonNull private InputMethodManagerDelegate getImmDelegate() { InputMethodManagerDelegate delegate = mDelegate; if (delegate != null) { return delegate; } delegate = mViewRootImpl.mContext.getSystemService(InputMethodManager.class).getDelegate(); mDelegate = delegate; return delegate; } /** Called when the view root is moved to a different display. */ @UiThread void onMovedToDisplay() { // InputMethodManager managed its instances for different displays. So if the associated // display is changed, the delegate also needs to be refreshed (by getImmDelegate). // See the comment in {@link android.app.SystemServiceRegistry} for InputMethodManager // and {@link android.view.inputmethod.InputMethodManager#forContext}. mDelegate = null; } @UiThread void onTraversal(boolean hasWindowFocus, WindowManager.LayoutParams windowAttribute) { final boolean hasImeFocus = updateImeFocusable(windowAttribute, false /* force */); if (!hasWindowFocus || isInLocalFocusMode(windowAttribute)) { return; } if (hasImeFocus == mHasImeFocus) { return; } mHasImeFocus = hasImeFocus; if (mHasImeFocus) { onPreWindowFocus(true /* hasWindowFocus */, windowAttribute); onPostWindowFocus(mViewRootImpl.mView.findFocus(), true /* hasWindowFocus */, windowAttribute); } } @UiThread void onPreWindowFocus(boolean hasWindowFocus, WindowManager.LayoutParams windowAttribute) { if (!mHasImeFocus || isInLocalFocusMode(windowAttribute)) { return; } if (hasWindowFocus) { getImmDelegate().setCurrentRootView(mViewRootImpl); } } @UiThread boolean updateImeFocusable(WindowManager.LayoutParams windowAttribute, boolean force) { final boolean hasImeFocus = WindowManager.LayoutParams.mayUseInputMethod( windowAttribute.flags); if (force) { mHasImeFocus = hasImeFocus; } return hasImeFocus; } @UiThread void onPostWindowFocus(View focusedView, boolean hasWindowFocus, WindowManager.LayoutParams windowAttribute) { if (!hasWindowFocus || !mHasImeFocus || isInLocalFocusMode(windowAttribute)) { return; } if (DEBUG) { Log.v(TAG, "onWindowFocus: " + focusedView + " softInputMode=" + InputMethodDebug.softInputModeToString( windowAttribute.softInputMode)); } boolean forceFocus = false; final InputMethodManagerDelegate immDelegate = getImmDelegate(); if (immDelegate.isRestartOnNextWindowFocus(true /* reset */)) { if (DEBUG) Log.v(TAG, "Restarting due to isRestartOnNextWindowFocus as true"); forceFocus = true; } // Update mNextServedView when focusedView changed. final View viewForWindowFocus = focusedView != null ? focusedView : mViewRootImpl.mView; onViewFocusChanged(viewForWindowFocus, true); // Starting new input when the next focused view is same as served view but the currently // active connection (if any) is not associated with it. final boolean nextFocusIsServedView = mServedView == viewForWindowFocus; if (nextFocusIsServedView && !immDelegate.hasActiveConnection(viewForWindowFocus)) { forceFocus = true; } immDelegate.startInputAsyncOnWindowFocusGain(viewForWindowFocus, windowAttribute.softInputMode, windowAttribute.flags, forceFocus); } public boolean checkFocus(boolean forceNewFocus, boolean startInput) { final InputMethodManagerDelegate immDelegate = getImmDelegate(); if (!immDelegate.isCurrentRootView(mViewRootImpl) || (mServedView == mNextServedView && !forceNewFocus)) { return false; } if (DEBUG) Log.v(TAG, "checkFocus: view=" + mServedView + " next=" + mNextServedView + " force=" + forceNewFocus + " package=" + (mServedView != null ? mServedView.getContext().getPackageName() : "")); // Close the connection when no next served view coming. if (mNextServedView == null) { immDelegate.finishInput(); immDelegate.closeCurrentIme(); return false; } mServedView = mNextServedView; immDelegate.finishComposingText(); if (startInput) { immDelegate.startInput(StartInputReason.CHECK_FOCUS, null /* focusedView */, 0 /* startInputFlags */, 0 /* softInputMode */, 0 /* windowFlags */); } return true; } @UiThread void onViewFocusChanged(View view, boolean hasFocus) { if (view == null || view.isTemporarilyDetached()) { return; } if (!getImmDelegate().isCurrentRootView(view.getViewRootImpl())) { return; } if (!view.hasImeFocus() || !view.hasWindowFocus()) { return; } if (DEBUG) Log.d(TAG, "onViewFocusChanged, view=" + view + ", mServedView=" + mServedView); // We don't need to track the next served view when the view lost focus here because: // 1) The current view focus may be cleared temporary when in touch mode, closing input // at this moment isn't the right way. // 2) We only care about the served view change when it focused, since changing input // connection when the focus target changed is reasonable. // 3) Setting the next served view as null when no more served view should be handled in // other special events (e.g. view detached from window or the window dismissed). if (hasFocus) { mNextServedView = view; } mViewRootImpl.dispatchCheckFocus(); } @UiThread void onViewDetachedFromWindow(View view) { if (!getImmDelegate().isCurrentRootView(view.getViewRootImpl())) { return; } if (mServedView == view) { mNextServedView = null; mViewRootImpl.dispatchCheckFocus(); } } @UiThread void onWindowDismissed() { final InputMethodManagerDelegate immDelegate = getImmDelegate(); if (!immDelegate.isCurrentRootView(mViewRootImpl)) { return; } if (mServedView != null) { immDelegate.finishInput(); } immDelegate.setCurrentRootView(null); mHasImeFocus = false; } /** * @param windowAttribute {@link WindowManager.LayoutParams} to be checked. * @return Whether the window is in local focus mode or not. */ @AnyThread private static boolean isInLocalFocusMode(WindowManager.LayoutParams windowAttribute) { return (windowAttribute.flags & WindowManager.LayoutParams.FLAG_LOCAL_FOCUS_MODE) != 0; } int onProcessImeInputStage(Object token, InputEvent event, WindowManager.LayoutParams windowAttribute, InputMethodManager.FinishedInputEventCallback callback) { if (!mHasImeFocus || isInLocalFocusMode(windowAttribute)) { return InputMethodManager.DISPATCH_NOT_HANDLED; } final InputMethodManager imm = mViewRootImpl.mContext.getSystemService(InputMethodManager.class); if (imm == null) { return InputMethodManager.DISPATCH_NOT_HANDLED; } return imm.dispatchInputEvent(event, token, callback, mViewRootImpl.mHandler); } /** * A delegate implementing some basic {@link InputMethodManager} APIs. * @hide */ public interface InputMethodManagerDelegate { boolean startInput(@StartInputReason int startInputReason, View focusedView, @StartInputFlags int startInputFlags, @WindowManager.LayoutParams.SoftInputModeFlags int softInputMode, int windowFlags); void startInputAsyncOnWindowFocusGain(View rootView, @WindowManager.LayoutParams.SoftInputModeFlags int softInputMode, int windowFlags, boolean forceNewFocus); void finishInput(); void closeCurrentIme(); void finishComposingText(); void setCurrentRootView(ViewRootImpl rootView); boolean isCurrentRootView(ViewRootImpl rootView); boolean isRestartOnNextWindowFocus(boolean reset); boolean hasActiveConnection(View view); } public View getServedView() { return mServedView; } public View getNextServedView() { return mNextServedView; } public void setServedView(View view) { mServedView = view; } public void setNextServedView(View view) { mNextServedView = view; } /** * Indicates whether the view's window has IME focused. */ @UiThread boolean hasImeFocus() { return mHasImeFocus; } }