1 /*
2  * Copyright (C) 2018 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 com.example.android.multiclientinputmethod;
18 
19 import android.inputmethodservice.MultiClientInputMethodServiceDelegate;
20 import android.os.Bundle;
21 import android.os.Looper;
22 import android.os.ResultReceiver;
23 import android.util.Log;
24 import android.view.KeyEvent;
25 import android.view.MotionEvent;
26 import android.view.WindowManager;
27 import android.view.inputmethod.CompletionInfo;
28 import android.view.inputmethod.CursorAnchorInfo;
29 import android.view.inputmethod.EditorInfo;
30 import android.view.inputmethod.InputConnection;
31 
32 final class ClientCallbackImpl implements MultiClientInputMethodServiceDelegate.ClientCallback {
33     private static final String TAG = "ClientCallbackImpl";
34     private static final boolean DEBUG = false;
35 
36     private final MultiClientInputMethodServiceDelegate mDelegate;
37     private final SoftInputWindowManager mSoftInputWindowManager;
38     private final int mClientId;
39     private final int mUid;
40     private final int mPid;
41     private final int mSelfReportedDisplayId;
42     private final KeyEvent.DispatcherState mDispatcherState;
43     private final Looper mLooper;
44     private final MultiClientInputMethod mInputMethod;
45 
ClientCallbackImpl(MultiClientInputMethod inputMethod, MultiClientInputMethodServiceDelegate delegate, SoftInputWindowManager softInputWindowManager, int clientId, int uid, int pid, int selfReportedDisplayId)46     ClientCallbackImpl(MultiClientInputMethod inputMethod,
47             MultiClientInputMethodServiceDelegate delegate,
48             SoftInputWindowManager softInputWindowManager, int clientId, int uid, int pid,
49             int selfReportedDisplayId) {
50         mInputMethod = inputMethod;
51         mDelegate = delegate;
52         mSoftInputWindowManager = softInputWindowManager;
53         mClientId = clientId;
54         mUid = uid;
55         mPid = pid;
56         mSelfReportedDisplayId = selfReportedDisplayId;
57         mDispatcherState = new KeyEvent.DispatcherState();
58         // For simplicity, we use the main looper for this sample.
59         // To use other looper thread, make sure that the IME Window also runs on the same looper
60         // and introduce an appropriate synchronization mechanism instead of directly accessing
61         // MultiClientInputMethod#mDisplayToLastClientId.
62         mLooper = Looper.getMainLooper();
63     }
64 
getDispatcherState()65     KeyEvent.DispatcherState getDispatcherState() {
66         return mDispatcherState;
67     }
68 
getLooper()69     Looper getLooper() {
70         return mLooper;
71     }
72 
73     @Override
onAppPrivateCommand(String action, Bundle data)74     public void onAppPrivateCommand(String action, Bundle data) {
75     }
76 
77     @Override
onDisplayCompletions(CompletionInfo[] completions)78     public void onDisplayCompletions(CompletionInfo[] completions) {
79     }
80 
81     @Override
onFinishSession()82     public void onFinishSession() {
83         if (DEBUG) {
84             Log.v(TAG, "onFinishSession clientId=" + mClientId);
85         }
86         final SoftInputWindow window =
87                 mSoftInputWindowManager.getSoftInputWindow(mSelfReportedDisplayId);
88         if (window == null) {
89             return;
90         }
91         // SoftInputWindow also needs to be cleaned up when this IME client is still associated with
92         // it.
93         if (mClientId == window.getClientId()) {
94             window.onFinishClient();
95         }
96     }
97 
98     @Override
onHideSoftInput(int flags, ResultReceiver resultReceiver)99     public void onHideSoftInput(int flags, ResultReceiver resultReceiver) {
100         if (DEBUG) {
101             Log.v(TAG, "onHideSoftInput clientId=" + mClientId + " flags=" + flags);
102         }
103         final SoftInputWindow window =
104                 mSoftInputWindowManager.getSoftInputWindow(mSelfReportedDisplayId);
105         if (window == null) {
106             return;
107         }
108         // Seems that the Launcher3 has a bug to call onHideSoftInput() too early so we cannot
109         // enforce clientId check yet.
110         // TODO: Check clientId like we do so for onShowSoftInput().
111         window.hide();
112     }
113 
114     @Override
onShowSoftInput(int flags, ResultReceiver resultReceiver)115     public void onShowSoftInput(int flags, ResultReceiver resultReceiver) {
116         if (DEBUG) {
117             Log.v(TAG, "onShowSoftInput clientId=" + mClientId + " flags=" + flags);
118         }
119         final SoftInputWindow window =
120                 mSoftInputWindowManager.getSoftInputWindow(mSelfReportedDisplayId);
121         if (window == null) {
122             return;
123         }
124         if (mClientId != window.getClientId()) {
125             Log.w(TAG, "onShowSoftInput() from a background client is ignored."
126                     + " windowClientId=" + window.getClientId()
127                     + " clientId=" + mClientId);
128             return;
129         }
130         window.show();
131     }
132 
133     @Override
onStartInputOrWindowGainedFocus(InputConnection inputConnection, EditorInfo editorInfo, int startInputFlags, int softInputMode, int targetWindowHandle)134     public void onStartInputOrWindowGainedFocus(InputConnection inputConnection,
135             EditorInfo editorInfo, int startInputFlags, int softInputMode, int targetWindowHandle) {
136         if (DEBUG) {
137             Log.v(TAG, "onStartInputOrWindowGainedFocus clientId=" + mClientId
138                     + " editorInfo=" + editorInfo
139                     + " startInputFlags="
140                     + InputMethodDebug.startInputFlagsToString(startInputFlags)
141                     + " softInputMode=" + InputMethodDebug.softInputModeToString(softInputMode)
142                     + " targetWindowHandle=" + targetWindowHandle);
143         }
144 
145         final int state = softInputMode & WindowManager.LayoutParams.SOFT_INPUT_MASK_STATE;
146         final boolean forwardNavigation =
147                 (softInputMode & WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION) != 0;
148 
149         final SoftInputWindow window =
150                 mSoftInputWindowManager.getOrCreateSoftInputWindow(mSelfReportedDisplayId);
151         if (window == null) {
152             return;
153         }
154 
155         if (window.getTargetWindowHandle() != targetWindowHandle) {
156             // Target window has changed.  Report new IME target window to the system.
157             mDelegate.reportImeWindowTarget(
158                     mClientId, targetWindowHandle, window.getWindow().getAttributes().token);
159         }
160         final int lastClientId = mInputMethod.mDisplayToLastClientId.get(mSelfReportedDisplayId);
161         if (lastClientId != mClientId) {
162             // deactivate previous client and activate current.
163             mDelegate.setActive(lastClientId, false /* active */);
164             mDelegate.setActive(mClientId, true /* active */);
165         }
166         if (inputConnection == null || editorInfo == null) {
167             // Placeholder InputConnection case.
168             if (window.getClientId() == mClientId) {
169                 // Special hack for temporary focus changes (e.g. notification shade).
170                 // If we have already established a connection to this client, and if a placeholder
171                 // InputConnection is notified, just ignore this event.
172             } else {
173                 window.onDummyStartInput(mClientId, targetWindowHandle);
174             }
175         } else {
176             window.onStartInput(mClientId, targetWindowHandle, inputConnection);
177         }
178 
179         switch (state) {
180             case WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE:
181                 if (forwardNavigation) {
182                     window.show();
183                 }
184                 break;
185             case WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE:
186                 window.show();
187                 break;
188             case WindowManager.LayoutParams.SOFT_INPUT_STATE_HIDDEN:
189                 if (forwardNavigation) {
190                     window.hide();
191                 }
192                 break;
193             case WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN:
194                 window.hide();
195                 break;
196         }
197         mInputMethod.mDisplayToLastClientId.put(mSelfReportedDisplayId, mClientId);
198     }
199 
200     @Override
onUpdateCursorAnchorInfo(CursorAnchorInfo info)201     public void onUpdateCursorAnchorInfo(CursorAnchorInfo info) {
202     }
203 
204     @Override
onUpdateSelection(int oldSelStart, int oldSelEnd, int newSelStart, int newSelEnd, int candidatesStart, int candidatesEnd)205     public void onUpdateSelection(int oldSelStart, int oldSelEnd, int newSelStart, int newSelEnd,
206             int candidatesStart, int candidatesEnd) {
207     }
208 
209     @Override
onGenericMotionEvent(MotionEvent event)210     public boolean onGenericMotionEvent(MotionEvent event) {
211         return false;
212     }
213 
214     @Override
onKeyDown(int keyCode, KeyEvent event)215     public boolean onKeyDown(int keyCode, KeyEvent event) {
216         if (DEBUG) {
217             Log.v(TAG, "onKeyDown clientId=" + mClientId + " keyCode=" + keyCode
218                     + " event=" + event);
219         }
220         if (keyCode == KeyEvent.KEYCODE_BACK) {
221             final SoftInputWindow window =
222                     mSoftInputWindowManager.getSoftInputWindow(mSelfReportedDisplayId);
223             if (window != null && window.isShowing()) {
224                 event.startTracking();
225                 return true;
226             }
227         }
228         return false;
229     }
230 
231     @Override
onKeyLongPress(int keyCode, KeyEvent event)232     public boolean onKeyLongPress(int keyCode, KeyEvent event) {
233         return false;
234     }
235 
236     @Override
onKeyMultiple(int keyCode, KeyEvent event)237     public boolean onKeyMultiple(int keyCode, KeyEvent event) {
238         return false;
239     }
240 
241     @Override
onKeyUp(int keyCode, KeyEvent event)242     public boolean onKeyUp(int keyCode, KeyEvent event) {
243         if (DEBUG) {
244             Log.v(TAG, "onKeyUp clientId=" + mClientId + "keyCode=" + keyCode
245                     + " event=" + event);
246         }
247         if (keyCode == KeyEvent.KEYCODE_BACK && event.isTracking() && !event.isCanceled()) {
248             final SoftInputWindow window =
249                     mSoftInputWindowManager.getSoftInputWindow(mSelfReportedDisplayId);
250             if (window != null && window.isShowing()) {
251                 window.hide();
252                 return true;
253             }
254         }
255         return false;
256     }
257 
258     @Override
onTrackballEvent(MotionEvent event)259     public boolean onTrackballEvent(MotionEvent event) {
260         return false;
261     }
262 }
263