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 if (mDelegate == null) { 58 mDelegate = mViewRootImpl.mContext.getSystemService( 59 InputMethodManager.class).getDelegate(); 60 } 61 return mDelegate; 62 } 63 64 /** Called when the view root is moved to a different display. */ 65 @UiThread onMovedToDisplay()66 void onMovedToDisplay() { 67 // InputMethodManager managed its instances for different displays. So if the associated 68 // display is changed, the delegate also needs to be refreshed (by getImmDelegate). 69 // See the comment in {@link android.app.SystemServiceRegistry} for InputMethodManager 70 // and {@link android.view.inputmethod.InputMethodManager#forContext}. 71 mDelegate = null; 72 } 73 74 @UiThread onTraversal(boolean hasWindowFocus, WindowManager.LayoutParams windowAttribute)75 void onTraversal(boolean hasWindowFocus, WindowManager.LayoutParams windowAttribute) { 76 final boolean hasImeFocus = updateImeFocusable(windowAttribute, false /* force */); 77 if (!hasWindowFocus || isInLocalFocusMode(windowAttribute)) { 78 return; 79 } 80 if (hasImeFocus == mHasImeFocus) { 81 return; 82 } 83 mHasImeFocus = hasImeFocus; 84 if (mHasImeFocus) { 85 onPreWindowFocus(true /* hasWindowFocus */, windowAttribute); 86 onPostWindowFocus(mViewRootImpl.mView.findFocus(), true /* hasWindowFocus */, 87 windowAttribute); 88 } 89 } 90 91 @UiThread onPreWindowFocus(boolean hasWindowFocus, WindowManager.LayoutParams windowAttribute)92 void onPreWindowFocus(boolean hasWindowFocus, WindowManager.LayoutParams windowAttribute) { 93 if (!mHasImeFocus || isInLocalFocusMode(windowAttribute)) { 94 return; 95 } 96 if (hasWindowFocus) { 97 getImmDelegate().setCurrentRootView(mViewRootImpl); 98 } 99 } 100 101 @UiThread updateImeFocusable(WindowManager.LayoutParams windowAttribute, boolean force)102 boolean updateImeFocusable(WindowManager.LayoutParams windowAttribute, boolean force) { 103 final boolean hasImeFocus = WindowManager.LayoutParams.mayUseInputMethod( 104 windowAttribute.flags); 105 if (force) { 106 mHasImeFocus = hasImeFocus; 107 } 108 return hasImeFocus; 109 } 110 111 @UiThread onPostWindowFocus(View focusedView, boolean hasWindowFocus, WindowManager.LayoutParams windowAttribute)112 void onPostWindowFocus(View focusedView, boolean hasWindowFocus, 113 WindowManager.LayoutParams windowAttribute) { 114 if (!hasWindowFocus || !mHasImeFocus || isInLocalFocusMode(windowAttribute)) { 115 return; 116 } 117 View viewForWindowFocus = focusedView != null ? focusedView : mViewRootImpl.mView; 118 if (DEBUG) { 119 Log.v(TAG, "onWindowFocus: " + viewForWindowFocus 120 + " softInputMode=" + InputMethodDebug.softInputModeToString( 121 windowAttribute.softInputMode)); 122 } 123 124 boolean forceFocus = false; 125 final InputMethodManagerDelegate immDelegate = getImmDelegate(); 126 if (immDelegate.isRestartOnNextWindowFocus(true /* reset */)) { 127 if (DEBUG) Log.v(TAG, "Restarting due to isRestartOnNextWindowFocus as true"); 128 forceFocus = true; 129 } 130 131 // Update mNextServedView when focusedView changed. 132 onViewFocusChanged(viewForWindowFocus, true); 133 134 // Starting new input when the next focused view is same as served view but the currently 135 // active connection (if any) is not associated with it. 136 final boolean nextFocusIsServedView = mServedView == viewForWindowFocus; 137 if (nextFocusIsServedView && !immDelegate.hasActiveConnection(viewForWindowFocus)) { 138 forceFocus = true; 139 } 140 141 immDelegate.startInputAsyncOnWindowFocusGain(viewForWindowFocus, 142 windowAttribute.softInputMode, windowAttribute.flags, forceFocus); 143 } 144 checkFocus(boolean forceNewFocus, boolean startInput)145 public boolean checkFocus(boolean forceNewFocus, boolean startInput) { 146 final InputMethodManagerDelegate immDelegate = getImmDelegate(); 147 if (!immDelegate.isCurrentRootView(mViewRootImpl) 148 || (mServedView == mNextServedView && !forceNewFocus)) { 149 return false; 150 } 151 if (DEBUG) Log.v(TAG, "checkFocus: view=" + mServedView 152 + " next=" + mNextServedView 153 + " force=" + forceNewFocus 154 + " package=" 155 + (mServedView != null ? mServedView.getContext().getPackageName() : "<none>")); 156 157 // Close the connection when no next served view coming. 158 if (mNextServedView == null) { 159 immDelegate.finishInput(); 160 immDelegate.closeCurrentIme(); 161 return false; 162 } 163 mServedView = mNextServedView; 164 immDelegate.finishComposingText(); 165 166 if (startInput) { 167 immDelegate.startInput(StartInputReason.CHECK_FOCUS, null /* focusedView */, 168 0 /* startInputFlags */, 0 /* softInputMode */, 0 /* windowFlags */); 169 } 170 return true; 171 } 172 173 @UiThread onViewFocusChanged(View view, boolean hasFocus)174 void onViewFocusChanged(View view, boolean hasFocus) { 175 if (view == null || view.isTemporarilyDetached()) { 176 return; 177 } 178 if (!getImmDelegate().isCurrentRootView(view.getViewRootImpl())) { 179 return; 180 } 181 if (!view.hasImeFocus() || !view.hasWindowFocus()) { 182 return; 183 } 184 if (DEBUG) Log.d(TAG, "onViewFocusChanged, view=" + view + ", mServedView=" + mServedView); 185 186 // We don't need to track the next served view when the view lost focus here because: 187 // 1) The current view focus may be cleared temporary when in touch mode, closing input 188 // at this moment isn't the right way. 189 // 2) We only care about the served view change when it focused, since changing input 190 // connection when the focus target changed is reasonable. 191 // 3) Setting the next served view as null when no more served view should be handled in 192 // other special events (e.g. view detached from window or the window dismissed). 193 if (hasFocus) { 194 mNextServedView = view; 195 } 196 mViewRootImpl.dispatchCheckFocus(); 197 } 198 199 @UiThread onViewDetachedFromWindow(View view)200 void onViewDetachedFromWindow(View view) { 201 if (!getImmDelegate().isCurrentRootView(view.getViewRootImpl())) { 202 return; 203 } 204 if (mNextServedView == view) { 205 mNextServedView = null; 206 } 207 if (mServedView == view) { 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