1 /* 2 * Copyright (C) 2019 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package android.view; 18 19 import android.annotation.AnyThread; 20 import android.annotation.NonNull; 21 import android.annotation.UiThread; 22 import android.util.Log; 23 import android.view.inputmethod.InputMethodManager; 24 25 import com.android.internal.inputmethod.InputMethodDebug; 26 import com.android.internal.inputmethod.StartInputFlags; 27 import com.android.internal.inputmethod.StartInputReason; 28 29 /** 30 * Responsible for IME focus handling inside {@link ViewRootImpl}. 31 * @hide 32 */ 33 public final class ImeFocusController { 34 private static final boolean DEBUG = false; 35 private static final String TAG = "ImeFocusController"; 36 37 private final ViewRootImpl mViewRootImpl; 38 private boolean mHasImeFocus = false; 39 private View mServedView; 40 private View mNextServedView; 41 private InputMethodManagerDelegate mDelegate; 42 43 @UiThread ImeFocusController(@onNull ViewRootImpl viewRootImpl)44 ImeFocusController(@NonNull ViewRootImpl viewRootImpl) { 45 mViewRootImpl = viewRootImpl; 46 } 47 48 @NonNull getImmDelegate()49 private InputMethodManagerDelegate getImmDelegate() { 50 InputMethodManagerDelegate delegate = mDelegate; 51 if (delegate != null) { 52 return delegate; 53 } 54 delegate = mViewRootImpl.mContext.getSystemService(InputMethodManager.class).getDelegate(); 55 mDelegate = delegate; 56 return delegate; 57 } 58 59 /** Called when the view root is moved to a different display. */ 60 @UiThread onMovedToDisplay()61 void onMovedToDisplay() { 62 // InputMethodManager managed its instances for different displays. So if the associated 63 // display is changed, the delegate also needs to be refreshed (by getImmDelegate). 64 // See the comment in {@link android.app.SystemServiceRegistry} for InputMethodManager 65 // and {@link android.view.inputmethod.InputMethodManager#forContext}. 66 mDelegate = null; 67 } 68 69 @UiThread onTraversal(boolean hasWindowFocus, WindowManager.LayoutParams windowAttribute)70 void onTraversal(boolean hasWindowFocus, WindowManager.LayoutParams windowAttribute) { 71 final boolean hasImeFocus = updateImeFocusable(windowAttribute, false /* force */); 72 if (!hasWindowFocus || isInLocalFocusMode(windowAttribute)) { 73 return; 74 } 75 if (hasImeFocus == mHasImeFocus) { 76 return; 77 } 78 mHasImeFocus = hasImeFocus; 79 if (mHasImeFocus) { 80 onPreWindowFocus(true /* hasWindowFocus */, windowAttribute); 81 onPostWindowFocus(mViewRootImpl.mView.findFocus(), true /* hasWindowFocus */, 82 windowAttribute); 83 } 84 } 85 86 @UiThread onPreWindowFocus(boolean hasWindowFocus, WindowManager.LayoutParams windowAttribute)87 void onPreWindowFocus(boolean hasWindowFocus, WindowManager.LayoutParams windowAttribute) { 88 if (!mHasImeFocus || isInLocalFocusMode(windowAttribute)) { 89 return; 90 } 91 if (hasWindowFocus) { 92 getImmDelegate().setCurrentRootView(mViewRootImpl); 93 } 94 } 95 96 @UiThread updateImeFocusable(WindowManager.LayoutParams windowAttribute, boolean force)97 boolean updateImeFocusable(WindowManager.LayoutParams windowAttribute, boolean force) { 98 final boolean hasImeFocus = WindowManager.LayoutParams.mayUseInputMethod( 99 windowAttribute.flags); 100 if (force) { 101 mHasImeFocus = hasImeFocus; 102 } 103 return hasImeFocus; 104 } 105 106 @UiThread onPostWindowFocus(View focusedView, boolean hasWindowFocus, WindowManager.LayoutParams windowAttribute)107 void onPostWindowFocus(View focusedView, boolean hasWindowFocus, 108 WindowManager.LayoutParams windowAttribute) { 109 if (!hasWindowFocus || !mHasImeFocus || isInLocalFocusMode(windowAttribute)) { 110 return; 111 } 112 if (DEBUG) { 113 Log.v(TAG, "onWindowFocus: " + focusedView 114 + " softInputMode=" + InputMethodDebug.softInputModeToString( 115 windowAttribute.softInputMode)); 116 } 117 118 boolean forceFocus = false; 119 final InputMethodManagerDelegate immDelegate = getImmDelegate(); 120 if (immDelegate.isRestartOnNextWindowFocus(true /* reset */)) { 121 if (DEBUG) Log.v(TAG, "Restarting due to isRestartOnNextWindowFocus as true"); 122 forceFocus = true; 123 } 124 // Update mNextServedView when focusedView changed. 125 final View viewForWindowFocus = focusedView != null ? focusedView : mViewRootImpl.mView; 126 onViewFocusChanged(viewForWindowFocus, true); 127 128 // Starting new input when the next focused view is same as served view but the currently 129 // active connection (if any) is not associated with it. 130 final boolean nextFocusIsServedView = mServedView == viewForWindowFocus; 131 if (nextFocusIsServedView && !immDelegate.hasActiveConnection(viewForWindowFocus)) { 132 forceFocus = true; 133 } 134 135 immDelegate.startInputAsyncOnWindowFocusGain(viewForWindowFocus, 136 windowAttribute.softInputMode, windowAttribute.flags, forceFocus); 137 } 138 checkFocus(boolean forceNewFocus, boolean startInput)139 public boolean checkFocus(boolean forceNewFocus, boolean startInput) { 140 final InputMethodManagerDelegate immDelegate = getImmDelegate(); 141 if (!immDelegate.isCurrentRootView(mViewRootImpl) 142 || (mServedView == mNextServedView && !forceNewFocus)) { 143 return false; 144 } 145 if (DEBUG) Log.v(TAG, "checkFocus: view=" + mServedView 146 + " next=" + mNextServedView 147 + " force=" + forceNewFocus 148 + " package=" 149 + (mServedView != null ? mServedView.getContext().getPackageName() : "<none>")); 150 151 // Close the connection when no next served view coming. 152 if (mNextServedView == null) { 153 immDelegate.finishInput(); 154 immDelegate.closeCurrentIme(); 155 return false; 156 } 157 mServedView = mNextServedView; 158 immDelegate.finishComposingText(); 159 160 if (startInput) { 161 immDelegate.startInput(StartInputReason.CHECK_FOCUS, null /* focusedView */, 162 0 /* startInputFlags */, 0 /* softInputMode */, 0 /* windowFlags */); 163 } 164 return true; 165 } 166 167 @UiThread onViewFocusChanged(View view, boolean hasFocus)168 void onViewFocusChanged(View view, boolean hasFocus) { 169 if (view == null || view.isTemporarilyDetached()) { 170 return; 171 } 172 if (!getImmDelegate().isCurrentRootView(view.getViewRootImpl())) { 173 return; 174 } 175 if (!view.hasImeFocus() || !view.hasWindowFocus()) { 176 return; 177 } 178 if (DEBUG) Log.d(TAG, "onViewFocusChanged, view=" + view + ", mServedView=" + mServedView); 179 180 // We don't need to track the next served view when the view lost focus here because: 181 // 1) The current view focus may be cleared temporary when in touch mode, closing input 182 // at this moment isn't the right way. 183 // 2) We only care about the served view change when it focused, since changing input 184 // connection when the focus target changed is reasonable. 185 // 3) Setting the next served view as null when no more served view should be handled in 186 // other special events (e.g. view detached from window or the window dismissed). 187 if (hasFocus) { 188 mNextServedView = view; 189 } 190 mViewRootImpl.dispatchCheckFocus(); 191 } 192 193 @UiThread onViewDetachedFromWindow(View view)194 void onViewDetachedFromWindow(View view) { 195 if (!getImmDelegate().isCurrentRootView(view.getViewRootImpl())) { 196 return; 197 } 198 if (mServedView == view) { 199 mNextServedView = null; 200 mViewRootImpl.dispatchCheckFocus(); 201 } 202 } 203 204 @UiThread onWindowDismissed()205 void onWindowDismissed() { 206 final InputMethodManagerDelegate immDelegate = getImmDelegate(); 207 if (!immDelegate.isCurrentRootView(mViewRootImpl)) { 208 return; 209 } 210 if (mServedView != null) { 211 immDelegate.finishInput(); 212 } 213 immDelegate.setCurrentRootView(null); 214 mHasImeFocus = false; 215 } 216 217 /** 218 * @param windowAttribute {@link WindowManager.LayoutParams} to be checked. 219 * @return Whether the window is in local focus mode or not. 220 */ 221 @AnyThread isInLocalFocusMode(WindowManager.LayoutParams windowAttribute)222 private static boolean isInLocalFocusMode(WindowManager.LayoutParams windowAttribute) { 223 return (windowAttribute.flags & WindowManager.LayoutParams.FLAG_LOCAL_FOCUS_MODE) != 0; 224 } 225 onProcessImeInputStage(Object token, InputEvent event, WindowManager.LayoutParams windowAttribute, InputMethodManager.FinishedInputEventCallback callback)226 int onProcessImeInputStage(Object token, InputEvent event, 227 WindowManager.LayoutParams windowAttribute, 228 InputMethodManager.FinishedInputEventCallback callback) { 229 if (!mHasImeFocus || isInLocalFocusMode(windowAttribute)) { 230 return InputMethodManager.DISPATCH_NOT_HANDLED; 231 } 232 final InputMethodManager imm = 233 mViewRootImpl.mContext.getSystemService(InputMethodManager.class); 234 if (imm == null) { 235 return InputMethodManager.DISPATCH_NOT_HANDLED; 236 } 237 return imm.dispatchInputEvent(event, token, callback, mViewRootImpl.mHandler); 238 } 239 240 /** 241 * A delegate implementing some basic {@link InputMethodManager} APIs. 242 * @hide 243 */ 244 public interface InputMethodManagerDelegate { startInput(@tartInputReason int startInputReason, View focusedView, @StartInputFlags int startInputFlags, @WindowManager.LayoutParams.SoftInputModeFlags int softInputMode, int windowFlags)245 boolean startInput(@StartInputReason int startInputReason, View focusedView, 246 @StartInputFlags int startInputFlags, 247 @WindowManager.LayoutParams.SoftInputModeFlags int softInputMode, int windowFlags); startInputAsyncOnWindowFocusGain(View rootView, @WindowManager.LayoutParams.SoftInputModeFlags int softInputMode, int windowFlags, boolean forceNewFocus)248 void startInputAsyncOnWindowFocusGain(View rootView, 249 @WindowManager.LayoutParams.SoftInputModeFlags int softInputMode, int windowFlags, 250 boolean forceNewFocus); finishInput()251 void finishInput(); closeCurrentIme()252 void closeCurrentIme(); finishComposingText()253 void finishComposingText(); setCurrentRootView(ViewRootImpl rootView)254 void setCurrentRootView(ViewRootImpl rootView); isCurrentRootView(ViewRootImpl rootView)255 boolean isCurrentRootView(ViewRootImpl rootView); isRestartOnNextWindowFocus(boolean reset)256 boolean isRestartOnNextWindowFocus(boolean reset); hasActiveConnection(View view)257 boolean hasActiveConnection(View view); 258 } 259 getServedView()260 public View getServedView() { 261 return mServedView; 262 } 263 getNextServedView()264 public View getNextServedView() { 265 return mNextServedView; 266 } 267 setServedView(View view)268 public void setServedView(View view) { 269 mServedView = view; 270 } 271 setNextServedView(View view)272 public void setNextServedView(View view) { 273 mNextServedView = view; 274 } 275 276 /** 277 * Indicates whether the view's window has IME focused. 278 */ 279 @UiThread hasImeFocus()280 boolean hasImeFocus() { 281 return mHasImeFocus; 282 } 283 } 284