blob: 2129e9bd34f3f8b41280f7f38bfc1ca06be34944 [file] [log] [blame]
Yohei Yukawabae5bea2018-11-12 15:08:30 -08001/*
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
17package com.android.server.inputmethod;
18
Yohei Yukawa1fb13c52019-02-05 07:55:28 -080019import static android.Manifest.permission.INTERACT_ACROSS_USERS_FULL;
Yohei Yukawabae5bea2018-11-12 15:08:30 -080020import static android.view.WindowManager.LayoutParams.TYPE_INPUT_METHOD;
21
22import static java.lang.annotation.RetentionPolicy.SOURCE;
23
24import android.annotation.AnyThread;
25import android.annotation.BinderThread;
26import android.annotation.IntDef;
27import android.annotation.MainThread;
lpeter133fce02020-03-05 20:32:16 +080028import android.annotation.NonNull;
Yohei Yukawabae5bea2018-11-12 15:08:30 -080029import android.annotation.Nullable;
30import android.annotation.UserIdInt;
31import android.annotation.WorkerThread;
32import android.app.AppOpsManager;
Yohei Yukawae6e62f92019-03-09 01:15:04 -080033import android.app.Notification;
34import android.app.NotificationManager;
Yohei Yukawabae5bea2018-11-12 15:08:30 -080035import android.app.PendingIntent;
36import android.content.BroadcastReceiver;
37import android.content.ComponentName;
38import android.content.Context;
39import android.content.Intent;
40import android.content.IntentFilter;
41import android.content.ServiceConnection;
42import android.content.pm.ApplicationInfo;
43import android.content.pm.PackageManager;
44import android.content.pm.ResolveInfo;
45import android.content.pm.ServiceInfo;
46import android.inputmethodservice.MultiClientInputMethodServiceDelegate;
47import android.net.Uri;
48import android.os.Binder;
49import android.os.Build;
Yohei Yukawae6e62f92019-03-09 01:15:04 -080050import android.os.Bundle;
Yohei Yukawabae5bea2018-11-12 15:08:30 -080051import android.os.Debug;
52import android.os.Handler;
53import android.os.HandlerThread;
54import android.os.IBinder;
55import android.os.RemoteException;
56import android.os.ResultReceiver;
57import android.os.ShellCallback;
Yohei Yukawabae5bea2018-11-12 15:08:30 -080058import android.os.UserHandle;
59import android.provider.Settings;
60import android.text.TextUtils;
Yohei Yukawabae5bea2018-11-12 15:08:30 -080061import android.util.ArrayMap;
62import android.util.ArraySet;
63import android.util.Slog;
64import android.util.SparseArray;
65import android.view.InputChannel;
66import android.view.WindowManager.LayoutParams.SoftInputModeFlags;
67import android.view.inputmethod.EditorInfo;
68import android.view.inputmethod.InputConnectionInspector.MissingMethodFlags;
69import android.view.inputmethod.InputMethodInfo;
70import android.view.inputmethod.InputMethodSubtype;
71
Yohei Yukawae6e62f92019-03-09 01:15:04 -080072import com.android.internal.R;
Yohei Yukawabae5bea2018-11-12 15:08:30 -080073import com.android.internal.annotations.GuardedBy;
74import com.android.internal.inputmethod.IMultiClientInputMethod;
75import com.android.internal.inputmethod.IMultiClientInputMethodPrivilegedOperations;
76import com.android.internal.inputmethod.IMultiClientInputMethodSession;
lumarkd85e1582019-12-29 20:20:41 +080077import com.android.internal.inputmethod.SoftInputShowHideReason;
Yohei Yukawabae5bea2018-11-12 15:08:30 -080078import com.android.internal.inputmethod.StartInputFlags;
79import com.android.internal.inputmethod.StartInputReason;
80import com.android.internal.inputmethod.UnbindReason;
Yohei Yukawae6e62f92019-03-09 01:15:04 -080081import com.android.internal.messages.nano.SystemMessageProto;
82import com.android.internal.notification.SystemNotificationChannels;
Keun young Park9b673eb2019-04-29 17:54:11 -070083import com.android.internal.os.TransferPipe;
84import com.android.internal.util.DumpUtils;
85import com.android.internal.util.IndentingPrintWriter;
Yohei Yukawabae5bea2018-11-12 15:08:30 -080086import com.android.internal.util.function.pooled.PooledLambda;
Adam Hebc67f2e2019-11-13 14:34:56 -080087import com.android.internal.view.IInlineSuggestionsRequestCallback;
Yohei Yukawabae5bea2018-11-12 15:08:30 -080088import com.android.internal.view.IInputContext;
89import com.android.internal.view.IInputMethodClient;
90import com.android.internal.view.IInputMethodManager;
91import com.android.internal.view.IInputMethodSession;
Feng Cao36960ee2020-02-18 18:23:30 -080092import com.android.internal.view.InlineSuggestionsRequestInfo;
Yohei Yukawabae5bea2018-11-12 15:08:30 -080093import com.android.internal.view.InputBindResult;
94import com.android.server.LocalServices;
95import com.android.server.SystemService;
96import com.android.server.wm.WindowManagerInternal;
97
98import java.io.FileDescriptor;
Keun young Park9b673eb2019-04-29 17:54:11 -070099import java.io.IOException;
100import java.io.PrintWriter;
Yohei Yukawabae5bea2018-11-12 15:08:30 -0800101import java.lang.annotation.Retention;
102import java.util.Collections;
103import java.util.List;
104import java.util.WeakHashMap;
105
106/**
107 * Actual implementation of multi-client InputMethodManagerService.
108 *
109 * <p>This system service is intentionally compatible with {@link InputMethodManagerService} so that
110 * we can switch the implementation at the boot time.</p>
111 */
112public final class MultiClientInputMethodManagerService {
Yohei Yukawae6e62f92019-03-09 01:15:04 -0800113 private static final String TAG = "MultiClientInputMethodManagerService";
114 private static final boolean DEBUG = false;
115
116 private static final String PER_DISPLAY_FOCUS_DISABLED_WARNING_TITLE =
117 "config_perDisplayFocusEnabled is not true.";
118
119 private static final String PER_DISPLAY_FOCUS_DISABLED_WARNING_MSG =
120 "Consider rebuilding the system image after enabling config_perDisplayFocusEnabled to "
121 + "make IME focus compatible with multi-client IME mode.";
Yohei Yukawabae5bea2018-11-12 15:08:30 -0800122
Yohei Yukawabae5bea2018-11-12 15:08:30 -0800123 private static final long RECONNECT_DELAY_MSEC = 1000;
124
125 /**
126 * Unlike {@link InputMethodManagerService}, {@link MultiClientInputMethodManagerService}
127 * always binds to the IME with {@link Context#BIND_FOREGROUND_SERVICE} for now for simplicity.
128 */
129 private static final int IME_CONNECTION_UNIFIED_BIND_FLAGS =
130 Context.BIND_AUTO_CREATE
131 | Context.BIND_NOT_VISIBLE
132 | Context.BIND_NOT_FOREGROUND
133 | Context.BIND_FOREGROUND_SERVICE;
134
Yohei Yukawa6048d892018-12-25 09:57:31 -0800135 private static final ComponentName sImeComponentName =
136 InputMethodSystemProperty.sMultiClientImeComponentName;
Yohei Yukawabae5bea2018-11-12 15:08:30 -0800137
138 private static void reportNotSupported() {
139 if (DEBUG) {
140 Slog.d(TAG, "non-supported operation. callers=" + Debug.getCallers(3));
141 }
142 }
143
144 /**
145 * {@link MultiClientInputMethodManagerService} is not intended to be instantiated.
146 */
147 private MultiClientInputMethodManagerService() {
148 }
149
150 /**
151 * The implementation of {@link SystemService} for multi-client IME.
152 */
153 public static final class Lifecycle extends SystemService {
154 private final ApiCallbacks mApiCallbacks;
155 private final OnWorkerThreadCallback mOnWorkerThreadCallback;
156
157 @MainThread
158 public Lifecycle(Context context) {
159 super(context);
160
161 final UserToInputMethodInfoMap userIdToInputMethodInfoMapper =
162 new UserToInputMethodInfoMap();
163 final UserDataMap userDataMap = new UserDataMap();
164 final HandlerThread workerThread = new HandlerThread(TAG);
165 workerThread.start();
166 mApiCallbacks = new ApiCallbacks(context, userDataMap, userIdToInputMethodInfoMapper);
167 mOnWorkerThreadCallback = new OnWorkerThreadCallback(
168 context, userDataMap, userIdToInputMethodInfoMapper,
169 new Handler(workerThread.getLooper(), msg -> false, true));
170
171 LocalServices.addService(InputMethodManagerInternal.class,
172 new InputMethodManagerInternal() {
173 @Override
Ming-Shin Lu2c6e80b2020-05-13 00:12:26 +0800174 public void setInteractive(boolean interactive) {
175 reportNotSupported();
176 }
177
178 @Override
lumarkd85e1582019-12-29 20:20:41 +0800179 public void hideCurrentInputMethod(@SoftInputShowHideReason int reason) {
Yohei Yukawabae5bea2018-11-12 15:08:30 -0800180 reportNotSupported();
181 }
182
183 @Override
Yohei Yukawaa878b952019-01-10 19:36:24 -0800184 public List<InputMethodInfo> getInputMethodListAsUser(
185 @UserIdInt int userId) {
186 return userIdToInputMethodInfoMapper.getAsList(userId);
187 }
188
189 @Override
190 public List<InputMethodInfo> getEnabledInputMethodListAsUser(
191 @UserIdInt int userId) {
192 return userIdToInputMethodInfoMapper.getAsList(userId);
193 }
Adam Hebc67f2e2019-11-13 14:34:56 -0800194
195 @Override
Feng Cao16b2de52020-01-09 17:27:27 -0800196 public void onCreateInlineSuggestionsRequest(int userId,
Feng Cao36960ee2020-02-18 18:23:30 -0800197 InlineSuggestionsRequestInfo requestInfo,
Feng Cao16b2de52020-01-09 17:27:27 -0800198 IInlineSuggestionsRequestCallback cb) {
Adam Hebc67f2e2019-11-13 14:34:56 -0800199 try {
Adam Hebc67f2e2019-11-13 14:34:56 -0800200 cb.onInlineSuggestionsUnsupported();
201 } catch (RemoteException e) {
Joanne Chungd9a916f2020-01-02 19:03:16 +0800202 Slog.w(TAG, "Failed to call onInlineSuggestionsUnsupported.", e);
Adam Hebc67f2e2019-11-13 14:34:56 -0800203 }
204 }
mincheli850892b2019-12-05 19:47:59 +0800205
206 @Override
207 public boolean switchToInputMethod(String imeId, @UserIdInt int userId) {
208 reportNotSupported();
209 return false;
210 }
Yuichiro Hanada34c4c0e2020-01-15 16:53:07 +0900211
212 @Override
213 public void registerInputMethodListListener(
214 InputMethodListListener listener) {
215 reportNotSupported();
216 }
lpeter133fce02020-03-05 20:32:16 +0800217
218 @Override
219 public boolean transferTouchFocusToImeWindow(
220 @NonNull IBinder sourceInputToken, int displayId) {
221 reportNotSupported();
222 return false;
223 }
Yohei Yukawabae5bea2018-11-12 15:08:30 -0800224 });
225 }
226
227 @MainThread
228 @Override
229 public void onBootPhase(int phase) {
230 mOnWorkerThreadCallback.getHandler().sendMessage(PooledLambda.obtainMessage(
231 OnWorkerThreadCallback::onBootPhase, mOnWorkerThreadCallback, phase));
232 }
233
234 @MainThread
235 @Override
236 public void onStart() {
237 publishBinderService(Context.INPUT_METHOD_SERVICE, mApiCallbacks);
238 }
239
240 @MainThread
241 @Override
242 public void onStartUser(@UserIdInt int userId) {
243 mOnWorkerThreadCallback.getHandler().sendMessage(PooledLambda.obtainMessage(
244 OnWorkerThreadCallback::onStartUser, mOnWorkerThreadCallback, userId));
245 }
246
247 @MainThread
248 @Override
249 public void onUnlockUser(@UserIdInt int userId) {
250 mOnWorkerThreadCallback.getHandler().sendMessage(PooledLambda.obtainMessage(
251 OnWorkerThreadCallback::onUnlockUser, mOnWorkerThreadCallback, userId));
252 }
253
254 @MainThread
255 @Override
256 public void onStopUser(@UserIdInt int userId) {
257 mOnWorkerThreadCallback.getHandler().sendMessage(PooledLambda.obtainMessage(
258 OnWorkerThreadCallback::onStopUser, mOnWorkerThreadCallback, userId));
259 }
260 }
261
262 private static final class OnWorkerThreadCallback {
263 private final Context mContext;
264 private final UserDataMap mUserDataMap;
265 private final UserToInputMethodInfoMap mInputMethodInfoMap;
266 private final Handler mHandler;
267
268 OnWorkerThreadCallback(Context context, UserDataMap userDataMap,
269 UserToInputMethodInfoMap inputMethodInfoMap, Handler handler) {
270 mContext = context;
271 mUserDataMap = userDataMap;
272 mInputMethodInfoMap = inputMethodInfoMap;
273 mHandler = handler;
274 }
275
276 @AnyThread
277 Handler getHandler() {
278 return mHandler;
279 }
280
281 @WorkerThread
282 private void tryBindInputMethodService(@UserIdInt int userId) {
283 final PerUserData data = mUserDataMap.get(userId);
284 if (data == null) {
285 Slog.i(TAG, "tryBindInputMethodService is called for an unknown user=" + userId);
286 return;
287 }
288
Yohei Yukawa6048d892018-12-25 09:57:31 -0800289 final InputMethodInfo imi = queryInputMethod(mContext, userId, sImeComponentName);
Yohei Yukawabae5bea2018-11-12 15:08:30 -0800290 if (imi == null) {
291 Slog.w(TAG, "Multi-client InputMethod is not found. component="
Yohei Yukawa6048d892018-12-25 09:57:31 -0800292 + sImeComponentName);
Yohei Yukawabae5bea2018-11-12 15:08:30 -0800293 synchronized (data.mLock) {
294 switch (data.mState) {
295 case PerUserState.USER_LOCKED:
296 case PerUserState.SERVICE_NOT_QUERIED:
297 case PerUserState.SERVICE_RECOGNIZED:
298 case PerUserState.UNBIND_CALLED:
299 // Safe to clean up.
300 mInputMethodInfoMap.remove(userId);
301 break;
302 }
303 }
304 return;
305 }
306
307 synchronized (data.mLock) {
308 switch (data.mState) {
309 case PerUserState.USER_LOCKED:
310 // If the user is still locked, we currently do not try to start IME.
311 return;
312 case PerUserState.SERVICE_NOT_QUERIED:
313 case PerUserState.SERVICE_RECOGNIZED:
314 case PerUserState.UNBIND_CALLED:
315 break;
316 case PerUserState.WAITING_SERVICE_CONNECTED:
317 case PerUserState.SERVICE_CONNECTED:
318 // OK, nothing to do.
319 return;
320 default:
321 Slog.wtf(TAG, "Unknown state=" + data.mState);
322 return;
323 }
324 data.mState = PerUserState.SERVICE_RECOGNIZED;
325 data.mCurrentInputMethodInfo = imi;
326 mInputMethodInfoMap.put(userId, imi);
327 final boolean bindResult = data.bindServiceLocked(mContext, userId);
328 if (!bindResult) {
329 Slog.e(TAG, "Failed to bind Multi-client InputMethod.");
330 return;
331 }
332 data.mState = PerUserState.WAITING_SERVICE_CONNECTED;
333 }
334 }
335
336 @WorkerThread
337 void onStartUser(@UserIdInt int userId) {
338 if (DEBUG) {
339 Slog.v(TAG, "onStartUser userId=" + userId);
340 }
341 final PerUserData data = new PerUserData(userId, null, PerUserState.USER_LOCKED, this);
342 mUserDataMap.put(userId, data);
343 }
344
345 @WorkerThread
346 void onUnlockUser(@UserIdInt int userId) {
347 if (DEBUG) {
348 Slog.v(TAG, "onUnlockUser() userId=" + userId);
349 }
350 final PerUserData data = mUserDataMap.get(userId);
351 if (data == null) {
352 Slog.i(TAG, "onUnlockUser is called for an unknown user=" + userId);
353 return;
354 }
355 synchronized (data.mLock) {
356 switch (data.mState) {
357 case PerUserState.USER_LOCKED:
358 data.mState = PerUserState.SERVICE_NOT_QUERIED;
359 tryBindInputMethodService(userId);
360 break;
361 default:
362 Slog.wtf(TAG, "Unknown state=" + data.mState);
363 break;
364 }
365 }
366 }
367
368 @WorkerThread
369 void onStopUser(@UserIdInt int userId) {
370 if (DEBUG) {
371 Slog.v(TAG, "onStopUser() userId=" + userId);
372 }
373 mInputMethodInfoMap.remove(userId);
374 final PerUserData data = mUserDataMap.removeReturnOld(userId);
375 if (data == null) {
376 Slog.i(TAG, "onStopUser is called for an unknown user=" + userId);
377 return;
378 }
379 synchronized (data.mLock) {
380 switch (data.mState) {
381 case PerUserState.USER_LOCKED:
382 case PerUserState.SERVICE_RECOGNIZED:
383 case PerUserState.UNBIND_CALLED:
384 // OK, nothing to do.
385 return;
386 case PerUserState.SERVICE_CONNECTED:
387 case PerUserState.WAITING_SERVICE_CONNECTED:
388 break;
389 default:
390 Slog.wtf(TAG, "Unknown state=" + data.mState);
391 break;
392 }
393 data.unbindServiceLocked(mContext);
394 data.mState = PerUserState.UNBIND_CALLED;
395 data.mCurrentInputMethod = null;
396
397 // When a Service is explicitly unbound with Context.unbindService(),
398 // onServiceDisconnected() will not be triggered. Hence here we explicitly call
399 // onInputMethodDisconnectedLocked() as if the Service is already gone.
400 data.onInputMethodDisconnectedLocked();
401 }
402 }
403
404 @WorkerThread
405 void onServiceConnected(PerUserData data, IMultiClientInputMethod service) {
406 if (DEBUG) {
407 Slog.v(TAG, "onServiceConnected() data.mUserId=" + data.mUserId);
408 }
409 synchronized (data.mLock) {
410 switch (data.mState) {
411 case PerUserState.UNBIND_CALLED:
412 // We should ignore this callback.
413 return;
414 case PerUserState.WAITING_SERVICE_CONNECTED:
415 // OK.
416 data.mState = PerUserState.SERVICE_CONNECTED;
417 data.mCurrentInputMethod = service;
418 try {
419 data.mCurrentInputMethod.initialize(new ImeCallbacks(data));
420 } catch (RemoteException e) {
421 }
422 data.onInputMethodConnectedLocked();
423 break;
424 default:
425 Slog.wtf(TAG, "Unknown state=" + data.mState);
426 return;
427 }
428 }
429 }
430
431 @WorkerThread
432 void onServiceDisconnected(PerUserData data) {
433 if (DEBUG) {
434 Slog.v(TAG, "onServiceDisconnected() data.mUserId=" + data.mUserId);
435 }
436 final WindowManagerInternal windowManagerInternal =
437 LocalServices.getService(WindowManagerInternal.class);
438 synchronized (data.mLock) {
439 // We assume the number of tokens would not be that large (up to 10 or so) hence
440 // linear search should be acceptable.
441 final int numTokens = data.mDisplayIdToImeWindowTokenMap.size();
442 for (int i = 0; i < numTokens; ++i) {
443 final TokenInfo info = data.mDisplayIdToImeWindowTokenMap.valueAt(i);
444 windowManagerInternal.removeWindowToken(info.mToken, false, info.mDisplayId);
445 }
446 data.mDisplayIdToImeWindowTokenMap.clear();
447 switch (data.mState) {
448 case PerUserState.UNBIND_CALLED:
449 // We should ignore this callback.
450 return;
451 case PerUserState.WAITING_SERVICE_CONNECTED:
452 case PerUserState.SERVICE_CONNECTED:
453 // onServiceDisconnected() means the biding is still alive.
454 data.mState = PerUserState.WAITING_SERVICE_CONNECTED;
455 data.mCurrentInputMethod = null;
456 data.onInputMethodDisconnectedLocked();
457 break;
458 default:
459 Slog.wtf(TAG, "Unknown state=" + data.mState);
460 return;
461 }
462 }
463 }
464
465 @WorkerThread
466 void onBindingDied(PerUserData data) {
467 if (DEBUG) {
468 Slog.v(TAG, "onBindingDied() data.mUserId=" + data.mUserId);
469 }
470 final WindowManagerInternal windowManagerInternal =
471 LocalServices.getService(WindowManagerInternal.class);
472 synchronized (data.mLock) {
473 // We assume the number of tokens would not be that large (up to 10 or so) hence
474 // linear search should be acceptable.
475 final int numTokens = data.mDisplayIdToImeWindowTokenMap.size();
476 for (int i = 0; i < numTokens; ++i) {
477 final TokenInfo info = data.mDisplayIdToImeWindowTokenMap.valueAt(i);
478 windowManagerInternal.removeWindowToken(info.mToken, false, info.mDisplayId);
479 }
480 data.mDisplayIdToImeWindowTokenMap.clear();
481 switch (data.mState) {
482 case PerUserState.UNBIND_CALLED:
483 // We should ignore this callback.
484 return;
485 case PerUserState.WAITING_SERVICE_CONNECTED:
486 case PerUserState.SERVICE_CONNECTED: {
487 // onBindingDied() means the biding is dead.
488 data.mState = PerUserState.UNBIND_CALLED;
489 data.mCurrentInputMethod = null;
490 data.onInputMethodDisconnectedLocked();
491 // Schedule a retry
492 mHandler.sendMessageDelayed(PooledLambda.obtainMessage(
493 OnWorkerThreadCallback::tryBindInputMethodService,
494 this, data.mUserId), RECONNECT_DELAY_MSEC);
495 break;
496 }
497 default:
498 Slog.wtf(TAG, "Unknown state=" + data.mState);
499 return;
500 }
501 }
502 }
503
504 @WorkerThread
505 void onBootPhase(int phase) {
506 if (DEBUG) {
507 Slog.v(TAG, "onBootPhase() phase=" + phase);
508 }
509 switch (phase) {
510 case SystemService.PHASE_ACTIVITY_MANAGER_READY: {
511 final IntentFilter filter = new IntentFilter(Intent.ACTION_PACKAGE_ADDED);
512 filter.addDataScheme("package");
513 mContext.registerReceiver(new BroadcastReceiver() {
514 @Override
515 public void onReceive(Context context, Intent intent) {
516 onPackageAdded(intent);
517 }
518 }, filter, null, mHandler);
Yohei Yukawae6e62f92019-03-09 01:15:04 -0800519 break;
Yohei Yukawabae5bea2018-11-12 15:08:30 -0800520 }
Yohei Yukawae6e62f92019-03-09 01:15:04 -0800521 case SystemService.PHASE_BOOT_COMPLETED: {
522 final boolean perDisplayFocusEnabled = mContext.getResources().getBoolean(
523 com.android.internal.R.bool.config_perDisplayFocusEnabled);
524 if (!perDisplayFocusEnabled) {
525 final Bundle extras = new Bundle();
526 extras.putBoolean(Notification.EXTRA_ALLOW_DURING_SETUP, true);
527 mContext.getSystemService(NotificationManager.class).notifyAsUser(TAG,
528 SystemMessageProto.SystemMessage.NOTE_SELECT_INPUT_METHOD,
529 new Notification.Builder(mContext,
530 SystemNotificationChannels.VIRTUAL_KEYBOARD)
531 .setContentTitle(PER_DISPLAY_FOCUS_DISABLED_WARNING_TITLE)
532 .setStyle(new Notification.BigTextStyle()
533 .bigText(PER_DISPLAY_FOCUS_DISABLED_WARNING_MSG))
534 .setSmallIcon(R.drawable.ic_notification_ime_default)
535 .setWhen(0)
536 .setOngoing(true)
537 .setLocalOnly(true)
538 .addExtras(extras)
539 .setCategory(Notification.CATEGORY_SYSTEM)
540 .setColor(mContext.getColor(
541 R.color.system_notification_accent_color))
542 .build(), UserHandle.ALL);
543 }
544 break;
545 }
Yohei Yukawabae5bea2018-11-12 15:08:30 -0800546 }
547 }
548
549 @WorkerThread
550 void onPackageAdded(Intent intent) {
551 if (DEBUG) {
552 Slog.v(TAG, "onPackageAdded() intent=" + intent);
553 }
554 final Uri uri = intent.getData();
555 if (uri == null) {
556 return;
557 }
558 if (!intent.hasExtra(Intent.EXTRA_UID)) {
559 return;
560 }
561 final String packageName = uri.getSchemeSpecificPart();
Yohei Yukawa6048d892018-12-25 09:57:31 -0800562 if (sImeComponentName == null
Yohei Yukawabae5bea2018-11-12 15:08:30 -0800563 || packageName == null
Yohei Yukawa6048d892018-12-25 09:57:31 -0800564 || !TextUtils.equals(sImeComponentName.getPackageName(), packageName)) {
Yohei Yukawabae5bea2018-11-12 15:08:30 -0800565 return;
566 }
567 final int userId = UserHandle.getUserId(intent.getIntExtra(Intent.EXTRA_UID, 0));
568 tryBindInputMethodService(userId);
569 }
570 }
571
572 private static final class WindowInfo {
573 final IBinder mWindowToken;
574 final int mWindowHandle;
575
576 WindowInfo(IBinder windowToken, int windowCookie) {
577 mWindowToken = windowToken;
578 mWindowHandle = windowCookie;
579 }
580 }
581
582 /**
583 * Describes the state of each IME client.
584 */
585 @Retention(SOURCE)
586 @IntDef({InputMethodClientState.REGISTERED,
587 InputMethodClientState.WAITING_FOR_IME_SESSION,
588 InputMethodClientState.READY_TO_SEND_FIRST_BIND_RESULT,
589 InputMethodClientState.ALREADY_SENT_BIND_RESULT,
590 InputMethodClientState.UNREGISTERED})
591 private @interface InputMethodClientState {
592 /**
593 * {@link IInputMethodManager#addClient(IInputMethodClient, IInputContext, int)} is called
594 * and this client is now recognized by the system. When the system lost the connection to
595 * the current IME, all the clients need to be re-initialized from this state.
596 */
597 int REGISTERED = 1;
598 /**
599 * This client is notified to the current IME with {@link
600 * IMultiClientInputMethod#addClient(int, int, int, int)} but the IME is not yet responded
601 * with {@link IMultiClientInputMethodPrivilegedOperations#acceptClient(int,
602 * IInputMethodSession, IMultiClientInputMethodSession, InputChannel)}.
603 */
604 int WAITING_FOR_IME_SESSION = 2;
605 /**
606 * This client is already accepted by the IME but a valid {@link InputBindResult} has not
607 * been returned to the client yet.
608 */
609 int READY_TO_SEND_FIRST_BIND_RESULT = 3;
610 /**
611 * This client has already received a valid {@link InputBindResult} at least once. This
612 * means that the client can directly call {@link IInputMethodSession} IPCs and key events
613 * via {@link InputChannel}. When the current IME is unbound, these client end points also
614 * need to be cleared.
615 */
616 int ALREADY_SENT_BIND_RESULT = 4;
617 /**
618 * The client process is dying.
619 */
620 int UNREGISTERED = 5;
621 }
622
623 private static final class InputMethodClientIdSource {
624 @GuardedBy("InputMethodClientIdSource.class")
625 private static int sNextValue = 0;
626
627 private InputMethodClientIdSource() {
628 }
629
630 static synchronized int getNext() {
631 final int result = sNextValue;
632 sNextValue++;
633 if (sNextValue < 0) {
634 sNextValue = 0;
635 }
636 return result;
637 }
638 }
639
640 private static final class WindowHandleSource {
641 @GuardedBy("WindowHandleSource.class")
642 private static int sNextValue = 0;
643
644 private WindowHandleSource() {
645 }
646
647 static synchronized int getNext() {
648 final int result = sNextValue;
649 sNextValue++;
650 if (sNextValue < 0) {
651 sNextValue = 0;
652 }
653 return result;
654 }
655 }
656
657 private static final class InputMethodClientInfo {
658 final IInputMethodClient mClient;
659 final int mUid;
660 final int mPid;
661 final int mSelfReportedDisplayId;
662 final int mClientId;
663
664 @GuardedBy("PerUserData.mLock")
665 @InputMethodClientState
666 int mState;
667 @GuardedBy("PerUserData.mLock")
668 int mBindingSequence;
669 @GuardedBy("PerUserData.mLock")
670 InputChannel mWriteChannel;
671 @GuardedBy("PerUserData.mLock")
672 IInputMethodSession mInputMethodSession;
673 @GuardedBy("PerUserData.mLock")
674 IMultiClientInputMethodSession mMSInputMethodSession;
675 @GuardedBy("PerUserData.mLock")
676 final WeakHashMap<IBinder, WindowInfo> mWindowMap = new WeakHashMap<>();
677
678 InputMethodClientInfo(IInputMethodClient client, int uid, int pid,
679 int selfReportedDisplayId) {
680 mClient = client;
681 mUid = uid;
682 mPid = pid;
683 mSelfReportedDisplayId = selfReportedDisplayId;
684 mClientId = InputMethodClientIdSource.getNext();
685 }
Keun young Park9b673eb2019-04-29 17:54:11 -0700686
687 @GuardedBy("PerUserData.mLock")
688 void dumpLocked(FileDescriptor fd, IndentingPrintWriter ipw, String[] args) {
689 ipw.println("mState=" + mState + ",mBindingSequence=" + mBindingSequence
690 + ",mWriteChannel=" + mWriteChannel
691 + ",mInputMethodSession=" + mInputMethodSession
692 + ",mMSInputMethodSession=" + mMSInputMethodSession);
693 }
Yohei Yukawabae5bea2018-11-12 15:08:30 -0800694 }
695
696 private static final class UserDataMap {
697 @GuardedBy("mMap")
698 private final SparseArray<PerUserData> mMap = new SparseArray<>();
699
700 @AnyThread
701 @Nullable
702 PerUserData get(@UserIdInt int userId) {
703 synchronized (mMap) {
704 return mMap.get(userId);
705 }
706 }
707
708 @AnyThread
709 void put(@UserIdInt int userId, PerUserData data) {
710 synchronized (mMap) {
711 mMap.put(userId, data);
712 }
713 }
714
715 @AnyThread
716 @Nullable
717 PerUserData removeReturnOld(@UserIdInt int userId) {
718 synchronized (mMap) {
719 return mMap.removeReturnOld(userId);
720 }
721 }
Keun young Park9b673eb2019-04-29 17:54:11 -0700722
723 @AnyThread
724 void dump(FileDescriptor fd, IndentingPrintWriter ipw, String[] args) {
725 synchronized (mMap) {
726 for (int i = 0; i < mMap.size(); i++) {
727 int userId = mMap.keyAt(i);
728 PerUserData data = mMap.valueAt(i);
729 ipw.println("userId=" + userId + ", data=");
730 if (data != null) {
731 ipw.increaseIndent();
732 data.dump(fd, ipw, args);
733 ipw.decreaseIndent();
734 }
735 }
736 }
737 }
Yohei Yukawabae5bea2018-11-12 15:08:30 -0800738 }
739
740 private static final class TokenInfo {
741 final Binder mToken;
742 final int mDisplayId;
743 TokenInfo(Binder token, int displayId) {
744 mToken = token;
745 mDisplayId = displayId;
746 }
747 }
748
749 @Retention(SOURCE)
750 @IntDef({
751 PerUserState.USER_LOCKED,
752 PerUserState.SERVICE_NOT_QUERIED,
753 PerUserState.SERVICE_RECOGNIZED,
754 PerUserState.WAITING_SERVICE_CONNECTED,
755 PerUserState.SERVICE_CONNECTED,
756 PerUserState.UNBIND_CALLED})
757 private @interface PerUserState {
758 /**
759 * The user is still locked.
760 */
761 int USER_LOCKED = 1;
762 /**
763 * The system has not queried whether there is a multi-client IME or not.
764 */
765 int SERVICE_NOT_QUERIED = 2;
766 /**
767 * A multi-client IME specified in {@link #PROP_DEBUG_MULTI_CLIENT_IME} is found in the
768 * system, but not bound yet.
769 */
770 int SERVICE_RECOGNIZED = 3;
771 /**
772 * {@link Context#bindServiceAsUser(Intent, ServiceConnection, int, Handler, UserHandle)} is
773 * already called for the IME but
774 * {@link ServiceConnection#onServiceConnected(ComponentName, IBinder)} is not yet called
775 * back. This includes once the IME is bound but temporarily disconnected as notified with
776 * {@link ServiceConnection#onServiceDisconnected(ComponentName)}.
777 */
778 int WAITING_SERVICE_CONNECTED = 4;
779 /**
780 * {@link ServiceConnection#onServiceConnected(ComponentName, IBinder)} is already called
781 * back. The IME is ready to be used.
782 */
783 int SERVICE_CONNECTED = 5;
784 /**
785 * The binding is gone. Either {@link Context#unbindService(ServiceConnection)} is
786 * explicitly called or the system decided to destroy the binding as notified with
787 * {@link ServiceConnection#onBindingDied(ComponentName)}.
788 */
789 int UNBIND_CALLED = 6;
790 }
791
792 /**
793 * Takes care of per-user state separation.
794 */
795 private static final class PerUserData {
796 final Object mLock = new Object();
797
798 /**
799 * User ID (not UID) that is associated with this data.
800 */
801 @UserIdInt
802 private final int mUserId;
803
804 /**
805 * {@link IMultiClientInputMethod} of the currently connected multi-client IME. This
806 * must be non-{@code null} only while {@link #mState} is
807 * {@link PerUserState#SERVICE_CONNECTED}.
808 */
809 @Nullable
810 @GuardedBy("mLock")
811 IMultiClientInputMethod mCurrentInputMethod;
812
813 /**
814 * {@link InputMethodInfo} of the currently selected multi-client IME. This must be
815 * non-{@code null} unless {@link #mState} is {@link PerUserState#SERVICE_NOT_QUERIED}.
816 */
817 @GuardedBy("mLock")
818 @Nullable
819 InputMethodInfo mCurrentInputMethodInfo;
820
821 /**
822 * Describes the current service state.
823 */
824 @GuardedBy("mLock")
825 @PerUserState
826 int mState;
827
828 /**
829 * A {@link SparseArray} that maps display ID to IME Window token that is already issued to
830 * the IME.
831 */
832 @GuardedBy("mLock")
833 final ArraySet<TokenInfo> mDisplayIdToImeWindowTokenMap = new ArraySet<>();
834
835 @GuardedBy("mLock")
836 private final ArrayMap<IBinder, InputMethodClientInfo> mClientMap = new ArrayMap<>();
837
838 @GuardedBy("mLock")
839 private SparseArray<InputMethodClientInfo> mClientIdToClientMap = new SparseArray<>();
840
841 private final OnWorkerThreadServiceConnection mOnWorkerThreadServiceConnection;
842
843 /**
844 * A {@link ServiceConnection} that is designed to run on a certain worker thread with
845 * which {@link OnWorkerThreadCallback} is associated.
846 *
847 * @see Context#bindServiceAsUser(Intent, ServiceConnection, int, Handler, UserHandle).
848 */
849 private static final class OnWorkerThreadServiceConnection implements ServiceConnection {
850 private final PerUserData mData;
851 private final OnWorkerThreadCallback mCallback;
852
853 OnWorkerThreadServiceConnection(PerUserData data, OnWorkerThreadCallback callback) {
854 mData = data;
855 mCallback = callback;
856 }
857
858 @WorkerThread
859 @Override
860 public void onServiceConnected(ComponentName name, IBinder service) {
861 mCallback.onServiceConnected(mData,
862 IMultiClientInputMethod.Stub.asInterface(service));
863 }
864
865 @WorkerThread
866 @Override
867 public void onServiceDisconnected(ComponentName name) {
868 mCallback.onServiceDisconnected(mData);
869 }
870
871 @WorkerThread
872 @Override
873 public void onBindingDied(ComponentName name) {
874 mCallback.onBindingDied(mData);
875 }
876
877 Handler getHandler() {
878 return mCallback.getHandler();
879 }
880 }
881
882 PerUserData(@UserIdInt int userId, @Nullable InputMethodInfo inputMethodInfo,
883 @PerUserState int initialState, OnWorkerThreadCallback callback) {
884 mUserId = userId;
885 mCurrentInputMethodInfo = inputMethodInfo;
886 mState = initialState;
887 mOnWorkerThreadServiceConnection =
888 new OnWorkerThreadServiceConnection(this, callback);
889 }
890
891 @GuardedBy("mLock")
892 boolean bindServiceLocked(Context context, @UserIdInt int userId) {
893 final Intent intent =
894 new Intent(MultiClientInputMethodServiceDelegate.SERVICE_INTERFACE)
895 .setComponent(mCurrentInputMethodInfo.getComponent())
896 .putExtra(Intent.EXTRA_CLIENT_LABEL,
897 com.android.internal.R.string.input_method_binding_label)
898 .putExtra(Intent.EXTRA_CLIENT_INTENT, PendingIntent.getActivity(
899 context, 0,
900 new Intent(Settings.ACTION_INPUT_METHOD_SETTINGS), 0));
901
902 // Note: Instead of re-dispatching callback from the main thread to the worker thread
903 // where OnWorkerThreadCallback is running, we pass the Handler object here so that
904 // the callbacks will be directly dispatched to the worker thread.
905 return context.bindServiceAsUser(intent, mOnWorkerThreadServiceConnection,
906 IME_CONNECTION_UNIFIED_BIND_FLAGS,
907 mOnWorkerThreadServiceConnection.getHandler(), UserHandle.of(userId));
908 }
909
910 @GuardedBy("mLock")
911 void unbindServiceLocked(Context context) {
912 context.unbindService(mOnWorkerThreadServiceConnection);
913 }
914
915 @GuardedBy("mLock")
916 @Nullable
917 InputMethodClientInfo getClientLocked(IInputMethodClient client) {
918 return mClientMap.get(client.asBinder());
919 }
920
921 @GuardedBy("mLock")
922 @Nullable
923 InputMethodClientInfo getClientFromIdLocked(int clientId) {
924 return mClientIdToClientMap.get(clientId);
925 }
926
927 @GuardedBy("mLock")
928 @Nullable
929 InputMethodClientInfo removeClientLocked(IInputMethodClient client) {
930 final InputMethodClientInfo info = mClientMap.remove(client.asBinder());
931 if (info != null) {
932 mClientIdToClientMap.remove(info.mClientId);
933 }
934 return info;
935 }
936
937 @GuardedBy("mLock")
938 void addClientLocked(int uid, int pid, IInputMethodClient client,
939 int selfReportedDisplayId) {
940 if (getClientLocked(client) != null) {
941 Slog.wtf(TAG, "The same client is added multiple times");
942 return;
943 }
944 final ClientDeathRecipient deathRecipient = new ClientDeathRecipient(this, client);
945 try {
946 client.asBinder().linkToDeath(deathRecipient, 0);
947 } catch (RemoteException e) {
948 throw new IllegalStateException(e);
949 }
950 final InputMethodClientInfo clientInfo =
951 new InputMethodClientInfo(client, uid, pid, selfReportedDisplayId);
952 clientInfo.mState = InputMethodClientState.REGISTERED;
953 mClientMap.put(client.asBinder(), clientInfo);
954 mClientIdToClientMap.put(clientInfo.mClientId, clientInfo);
955 switch (mState) {
956 case PerUserState.SERVICE_CONNECTED:
957 try {
958 mCurrentInputMethod.addClient(
959 clientInfo.mClientId, clientInfo.mPid, clientInfo.mUid,
960 clientInfo.mSelfReportedDisplayId);
961 clientInfo.mState = InputMethodClientState.WAITING_FOR_IME_SESSION;
962 } catch (RemoteException e) {
963 // TODO(yukawa): Need logging and expected behavior
964 }
965 break;
966 }
967 }
968
969 @GuardedBy("mLock")
970 void onInputMethodConnectedLocked() {
971 final int numClients = mClientMap.size();
972 for (int i = 0; i < numClients; ++i) {
973 final InputMethodClientInfo clientInfo = mClientMap.valueAt(i);
974 switch (clientInfo.mState) {
975 case InputMethodClientState.REGISTERED:
976 // OK
977 break;
978 default:
979 Slog.e(TAG, "Unexpected state=" + clientInfo.mState);
980 return;
981 }
982 try {
983 mCurrentInputMethod.addClient(
984 clientInfo.mClientId, clientInfo.mUid, clientInfo.mPid,
985 clientInfo.mSelfReportedDisplayId);
986 clientInfo.mState = InputMethodClientState.WAITING_FOR_IME_SESSION;
987 } catch (RemoteException e) {
988 }
989 }
990 }
991
992 @GuardedBy("mLock")
993 void onInputMethodDisconnectedLocked() {
994 final int numClients = mClientMap.size();
995 for (int i = 0; i < numClients; ++i) {
996 final InputMethodClientInfo clientInfo = mClientMap.valueAt(i);
997 switch (clientInfo.mState) {
998 case InputMethodClientState.REGISTERED:
999 // Disconnected before onInputMethodConnectedLocked().
1000 break;
1001 case InputMethodClientState.WAITING_FOR_IME_SESSION:
1002 // Disconnected between addClient() and acceptClient().
1003 clientInfo.mState = InputMethodClientState.REGISTERED;
1004 break;
1005 case InputMethodClientState.READY_TO_SEND_FIRST_BIND_RESULT:
1006 clientInfo.mState = InputMethodClientState.REGISTERED;
1007 clientInfo.mInputMethodSession = null;
1008 clientInfo.mMSInputMethodSession = null;
1009 if (clientInfo.mWriteChannel != null) {
1010 clientInfo.mWriteChannel.dispose();
1011 clientInfo.mWriteChannel = null;
1012 }
1013 break;
1014 case InputMethodClientState.ALREADY_SENT_BIND_RESULT:
1015 try {
1016 clientInfo.mClient.onUnbindMethod(clientInfo.mBindingSequence,
1017 UnbindReason.DISCONNECT_IME);
1018 } catch (RemoteException e) {
1019 }
1020 clientInfo.mState = InputMethodClientState.REGISTERED;
1021 clientInfo.mInputMethodSession = null;
1022 clientInfo.mMSInputMethodSession = null;
1023 if (clientInfo.mWriteChannel != null) {
1024 clientInfo.mWriteChannel.dispose();
1025 clientInfo.mWriteChannel = null;
1026 }
1027 break;
1028 }
1029 }
1030 }
1031
Keun young Park9b673eb2019-04-29 17:54:11 -07001032 @AnyThread
1033 void dump(FileDescriptor fd, IndentingPrintWriter ipw, String[] args) {
1034 synchronized (mLock) {
1035 ipw.println("mState=" + mState
1036 + ",mCurrentInputMethod=" + mCurrentInputMethod
1037 + ",mCurrentInputMethodInfo=" + mCurrentInputMethodInfo);
1038
1039 if (mCurrentInputMethod != null) {
1040 // indentation will not be kept. So add visual separator here.
1041 ipw.println(">>Dump CurrentInputMethod>>");
1042 ipw.flush();
1043 try {
1044 TransferPipe.dumpAsync(mCurrentInputMethod.asBinder(), fd, args);
1045 } catch (IOException | RemoteException e) {
1046 ipw.println("Failed to dump input method service: " + e);
1047 }
1048 ipw.println("<<Dump CurrentInputMethod<<");
1049 }
1050
1051 ipw.println("mDisplayIdToImeWindowTokenMap=");
1052 for (TokenInfo info : mDisplayIdToImeWindowTokenMap) {
1053 ipw.println(" display=" + info.mDisplayId + ",token="
1054 + info.mToken);
1055 }
1056 ipw.println("mClientMap=");
1057 ipw.increaseIndent();
1058 for (int i = 0; i < mClientMap.size(); i++) {
1059
1060 ipw.println("binder=" + mClientMap.keyAt(i));
1061 ipw.println(" InputMethodClientInfo=");
1062 InputMethodClientInfo info = mClientMap.valueAt(i);
1063 if (info != null) {
1064 ipw.increaseIndent();
1065 info.dumpLocked(fd, ipw, args);
1066 ipw.decreaseIndent();
1067 }
1068 }
1069 ipw.decreaseIndent();
1070 ipw.println("mClientIdToClientMap=");
1071 ipw.increaseIndent();
1072 for (int i = 0; i < mClientIdToClientMap.size(); i++) {
1073 ipw.println("clientId=" + mClientIdToClientMap.keyAt(i));
1074 ipw.println(" InputMethodClientInfo=");
1075 InputMethodClientInfo info = mClientIdToClientMap.valueAt(i);
1076 if (info != null) {
1077 ipw.increaseIndent();
1078 info.dumpLocked(fd, ipw, args);
1079 ipw.decreaseIndent();
1080 }
1081 if (info.mClient != null) {
1082 // indentation will not be kept. So add visual separator here.
1083 ipw.println(">>DumpClientStart>>");
1084 ipw.flush(); // all writes should be flushed to guarantee order.
1085 try {
1086 TransferPipe.dumpAsync(info.mClient.asBinder(), fd, args);
1087 } catch (IOException | RemoteException e) {
1088 ipw.println(" Failed to dump client:" + e);
1089 }
1090 ipw.println("<<DumpClientEnd<<");
1091 }
1092 }
1093 ipw.decreaseIndent();
1094 }
1095 }
1096
Yohei Yukawabae5bea2018-11-12 15:08:30 -08001097 private static final class ClientDeathRecipient implements IBinder.DeathRecipient {
1098 private final PerUserData mPerUserData;
1099 private final IInputMethodClient mClient;
1100
1101 ClientDeathRecipient(PerUserData perUserData, IInputMethodClient client) {
1102 mPerUserData = perUserData;
1103 mClient = client;
1104 }
1105
1106 @BinderThread
1107 @Override
1108 public void binderDied() {
1109 synchronized (mPerUserData.mLock) {
1110 mClient.asBinder().unlinkToDeath(this, 0);
1111
1112 final InputMethodClientInfo clientInfo =
1113 mPerUserData.removeClientLocked(mClient);
1114 if (clientInfo == null) {
1115 return;
1116 }
1117
1118 if (clientInfo.mWriteChannel != null) {
1119 clientInfo.mWriteChannel.dispose();
1120 clientInfo.mWriteChannel = null;
1121 }
1122 if (clientInfo.mInputMethodSession != null) {
1123 try {
1124 clientInfo.mInputMethodSession.finishSession();
1125 } catch (RemoteException e) {
1126 }
1127 clientInfo.mInputMethodSession = null;
1128 }
1129 clientInfo.mMSInputMethodSession = null;
1130 clientInfo.mState = InputMethodClientState.UNREGISTERED;
1131 switch (mPerUserData.mState) {
1132 case PerUserState.SERVICE_CONNECTED:
1133 try {
1134 mPerUserData.mCurrentInputMethod.removeClient(clientInfo.mClientId);
1135 } catch (RemoteException e) {
1136 // TODO(yukawa): Need logging and expected behavior
1137 }
1138 break;
1139 }
1140 }
1141 }
1142 }
1143 }
1144
1145 /**
1146 * Queries for multi-client IME specified with {@code componentName}.
1147 *
1148 * @param context {@link Context} to be used to query component.
1149 * @param userId User ID for which the multi-client IME is queried.
1150 * @param componentName {@link ComponentName} to be queried.
1151 * @return {@link InputMethodInfo} when multi-client IME is found. Otherwise {@code null}.
1152 */
1153 @Nullable
1154 private static InputMethodInfo queryInputMethod(Context context, @UserIdInt int userId,
1155 @Nullable ComponentName componentName) {
1156 if (componentName == null) {
1157 return null;
1158 }
1159
1160 // Use for queryIntentServicesAsUser
1161 final PackageManager pm = context.getPackageManager();
1162 final List<ResolveInfo> services = pm.queryIntentServicesAsUser(
1163 new Intent(MultiClientInputMethodServiceDelegate.SERVICE_INTERFACE)
1164 .setComponent(componentName),
1165 PackageManager.GET_META_DATA, userId);
1166
1167 if (services.isEmpty()) {
1168 Slog.e(TAG, "No IME found");
1169 return null;
1170 }
1171
1172 if (services.size() > 1) {
1173 Slog.e(TAG, "Only one IME service is supported.");
1174 return null;
1175 }
1176
1177 final ResolveInfo ri = services.get(0);
1178 ServiceInfo si = ri.serviceInfo;
1179 final String imeId = InputMethodInfo.computeId(ri);
1180 if (!android.Manifest.permission.BIND_INPUT_METHOD.equals(si.permission)) {
1181 Slog.e(TAG, imeId + " must have required"
1182 + android.Manifest.permission.BIND_INPUT_METHOD);
1183 return null;
1184 }
1185
1186 if (!Build.IS_DEBUGGABLE && (si.applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) == 0) {
1187 Slog.e(TAG, imeId + " must be pre-installed when Build.IS_DEBUGGABLE is false");
1188 return null;
1189 }
1190
1191 try {
1192 return new InputMethodInfo(context, ri);
1193 } catch (Exception e) {
1194 Slog.wtf(TAG, "Unable to load input method " + imeId, e);
1195 }
1196 return null;
1197 }
1198
1199 /**
1200 * Manages the mapping rule from user ID to {@link InputMethodInfo}.
1201 */
1202 private static final class UserToInputMethodInfoMap {
1203 @GuardedBy("mArray")
1204 private final SparseArray<InputMethodInfo> mArray = new SparseArray<>();
1205
1206 @AnyThread
1207 void put(@UserIdInt int userId, InputMethodInfo imi) {
1208 synchronized (mArray) {
1209 mArray.put(userId, imi);
1210 }
1211 }
1212
1213 @AnyThread
1214 void remove(@UserIdInt int userId) {
1215 synchronized (mArray) {
1216 mArray.remove(userId);
1217 }
1218 }
1219
1220 @AnyThread
1221 @Nullable
1222 InputMethodInfo get(@UserIdInt int userId) {
1223 synchronized (mArray) {
1224 return mArray.get(userId);
1225 }
1226 }
1227
1228 @AnyThread
1229 List<InputMethodInfo> getAsList(@UserIdInt int userId) {
1230 final InputMethodInfo info = get(userId);
1231 if (info == null) {
1232 return Collections.emptyList();
1233 }
1234 return Collections.singletonList(info);
1235 }
Keun young Park9b673eb2019-04-29 17:54:11 -07001236
1237 @AnyThread
1238 void dump(FileDescriptor fd, IndentingPrintWriter ipw, String[] args) {
1239 synchronized (mArray) {
1240 for (int i = 0; i < mArray.size(); i++) {
1241 ipw.println("userId=" + mArray.keyAt(i));
1242 ipw.println(" InputMethodInfo=" + mArray.valueAt(i));
1243 }
1244 }
1245 }
Yohei Yukawabae5bea2018-11-12 15:08:30 -08001246 }
1247
1248 /**
1249 * Takes care of IPCs exposed to the multi-client IME.
1250 */
1251 private static final class ImeCallbacks
1252 extends IMultiClientInputMethodPrivilegedOperations.Stub {
1253 private final PerUserData mPerUserData;
1254 private final WindowManagerInternal mIWindowManagerInternal;
1255
1256 ImeCallbacks(PerUserData perUserData) {
1257 mPerUserData = perUserData;
1258 mIWindowManagerInternal = LocalServices.getService(WindowManagerInternal.class);
1259 }
1260
1261 @BinderThread
1262 @Override
1263 public IBinder createInputMethodWindowToken(int displayId) {
1264 synchronized (mPerUserData.mLock) {
1265 // We assume the number of tokens would not be that large (up to 10 or so) hence
1266 // linear search should be acceptable.
1267 final int numTokens = mPerUserData.mDisplayIdToImeWindowTokenMap.size();
1268 for (int i = 0; i < numTokens; ++i) {
1269 final TokenInfo tokenInfo =
1270 mPerUserData.mDisplayIdToImeWindowTokenMap.valueAt(i);
1271 // Currently we issue up to one window token per display.
1272 if (tokenInfo.mDisplayId == displayId) {
1273 return tokenInfo.mToken;
1274 }
1275 }
1276
1277 final Binder token = new Binder();
1278 Binder.withCleanCallingIdentity(
1279 PooledLambda.obtainRunnable(WindowManagerInternal::addWindowToken,
1280 mIWindowManagerInternal, token, TYPE_INPUT_METHOD, displayId));
1281 mPerUserData.mDisplayIdToImeWindowTokenMap.add(new TokenInfo(token, displayId));
1282 return token;
1283 }
1284 }
1285
1286 @BinderThread
1287 @Override
1288 public void deleteInputMethodWindowToken(IBinder token) {
1289 synchronized (mPerUserData.mLock) {
1290 // We assume the number of tokens would not be that large (up to 10 or so) hence
1291 // linear search should be acceptable.
1292 final int numTokens = mPerUserData.mDisplayIdToImeWindowTokenMap.size();
1293 for (int i = 0; i < numTokens; ++i) {
1294 final TokenInfo tokenInfo =
1295 mPerUserData.mDisplayIdToImeWindowTokenMap.valueAt(i);
1296 if (tokenInfo.mToken == token) {
1297 mPerUserData.mDisplayIdToImeWindowTokenMap.remove(tokenInfo);
1298 break;
1299 }
1300 }
1301 }
1302 }
1303
1304 @BinderThread
1305 @Override
1306 public void acceptClient(int clientId, IInputMethodSession inputMethodSession,
1307 IMultiClientInputMethodSession multiSessionInputMethodSession,
1308 InputChannel writeChannel) {
1309 synchronized (mPerUserData.mLock) {
1310 final InputMethodClientInfo clientInfo =
1311 mPerUserData.getClientFromIdLocked(clientId);
1312 if (clientInfo == null) {
1313 Slog.e(TAG, "Unknown clientId=" + clientId);
1314 return;
1315 }
1316 switch (clientInfo.mState) {
1317 case InputMethodClientState.WAITING_FOR_IME_SESSION:
1318 try {
1319 clientInfo.mClient.setActive(true, false);
1320 } catch (RemoteException e) {
1321 // TODO(yukawa): Remove this client.
1322 return;
1323 }
1324 clientInfo.mState = InputMethodClientState.READY_TO_SEND_FIRST_BIND_RESULT;
1325 clientInfo.mWriteChannel = writeChannel;
1326 clientInfo.mInputMethodSession = inputMethodSession;
1327 clientInfo.mMSInputMethodSession = multiSessionInputMethodSession;
1328 break;
1329 default:
1330 Slog.e(TAG, "Unexpected state=" + clientInfo.mState);
1331 break;
1332 }
1333 }
1334 }
1335
1336 @BinderThread
1337 @Override
1338 public void reportImeWindowTarget(int clientId, int targetWindowHandle,
1339 IBinder imeWindowToken) {
1340 synchronized (mPerUserData.mLock) {
1341 final InputMethodClientInfo clientInfo =
1342 mPerUserData.getClientFromIdLocked(clientId);
1343 if (clientInfo == null) {
1344 Slog.e(TAG, "Unknown clientId=" + clientId);
1345 return;
1346 }
1347 for (WindowInfo windowInfo : clientInfo.mWindowMap.values()) {
1348 if (windowInfo.mWindowHandle == targetWindowHandle) {
1349 final IBinder targetWindowToken = windowInfo.mWindowToken;
1350 // TODO(yukawa): Report targetWindowToken and targetWindowToken to WMS.
1351 if (DEBUG) {
1352 Slog.v(TAG, "reportImeWindowTarget"
1353 + " clientId=" + clientId
1354 + " imeWindowToken=" + imeWindowToken
1355 + " targetWindowToken=" + targetWindowToken);
1356 }
1357 }
1358 }
1359 // not found.
1360 }
1361 }
1362
1363 @BinderThread
1364 @Override
1365 public boolean isUidAllowedOnDisplay(int displayId, int uid) {
1366 return mIWindowManagerInternal.isUidAllowedOnDisplay(displayId, uid);
1367 }
Tarandeep Singhe1921a72019-04-11 15:55:28 -07001368
1369 @BinderThread
1370 @Override
1371 public void setActive(int clientId, boolean active) {
1372 synchronized (mPerUserData.mLock) {
1373 final InputMethodClientInfo clientInfo =
1374 mPerUserData.getClientFromIdLocked(clientId);
1375 if (clientInfo == null) {
1376 Slog.e(TAG, "Unknown clientId=" + clientId);
1377 return;
1378 }
1379 try {
1380 clientInfo.mClient.setActive(active, false /* fullscreen */);
1381 } catch (RemoteException e) {
1382 return;
1383 }
1384 }
1385 }
Yohei Yukawabae5bea2018-11-12 15:08:30 -08001386 }
1387
1388 /**
1389 * Takes care of IPCs exposed to the IME client.
1390 */
1391 private static final class ApiCallbacks extends IInputMethodManager.Stub {
Yohei Yukawa1fb13c52019-02-05 07:55:28 -08001392 private final Context mContext;
Yohei Yukawabae5bea2018-11-12 15:08:30 -08001393 private final UserDataMap mUserDataMap;
1394 private final UserToInputMethodInfoMap mInputMethodInfoMap;
1395 private final AppOpsManager mAppOpsManager;
1396 private final WindowManagerInternal mWindowManagerInternal;
1397
1398 ApiCallbacks(Context context, UserDataMap userDataMap,
1399 UserToInputMethodInfoMap inputMethodInfoMap) {
Yohei Yukawa1fb13c52019-02-05 07:55:28 -08001400 mContext = context;
Yohei Yukawabae5bea2018-11-12 15:08:30 -08001401 mUserDataMap = userDataMap;
1402 mInputMethodInfoMap = inputMethodInfoMap;
1403 mAppOpsManager = context.getSystemService(AppOpsManager.class);
1404 mWindowManagerInternal = LocalServices.getService(WindowManagerInternal.class);
1405 }
1406
1407 @AnyThread
1408 private boolean checkFocus(int uid, int pid, int displayId) {
1409 return mWindowManagerInternal.isInputMethodClientFocus(uid, pid, displayId);
1410 }
1411
1412 @BinderThread
1413 @Override
1414 public void addClient(IInputMethodClient client, IInputContext inputContext,
1415 int selfReportedDisplayId) {
1416 final int callingUid = Binder.getCallingUid();
1417 final int callingPid = Binder.getCallingPid();
1418 final int userId = UserHandle.getUserId(callingUid);
1419 final PerUserData data = mUserDataMap.get(userId);
1420 if (data == null) {
1421 Slog.e(TAG, "addClient() from unknown userId=" + userId
1422 + " uid=" + callingUid + " pid=" + callingPid);
1423 return;
1424 }
1425 synchronized (data.mLock) {
1426 data.addClientLocked(callingUid, callingPid, client, selfReportedDisplayId);
1427 }
1428 }
1429
1430 @BinderThread
1431 @Override
Yohei Yukawad20eef82019-02-05 10:45:32 -08001432 public List<InputMethodInfo> getInputMethodList(@UserIdInt int userId) {
1433 if (UserHandle.getCallingUserId() != userId) {
1434 mContext.enforceCallingPermission(INTERACT_ACROSS_USERS_FULL, null);
1435 }
1436 return mInputMethodInfoMap.getAsList(userId);
Yohei Yukawabae5bea2018-11-12 15:08:30 -08001437 }
1438
1439 @BinderThread
1440 @Override
Yohei Yukawa1fb13c52019-02-05 07:55:28 -08001441 public List<InputMethodInfo> getEnabledInputMethodList(@UserIdInt int userId) {
1442 if (UserHandle.getCallingUserId() != userId) {
1443 mContext.enforceCallingPermission(INTERACT_ACROSS_USERS_FULL, null);
1444 }
1445 return mInputMethodInfoMap.getAsList(userId);
Yohei Yukawabae5bea2018-11-12 15:08:30 -08001446 }
1447
1448 @BinderThread
1449 @Override
1450 public List<InputMethodSubtype> getEnabledInputMethodSubtypeList(String imiId,
1451 boolean allowsImplicitlySelectedSubtypes) {
1452 reportNotSupported();
1453 return Collections.emptyList();
1454 }
1455
1456 @BinderThread
1457 @Override
1458 public InputMethodSubtype getLastInputMethodSubtype() {
1459 reportNotSupported();
1460 return null;
1461 }
1462
1463 @BinderThread
1464 @Override
Yohei Yukawabae5bea2018-11-12 15:08:30 -08001465 public boolean showSoftInput(
Tarandeep Singhbb0e2f72020-01-15 13:58:29 -08001466 IInputMethodClient client, IBinder token, int flags,
1467 ResultReceiver resultReceiver) {
Yohei Yukawabae5bea2018-11-12 15:08:30 -08001468 final int callingUid = Binder.getCallingUid();
1469 final int callingPid = Binder.getCallingPid();
1470 final int userId = UserHandle.getUserId(callingUid);
1471 final PerUserData data = mUserDataMap.get(userId);
1472 if (data == null) {
1473 Slog.e(TAG, "showSoftInput() from unknown userId=" + userId
1474 + " uid=" + callingUid + " pid=" + callingPid);
1475 return false;
1476 }
1477 synchronized (data.mLock) {
1478 final InputMethodClientInfo clientInfo = data.getClientLocked(client);
1479 if (clientInfo == null) {
1480 Slog.e(TAG, "showSoftInput. client not found. ignoring.");
1481 return false;
1482 }
1483 if (clientInfo.mUid != callingUid) {
1484 Slog.e(TAG, "Expected calling UID=" + clientInfo.mUid
1485 + " actual=" + callingUid);
1486 return false;
1487 }
1488 switch (clientInfo.mState) {
1489 case InputMethodClientState.READY_TO_SEND_FIRST_BIND_RESULT:
1490 case InputMethodClientState.ALREADY_SENT_BIND_RESULT:
1491 try {
1492 clientInfo.mMSInputMethodSession.showSoftInput(flags, resultReceiver);
1493 } catch (RemoteException e) {
1494 }
1495 break;
1496 default:
1497 if (DEBUG) {
1498 Slog.e(TAG, "Ignoring showSoftInput(). clientState="
1499 + clientInfo.mState);
1500 }
1501 break;
1502 }
1503 return true;
1504 }
1505 }
1506
1507 @BinderThread
1508 @Override
1509 public boolean hideSoftInput(
Tarandeep Singh4fe5b652020-02-20 17:20:19 -08001510 IInputMethodClient client, IBinder windowToken, int flags,
1511 ResultReceiver resultReceiver) {
Yohei Yukawabae5bea2018-11-12 15:08:30 -08001512 final int callingUid = Binder.getCallingUid();
1513 final int callingPid = Binder.getCallingPid();
1514 final int userId = UserHandle.getUserId(callingUid);
1515 final PerUserData data = mUserDataMap.get(userId);
1516 if (data == null) {
1517 Slog.e(TAG, "hideSoftInput() from unknown userId=" + userId
1518 + " uid=" + callingUid + " pid=" + callingPid);
1519 return false;
1520 }
1521 synchronized (data.mLock) {
1522 final InputMethodClientInfo clientInfo = data.getClientLocked(client);
1523 if (clientInfo == null) {
1524 return false;
1525 }
1526 if (clientInfo.mUid != callingUid) {
1527 Slog.e(TAG, "Expected calling UID=" + clientInfo.mUid
1528 + " actual=" + callingUid);
1529 return false;
1530 }
1531 switch (clientInfo.mState) {
1532 case InputMethodClientState.READY_TO_SEND_FIRST_BIND_RESULT:
1533 case InputMethodClientState.ALREADY_SENT_BIND_RESULT:
1534 try {
1535 clientInfo.mMSInputMethodSession.hideSoftInput(flags, resultReceiver);
1536 } catch (RemoteException e) {
1537 }
1538 break;
1539 default:
1540 if (DEBUG) {
1541 Slog.e(TAG, "Ignoring hideSoftInput(). clientState="
1542 + clientInfo.mState);
1543 }
1544 break;
1545 }
1546 return true;
1547 }
1548 }
1549
1550 @BinderThread
1551 @Override
1552 public InputBindResult startInputOrWindowGainedFocus(
1553 @StartInputReason int startInputReason,
1554 @Nullable IInputMethodClient client,
1555 @Nullable IBinder windowToken,
1556 @StartInputFlags int startInputFlags,
1557 @SoftInputModeFlags int softInputMode,
1558 int windowFlags,
1559 @Nullable EditorInfo editorInfo,
1560 @Nullable IInputContext inputContext,
1561 @MissingMethodFlags int missingMethods,
1562 int unverifiedTargetSdkVersion) {
1563 final int callingUid = Binder.getCallingUid();
1564 final int callingPid = Binder.getCallingPid();
1565 final int userId = UserHandle.getUserId(callingUid);
1566
1567 if (client == null) {
1568 return InputBindResult.INVALID_CLIENT;
1569 }
1570
1571 final boolean packageNameVerified =
1572 editorInfo != null && InputMethodUtils.checkIfPackageBelongsToUid(
1573 mAppOpsManager, callingUid, editorInfo.packageName);
1574 if (editorInfo != null && !packageNameVerified) {
1575 Slog.e(TAG, "Rejecting this client as it reported an invalid package name."
1576 + " uid=" + callingUid + " package=" + editorInfo.packageName);
1577 return InputBindResult.INVALID_PACKAGE_NAME;
1578 }
1579
1580 final PerUserData data = mUserDataMap.get(userId);
1581 if (data == null) {
1582 Slog.e(TAG, "startInputOrWindowGainedFocus() from unknown userId=" + userId
1583 + " uid=" + callingUid + " pid=" + callingPid);
1584 return InputBindResult.INVALID_USER;
1585 }
1586
1587 synchronized (data.mLock) {
1588 final InputMethodClientInfo clientInfo = data.getClientLocked(client);
1589 if (clientInfo == null) {
1590 return InputBindResult.INVALID_CLIENT;
1591 }
1592 if (clientInfo.mUid != callingUid) {
1593 Slog.e(TAG, "Expected calling UID=" + clientInfo.mUid
1594 + " actual=" + callingUid);
1595 return InputBindResult.INVALID_CLIENT;
1596 }
1597
1598 switch (data.mState) {
1599 case PerUserState.USER_LOCKED:
1600 case PerUserState.SERVICE_NOT_QUERIED:
1601 case PerUserState.SERVICE_RECOGNIZED:
1602 case PerUserState.WAITING_SERVICE_CONNECTED:
1603 case PerUserState.UNBIND_CALLED:
1604 return InputBindResult.IME_NOT_CONNECTED;
1605 case PerUserState.SERVICE_CONNECTED:
1606 // OK
1607 break;
1608 default:
1609 Slog.wtf(TAG, "Unexpected state=" + data.mState);
1610 return InputBindResult.IME_NOT_CONNECTED;
1611 }
1612
1613 WindowInfo windowInfo = null;
1614 if (windowToken != null) {
1615 windowInfo = clientInfo.mWindowMap.get(windowToken);
1616 if (windowInfo == null) {
1617 windowInfo = new WindowInfo(windowToken, WindowHandleSource.getNext());
1618 clientInfo.mWindowMap.put(windowToken, windowInfo);
1619 }
1620 }
1621
1622 if (!checkFocus(clientInfo.mUid, clientInfo.mPid,
1623 clientInfo.mSelfReportedDisplayId)) {
1624 return InputBindResult.NOT_IME_TARGET_WINDOW;
1625 }
1626
1627 if (editorInfo == null) {
1628 // So-called dummy InputConnection scenario. For app compatibility, we still
1629 // notify this to the IME.
1630 switch (clientInfo.mState) {
1631 case InputMethodClientState.READY_TO_SEND_FIRST_BIND_RESULT:
1632 case InputMethodClientState.ALREADY_SENT_BIND_RESULT:
1633 final int windowHandle = windowInfo != null
1634 ? windowInfo.mWindowHandle
1635 : MultiClientInputMethodServiceDelegate.INVALID_WINDOW_HANDLE;
1636 try {
1637 clientInfo.mMSInputMethodSession.startInputOrWindowGainedFocus(
1638 inputContext, missingMethods, editorInfo, startInputFlags,
1639 softInputMode, windowHandle);
1640 } catch (RemoteException e) {
1641 }
1642 break;
1643 }
1644 return InputBindResult.NULL_EDITOR_INFO;
1645 }
1646
1647 switch (clientInfo.mState) {
1648 case InputMethodClientState.REGISTERED:
1649 case InputMethodClientState.WAITING_FOR_IME_SESSION:
1650 clientInfo.mBindingSequence++;
1651 if (clientInfo.mBindingSequence < 0) {
1652 clientInfo.mBindingSequence = 0;
1653 }
1654 return new InputBindResult(
1655 InputBindResult.ResultCode.SUCCESS_WAITING_IME_SESSION,
1656 null, null, data.mCurrentInputMethodInfo.getId(),
Yohei Yukawab4f328a2019-05-02 08:41:27 -07001657 clientInfo.mBindingSequence, null);
Yohei Yukawabae5bea2018-11-12 15:08:30 -08001658 case InputMethodClientState.READY_TO_SEND_FIRST_BIND_RESULT:
1659 case InputMethodClientState.ALREADY_SENT_BIND_RESULT:
1660 clientInfo.mBindingSequence++;
1661 if (clientInfo.mBindingSequence < 0) {
1662 clientInfo.mBindingSequence = 0;
1663 }
1664 // Successful start input.
1665 final int windowHandle = windowInfo != null
1666 ? windowInfo.mWindowHandle
1667 : MultiClientInputMethodServiceDelegate.INVALID_WINDOW_HANDLE;
1668 try {
1669 clientInfo.mMSInputMethodSession.startInputOrWindowGainedFocus(
1670 inputContext, missingMethods, editorInfo, startInputFlags,
1671 softInputMode, windowHandle);
1672 } catch (RemoteException e) {
1673 }
1674 clientInfo.mState = InputMethodClientState.ALREADY_SENT_BIND_RESULT;
1675 return new InputBindResult(
1676 InputBindResult.ResultCode.SUCCESS_WITH_IME_SESSION,
1677 clientInfo.mInputMethodSession,
1678 clientInfo.mWriteChannel.dup(),
1679 data.mCurrentInputMethodInfo.getId(),
Yohei Yukawab4f328a2019-05-02 08:41:27 -07001680 clientInfo.mBindingSequence, null);
Yohei Yukawabae5bea2018-11-12 15:08:30 -08001681 case InputMethodClientState.UNREGISTERED:
1682 Slog.e(TAG, "The client is already unregistered.");
1683 return InputBindResult.INVALID_CLIENT;
1684 }
1685 }
1686 return null;
1687 }
1688
1689 @BinderThread
1690 @Override
1691 public void showInputMethodPickerFromClient(
1692 IInputMethodClient client, int auxiliarySubtypeMode) {
1693 reportNotSupported();
1694 }
1695
1696 @BinderThread
1697 @Override
lumark0b05f9e2018-11-26 15:09:06 +08001698 public void showInputMethodPickerFromSystem(
1699 IInputMethodClient client, int auxiliarySubtypeMode, int displayId) {
1700 reportNotSupported();
1701 }
1702
1703 @BinderThread
1704 @Override
Yohei Yukawabae5bea2018-11-12 15:08:30 -08001705 public void showInputMethodAndSubtypeEnablerFromClient(
1706 IInputMethodClient client, String inputMethodId) {
1707 reportNotSupported();
1708 }
1709
1710 @BinderThread
1711 @Override
1712 public boolean isInputMethodPickerShownForTest() {
1713 reportNotSupported();
1714 return false;
1715 }
1716
1717 @BinderThread
1718 @Override
Yohei Yukawabae5bea2018-11-12 15:08:30 -08001719 public InputMethodSubtype getCurrentInputMethodSubtype() {
1720 reportNotSupported();
1721 return null;
1722 }
1723
1724 @BinderThread
1725 @Override
Yohei Yukawabae5bea2018-11-12 15:08:30 -08001726 public void setAdditionalInputMethodSubtypes(String imiId, InputMethodSubtype[] subtypes) {
1727 reportNotSupported();
1728 }
1729
1730 @BinderThread
1731 @Override
1732 public int getInputMethodWindowVisibleHeight() {
1733 reportNotSupported();
1734 return 0;
1735 }
1736
1737 @BinderThread
1738 @Override
Yohei Yukawab4f328a2019-05-02 08:41:27 -07001739 public void reportActivityView(IInputMethodClient parentClient, int childDisplayId,
1740 float[] matrixValues) {
1741 reportNotSupported();
1742 }
1743
1744 @BinderThread
1745 @Override
Yohei Yukawabae5bea2018-11-12 15:08:30 -08001746 public void onShellCommand(@Nullable FileDescriptor in, @Nullable FileDescriptor out,
1747 @Nullable FileDescriptor err, String[] args, @Nullable ShellCallback callback,
1748 ResultReceiver resultReceiver) {
1749 }
Keun young Park9b673eb2019-04-29 17:54:11 -07001750
1751 @BinderThread
1752 @Override
1753 protected void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
1754 if (!DumpUtils.checkDumpPermission(mContext, TAG, pw)) return;
1755 final String prefixChild = " ";
1756 pw.println("Current Multi Client Input Method Manager state:");
1757 IndentingPrintWriter ipw = new IndentingPrintWriter(pw, " ");
1758 ipw.println("mUserDataMap=");
1759 if (mUserDataMap != null) {
1760 ipw.increaseIndent();
1761 mUserDataMap.dump(fd, ipw, args);
1762 }
1763 }
Yohei Yukawabae5bea2018-11-12 15:08:30 -08001764 }
1765}