• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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