• 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         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