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             // Dummy 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 dummy
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
onToggleSoftInput(int showFlags, int hideFlags)201     public void onToggleSoftInput(int showFlags, int hideFlags) {
202         // TODO: Implement
203         Log.w(TAG, "onToggleSoftInput is not yet implemented. clientId=" + mClientId
204                 + " showFlags=" + showFlags + " hideFlags=" + hideFlags);
205     }
206 
207     @Override
onUpdateCursorAnchorInfo(CursorAnchorInfo info)208     public void onUpdateCursorAnchorInfo(CursorAnchorInfo info) {
209     }
210 
211     @Override
onUpdateSelection(int oldSelStart, int oldSelEnd, int newSelStart, int newSelEnd, int candidatesStart, int candidatesEnd)212     public void onUpdateSelection(int oldSelStart, int oldSelEnd, int newSelStart, int newSelEnd,
213             int candidatesStart, int candidatesEnd) {
214     }
215 
216     @Override
onGenericMotionEvent(MotionEvent event)217     public boolean onGenericMotionEvent(MotionEvent event) {
218         return false;
219     }
220 
221     @Override
onKeyDown(int keyCode, KeyEvent event)222     public boolean onKeyDown(int keyCode, KeyEvent event) {
223         if (DEBUG) {
224             Log.v(TAG, "onKeyDown clientId=" + mClientId + " keyCode=" + keyCode
225                     + " event=" + event);
226         }
227         if (keyCode == KeyEvent.KEYCODE_BACK) {
228             final SoftInputWindow window =
229                     mSoftInputWindowManager.getSoftInputWindow(mSelfReportedDisplayId);
230             if (window != null && window.isShowing()) {
231                 event.startTracking();
232                 return true;
233             }
234         }
235         return false;
236     }
237 
238     @Override
onKeyLongPress(int keyCode, KeyEvent event)239     public boolean onKeyLongPress(int keyCode, KeyEvent event) {
240         return false;
241     }
242 
243     @Override
onKeyMultiple(int keyCode, KeyEvent event)244     public boolean onKeyMultiple(int keyCode, KeyEvent event) {
245         return false;
246     }
247 
248     @Override
onKeyUp(int keyCode, KeyEvent event)249     public boolean onKeyUp(int keyCode, KeyEvent event) {
250         if (DEBUG) {
251             Log.v(TAG, "onKeyUp clientId=" + mClientId + "keyCode=" + keyCode
252                     + " event=" + event);
253         }
254         if (keyCode == KeyEvent.KEYCODE_BACK && event.isTracking() && !event.isCanceled()) {
255             final SoftInputWindow window =
256                     mSoftInputWindowManager.getSoftInputWindow(mSelfReportedDisplayId);
257             if (window != null && window.isShowing()) {
258                 window.hide();
259                 return true;
260             }
261         }
262         return false;
263     }
264 
265     @Override
onTrackballEvent(MotionEvent event)266     public boolean onTrackballEvent(MotionEvent event) {
267         return false;
268     }
269 }
270