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