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 static android.view.ImeFocusControllerProto.HAS_IME_FOCUS; 20 import static android.view.ImeFocusControllerProto.NEXT_SERVED_VIEW; 21 import static android.view.ImeFocusControllerProto.SERVED_VIEW; 22 23 import android.annotation.AnyThread; 24 import android.annotation.NonNull; 25 import android.annotation.UiThread; 26 import android.util.Log; 27 import android.util.proto.ProtoOutputStream; 28 import android.view.inputmethod.InputMethodManager; 29 30 import com.android.internal.inputmethod.InputMethodDebug; 31 import com.android.internal.inputmethod.StartInputFlags; 32 import com.android.internal.inputmethod.StartInputReason; 33 34 import java.util.Objects; 35 36 /** 37 * Responsible for IME focus handling inside {@link ViewRootImpl}. 38 * @hide 39 */ 40 public final class ImeFocusController { 41 private static final boolean DEBUG = false; 42 private static final String TAG = "ImeFocusController"; 43 44 private final ViewRootImpl mViewRootImpl; 45 private boolean mHasImeFocus = false; 46 private View mServedView; 47 private View mNextServedView; 48 private InputMethodManagerDelegate mDelegate; 49 50 @UiThread ImeFocusController(@onNull ViewRootImpl viewRootImpl)51 ImeFocusController(@NonNull ViewRootImpl viewRootImpl) { 52 mViewRootImpl = viewRootImpl; 53 } 54 55 @NonNull getImmDelegate()56 private InputMethodManagerDelegate getImmDelegate() { 57 InputMethodManagerDelegate delegate = mDelegate; 58 if (delegate != null) { 59 return delegate; 60 } 61 delegate = mViewRootImpl.mContext.getSystemService(InputMethodManager.class).getDelegate(); 62 mDelegate = delegate; 63 return delegate; 64 } 65 66 /** Called when the view root is moved to a different display. */ 67 @UiThread onMovedToDisplay()68 void onMovedToDisplay() { 69 // InputMethodManager managed its instances for different displays. So if the associated 70 // display is changed, the delegate also needs to be refreshed (by getImmDelegate). 71 // See the comment in {@link android.app.SystemServiceRegistry} for InputMethodManager 72 // and {@link android.view.inputmethod.InputMethodManager#forContext}. 73 mDelegate = null; 74 } 75 76 @UiThread onTraversal(boolean hasWindowFocus, WindowManager.LayoutParams windowAttribute)77 void onTraversal(boolean hasWindowFocus, WindowManager.LayoutParams windowAttribute) { 78 final boolean hasImeFocus = updateImeFocusable(windowAttribute, false /* force */); 79 if (!hasWindowFocus || isInLocalFocusMode(windowAttribute)) { 80 return; 81 } 82 if (hasImeFocus == mHasImeFocus) { 83 return; 84 } 85 mHasImeFocus = hasImeFocus; 86 if (mHasImeFocus) { 87 onPreWindowFocus(true /* hasWindowFocus */, windowAttribute); 88 onPostWindowFocus(mViewRootImpl.mView.findFocus(), true /* hasWindowFocus */, 89 windowAttribute); 90 } 91 } 92 93 @UiThread onPreWindowFocus(boolean hasWindowFocus, WindowManager.LayoutParams windowAttribute)94 void onPreWindowFocus(boolean hasWindowFocus, WindowManager.LayoutParams windowAttribute) { 95 if (!mHasImeFocus || isInLocalFocusMode(windowAttribute)) { 96 return; 97 } 98 if (hasWindowFocus) { 99 getImmDelegate().setCurrentRootView(mViewRootImpl); 100 } 101 } 102 103 @UiThread updateImeFocusable(WindowManager.LayoutParams windowAttribute, boolean force)104 boolean updateImeFocusable(WindowManager.LayoutParams windowAttribute, boolean force) { 105 final boolean hasImeFocus = WindowManager.LayoutParams.mayUseInputMethod( 106 windowAttribute.flags); 107 if (force) { 108 mHasImeFocus = hasImeFocus; 109 } 110 return hasImeFocus; 111 } 112 113 @UiThread onPostWindowFocus(View focusedView, boolean hasWindowFocus, WindowManager.LayoutParams windowAttribute)114 void onPostWindowFocus(View focusedView, boolean hasWindowFocus, 115 WindowManager.LayoutParams windowAttribute) { 116 if (!hasWindowFocus || !mHasImeFocus || isInLocalFocusMode(windowAttribute)) { 117 return; 118 } 119 View viewForWindowFocus = focusedView != null ? focusedView : mViewRootImpl.mView; 120 if (DEBUG) { 121 Log.v(TAG, "onWindowFocus: " + viewForWindowFocus 122 + " softInputMode=" + InputMethodDebug.softInputModeToString( 123 windowAttribute.softInputMode)); 124 } 125 126 boolean forceFocus = false; 127 final InputMethodManagerDelegate immDelegate = getImmDelegate(); 128 if (immDelegate.isRestartOnNextWindowFocus(true /* reset */)) { 129 if (DEBUG) Log.v(TAG, "Restarting due to isRestartOnNextWindowFocus as true"); 130 forceFocus = true; 131 } 132 133 // Update mNextServedView when focusedView changed. 134 onViewFocusChanged(viewForWindowFocus, true); 135 136 // Starting new input when the next focused view is same as served view but the currently 137 // active connection (if any) is not associated with it. 138 final boolean nextFocusIsServedView = mServedView == viewForWindowFocus; 139 if (nextFocusIsServedView && !immDelegate.hasActiveConnection(viewForWindowFocus)) { 140 forceFocus = true; 141 } 142 143 immDelegate.startInputAsyncOnWindowFocusGain(viewForWindowFocus, 144 windowAttribute.softInputMode, windowAttribute.flags, forceFocus); 145 } 146 checkFocus(boolean forceNewFocus, boolean startInput)147 public boolean checkFocus(boolean forceNewFocus, boolean startInput) { 148 final InputMethodManagerDelegate immDelegate = getImmDelegate(); 149 if (!immDelegate.isCurrentRootView(mViewRootImpl) 150 || (mServedView == mNextServedView && !forceNewFocus)) { 151 return false; 152 } 153 if (DEBUG) Log.v(TAG, "checkFocus: view=" + mServedView 154 + " next=" + mNextServedView 155 + " force=" + forceNewFocus 156 + " package=" 157 + (mServedView != null ? mServedView.getContext().getPackageName() : "<none>")); 158 159 // Close the connection when no next served view coming. 160 if (mNextServedView == null) { 161 immDelegate.finishInput(); 162 immDelegate.closeCurrentIme(); 163 return false; 164 } 165 mServedView = mNextServedView; 166 immDelegate.finishComposingText(); 167 168 if (startInput) { 169 immDelegate.startInput(StartInputReason.CHECK_FOCUS, null /* focusedView */, 170 0 /* startInputFlags */, 0 /* softInputMode */, 0 /* windowFlags */); 171 } 172 return true; 173 } 174 175 @UiThread onViewFocusChanged(View view, boolean hasFocus)176 void onViewFocusChanged(View view, boolean hasFocus) { 177 if (view == null || view.isTemporarilyDetached()) { 178 return; 179 } 180 if (!getImmDelegate().isCurrentRootView(view.getViewRootImpl())) { 181 return; 182 } 183 if (!view.hasImeFocus() || !view.hasWindowFocus()) { 184 return; 185 } 186 if (DEBUG) Log.d(TAG, "onViewFocusChanged, view=" + view + ", mServedView=" + mServedView); 187 188 // We don't need to track the next served view when the view lost focus here because: 189 // 1) The current view focus may be cleared temporary when in touch mode, closing input 190 // at this moment isn't the right way. 191 // 2) We only care about the served view change when it focused, since changing input 192 // connection when the focus target changed is reasonable. 193 // 3) Setting the next served view as null when no more served view should be handled in 194 // other special events (e.g. view detached from window or the window dismissed). 195 if (hasFocus) { 196 mNextServedView = view; 197 } 198 mViewRootImpl.dispatchCheckFocus(); 199 } 200 201 @UiThread onViewDetachedFromWindow(View view)202 void onViewDetachedFromWindow(View view) { 203 if (!getImmDelegate().isCurrentRootView(view.getViewRootImpl())) { 204 return; 205 } 206 if (mServedView == view) { 207 mNextServedView = null; 208 mViewRootImpl.dispatchCheckFocus(); 209 } 210 } 211 212 @UiThread onWindowDismissed()213 void onWindowDismissed() { 214 final InputMethodManagerDelegate immDelegate = getImmDelegate(); 215 if (!immDelegate.isCurrentRootView(mViewRootImpl)) { 216 return; 217 } 218 if (mServedView != null) { 219 immDelegate.finishInput(); 220 } 221 immDelegate.setCurrentRootView(null); 222 mHasImeFocus = false; 223 } 224 225 /** 226 * To handle the lifecycle of the input connection when the device interactivity state changed. 227 * (i.e. Calling IMS#onFinishInput when the device screen-off and Calling IMS#onStartInput 228 * when the device screen-on again). 229 */ 230 @UiThread onInteractiveChanged(boolean interactive)231 public void onInteractiveChanged(boolean interactive) { 232 final InputMethodManagerDelegate immDelegate = getImmDelegate(); 233 if (!immDelegate.isCurrentRootView(mViewRootImpl)) { 234 return; 235 } 236 if (interactive) { 237 final View focusedView = mViewRootImpl.mView.findFocus(); 238 onViewFocusChanged(focusedView, focusedView != null); 239 } else { 240 mDelegate.finishInputAndReportToIme(); 241 } 242 } 243 244 /** 245 * @param windowAttribute {@link WindowManager.LayoutParams} to be checked. 246 * @return Whether the window is in local focus mode or not. 247 */ 248 @AnyThread isInLocalFocusMode(WindowManager.LayoutParams windowAttribute)249 private static boolean isInLocalFocusMode(WindowManager.LayoutParams windowAttribute) { 250 return (windowAttribute.flags & WindowManager.LayoutParams.FLAG_LOCAL_FOCUS_MODE) != 0; 251 } 252 onProcessImeInputStage(Object token, InputEvent event, WindowManager.LayoutParams windowAttribute, InputMethodManager.FinishedInputEventCallback callback)253 int onProcessImeInputStage(Object token, InputEvent event, 254 WindowManager.LayoutParams windowAttribute, 255 InputMethodManager.FinishedInputEventCallback callback) { 256 if (!mHasImeFocus || isInLocalFocusMode(windowAttribute)) { 257 return InputMethodManager.DISPATCH_NOT_HANDLED; 258 } 259 final InputMethodManager imm = 260 mViewRootImpl.mContext.getSystemService(InputMethodManager.class); 261 if (imm == null) { 262 return InputMethodManager.DISPATCH_NOT_HANDLED; 263 } 264 return imm.dispatchInputEvent(event, token, callback, mViewRootImpl.mHandler); 265 } 266 267 /** 268 * A delegate implementing some basic {@link InputMethodManager} APIs. 269 * @hide 270 */ 271 public interface InputMethodManagerDelegate { startInput(@tartInputReason int startInputReason, View focusedView, @StartInputFlags int startInputFlags, @WindowManager.LayoutParams.SoftInputModeFlags int softInputMode, int windowFlags)272 boolean startInput(@StartInputReason int startInputReason, View focusedView, 273 @StartInputFlags int startInputFlags, 274 @WindowManager.LayoutParams.SoftInputModeFlags int softInputMode, int windowFlags); startInputAsyncOnWindowFocusGain(View rootView, @WindowManager.LayoutParams.SoftInputModeFlags int softInputMode, int windowFlags, boolean forceNewFocus)275 void startInputAsyncOnWindowFocusGain(View rootView, 276 @WindowManager.LayoutParams.SoftInputModeFlags int softInputMode, int windowFlags, 277 boolean forceNewFocus); finishInput()278 void finishInput(); finishInputAndReportToIme()279 void finishInputAndReportToIme(); closeCurrentIme()280 void closeCurrentIme(); finishComposingText()281 void finishComposingText(); setCurrentRootView(ViewRootImpl rootView)282 void setCurrentRootView(ViewRootImpl rootView); isCurrentRootView(ViewRootImpl rootView)283 boolean isCurrentRootView(ViewRootImpl rootView); isRestartOnNextWindowFocus(boolean reset)284 boolean isRestartOnNextWindowFocus(boolean reset); hasActiveConnection(View view)285 boolean hasActiveConnection(View view); 286 } 287 getServedView()288 public View getServedView() { 289 return mServedView; 290 } 291 getNextServedView()292 public View getNextServedView() { 293 return mNextServedView; 294 } 295 setServedView(View view)296 public void setServedView(View view) { 297 mServedView = view; 298 } 299 setNextServedView(View view)300 public void setNextServedView(View view) { 301 mNextServedView = view; 302 } 303 304 /** 305 * Indicates whether the view's window has IME focused. 306 */ 307 @UiThread hasImeFocus()308 boolean hasImeFocus() { 309 return mHasImeFocus; 310 } 311 dumpDebug(ProtoOutputStream proto, long fieldId)312 void dumpDebug(ProtoOutputStream proto, long fieldId) { 313 final long token = proto.start(fieldId); 314 proto.write(HAS_IME_FOCUS, mHasImeFocus); 315 proto.write(SERVED_VIEW, Objects.toString(mServedView)); 316 proto.write(NEXT_SERVED_VIEW, Objects.toString(mNextServedView)); 317 proto.end(token); 318 } 319 } 320