1/* 2 * Copyright (C) 2011 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; 18 19import com.android.internal.annotations.GuardedBy; 20import com.android.internal.content.PackageMonitor; 21import com.android.internal.inputmethod.InputMethodUtils; 22import com.android.internal.textservice.ISpellCheckerService; 23import com.android.internal.textservice.ISpellCheckerServiceCallback; 24import com.android.internal.textservice.ISpellCheckerSession; 25import com.android.internal.textservice.ISpellCheckerSessionListener; 26import com.android.internal.textservice.ITextServicesManager; 27import com.android.internal.textservice.ITextServicesSessionListener; 28import com.android.internal.util.DumpUtils; 29 30import org.xmlpull.v1.XmlPullParserException; 31 32import android.annotation.NonNull; 33import android.annotation.Nullable; 34import android.annotation.UserIdInt; 35import android.app.ActivityManager; 36import android.app.AppGlobals; 37import android.content.BroadcastReceiver; 38import android.content.ComponentName; 39import android.content.ContentResolver; 40import android.content.Context; 41import android.content.Intent; 42import android.content.IntentFilter; 43import android.content.ServiceConnection; 44import android.content.pm.ApplicationInfo; 45import android.content.pm.PackageManager; 46import android.content.pm.ResolveInfo; 47import android.content.pm.ServiceInfo; 48import android.os.Binder; 49import android.os.Bundle; 50import android.os.IBinder; 51import android.os.Process; 52import android.os.RemoteCallbackList; 53import android.os.RemoteException; 54import android.os.UserHandle; 55import android.os.UserManager; 56import android.provider.Settings; 57import android.service.textservice.SpellCheckerService; 58import android.text.TextUtils; 59import android.util.Slog; 60import android.view.inputmethod.InputMethodManager; 61import android.view.inputmethod.InputMethodSubtype; 62import android.view.textservice.SpellCheckerInfo; 63import android.view.textservice.SpellCheckerSubtype; 64 65import java.io.FileDescriptor; 66import java.io.IOException; 67import java.io.PrintWriter; 68import java.util.Arrays; 69import java.util.ArrayList; 70import java.util.HashMap; 71import java.util.List; 72import java.util.Locale; 73import java.util.Map; 74 75public class TextServicesManagerService extends ITextServicesManager.Stub { 76 private static final String TAG = TextServicesManagerService.class.getSimpleName(); 77 private static final boolean DBG = false; 78 79 private final Context mContext; 80 private boolean mSystemReady; 81 private final TextServicesMonitor mMonitor; 82 private final HashMap<String, SpellCheckerInfo> mSpellCheckerMap = new HashMap<>(); 83 private final ArrayList<SpellCheckerInfo> mSpellCheckerList = new ArrayList<>(); 84 private final HashMap<String, SpellCheckerBindGroup> mSpellCheckerBindGroups = new HashMap<>(); 85 private final TextServicesSettings mSettings; 86 @NonNull 87 private final UserManager mUserManager; 88 private final Object mLock = new Object(); 89 90 public static final class Lifecycle extends SystemService { 91 private TextServicesManagerService mService; 92 93 public Lifecycle(Context context) { 94 super(context); 95 mService = new TextServicesManagerService(context); 96 } 97 98 @Override 99 public void onStart() { 100 publishBinderService(Context.TEXT_SERVICES_MANAGER_SERVICE, mService); 101 } 102 103 @Override 104 public void onSwitchUser(@UserIdInt int userHandle) { 105 // Called on the system server's main looper thread. 106 // TODO: Dispatch this to a worker thread as needed. 107 mService.onSwitchUser(userHandle); 108 } 109 110 @Override 111 public void onBootPhase(int phase) { 112 // Called on the system server's main looper thread. 113 // TODO: Dispatch this to a worker thread as needed. 114 if (phase == SystemService.PHASE_ACTIVITY_MANAGER_READY) { 115 mService.systemRunning(); 116 } 117 } 118 119 @Override 120 public void onUnlockUser(@UserIdInt int userHandle) { 121 // Called on the system server's main looper thread. 122 // TODO: Dispatch this to a worker thread as needed. 123 mService.onUnlockUser(userHandle); 124 } 125 } 126 127 void systemRunning() { 128 synchronized (mLock) { 129 if (!mSystemReady) { 130 mSystemReady = true; 131 resetInternalState(mSettings.getCurrentUserId()); 132 } 133 } 134 } 135 136 void onSwitchUser(@UserIdInt int userId) { 137 synchronized (mLock) { 138 resetInternalState(userId); 139 } 140 } 141 142 void onUnlockUser(@UserIdInt int userId) { 143 synchronized (mLock) { 144 final int currentUserId = mSettings.getCurrentUserId(); 145 if (userId != currentUserId) { 146 return; 147 } 148 resetInternalState(currentUserId); 149 } 150 } 151 152 public TextServicesManagerService(Context context) { 153 mSystemReady = false; 154 mContext = context; 155 156 mUserManager = mContext.getSystemService(UserManager.class); 157 158 final IntentFilter broadcastFilter = new IntentFilter(); 159 broadcastFilter.addAction(Intent.ACTION_USER_ADDED); 160 broadcastFilter.addAction(Intent.ACTION_USER_REMOVED); 161 mContext.registerReceiver(new TextServicesBroadcastReceiver(), broadcastFilter); 162 163 int userId = UserHandle.USER_SYSTEM; 164 try { 165 userId = ActivityManager.getService().getCurrentUser().id; 166 } catch (RemoteException e) { 167 Slog.w(TAG, "Couldn't get current user ID; guessing it's 0", e); 168 } 169 mMonitor = new TextServicesMonitor(); 170 mMonitor.register(context, null, true); 171 final boolean useCopyOnWriteSettings = 172 !mSystemReady || !mUserManager.isUserUnlockingOrUnlocked(userId); 173 mSettings = new TextServicesSettings(context.getContentResolver(), userId, 174 useCopyOnWriteSettings); 175 176 // "resetInternalState" initializes the states for the foreground user 177 resetInternalState(userId); 178 } 179 180 private void resetInternalState(@UserIdInt int userId) { 181 final boolean useCopyOnWriteSettings = 182 !mSystemReady || !mUserManager.isUserUnlockingOrUnlocked(userId); 183 mSettings.switchCurrentUser(userId, useCopyOnWriteSettings); 184 updateCurrentProfileIds(); 185 unbindServiceLocked(); 186 buildSpellCheckerMapLocked(mContext, mSpellCheckerList, mSpellCheckerMap, mSettings); 187 SpellCheckerInfo sci = getCurrentSpellChecker(null); 188 if (sci == null) { 189 sci = findAvailSystemSpellCheckerLocked(null); 190 // Set the current spell checker if there is one or more system spell checkers 191 // available. In this case, "sci" is the first one in the available spell 192 // checkers. 193 setCurrentSpellCheckerLocked(sci); 194 } 195 } 196 197 void updateCurrentProfileIds() { 198 mSettings.setCurrentProfileIds( 199 mUserManager.getProfileIdsWithDisabled(mSettings.getCurrentUserId())); 200 } 201 202 private final class TextServicesMonitor extends PackageMonitor { 203 private boolean isChangingPackagesOfCurrentUser() { 204 final int userId = getChangingUserId(); 205 final boolean retval = userId == mSettings.getCurrentUserId(); 206 if (DBG) { 207 Slog.d(TAG, "--- ignore this call back from a background user: " + userId); 208 } 209 return retval; 210 } 211 212 @Override 213 public void onSomePackagesChanged() { 214 if (!isChangingPackagesOfCurrentUser()) { 215 return; 216 } 217 synchronized (mLock) { 218 // TODO: Update for each locale 219 SpellCheckerInfo sci = getCurrentSpellChecker(null); 220 buildSpellCheckerMapLocked( 221 mContext, mSpellCheckerList, mSpellCheckerMap, mSettings); 222 // If spell checker is disabled, just return. The user should explicitly 223 // enable the spell checker. 224 if (!isSpellCheckerEnabledLocked()) return; 225 226 if (sci == null) { 227 sci = findAvailSystemSpellCheckerLocked(null); 228 // Set the current spell checker if there is one or more system spell checkers 229 // available. In this case, "sci" is the first one in the available spell 230 // checkers. 231 setCurrentSpellCheckerLocked(sci); 232 } else { 233 final String packageName = sci.getPackageName(); 234 final int change = isPackageDisappearing(packageName); 235 if (// Package disappearing 236 change == PACKAGE_PERMANENT_CHANGE || change == PACKAGE_TEMPORARY_CHANGE 237 // Package modified 238 || isPackageModified(packageName)) { 239 SpellCheckerInfo availSci = findAvailSystemSpellCheckerLocked(packageName); 240 // Set the spell checker settings if different than before 241 if (availSci != null && !availSci.getId().equals(sci.getId())) { 242 setCurrentSpellCheckerLocked(availSci); 243 } 244 } 245 } 246 } 247 } 248 } 249 250 private final class TextServicesBroadcastReceiver extends BroadcastReceiver { 251 @Override 252 public void onReceive(Context context, Intent intent) { 253 final String action = intent.getAction(); 254 if (Intent.ACTION_USER_ADDED.equals(action) 255 || Intent.ACTION_USER_REMOVED.equals(action)) { 256 updateCurrentProfileIds(); 257 return; 258 } 259 Slog.w(TAG, "Unexpected intent " + intent); 260 } 261 } 262 263 private static void buildSpellCheckerMapLocked(Context context, 264 ArrayList<SpellCheckerInfo> list, HashMap<String, SpellCheckerInfo> map, 265 TextServicesSettings settings) { 266 list.clear(); 267 map.clear(); 268 final PackageManager pm = context.getPackageManager(); 269 // Note: We do not specify PackageManager.MATCH_ENCRYPTION_* flags here because the default 270 // behavior of PackageManager is exactly what we want. It by default picks up appropriate 271 // services depending on the unlock state for the specified user. 272 final List<ResolveInfo> services = pm.queryIntentServicesAsUser( 273 new Intent(SpellCheckerService.SERVICE_INTERFACE), PackageManager.GET_META_DATA, 274 settings.getCurrentUserId()); 275 final int N = services.size(); 276 for (int i = 0; i < N; ++i) { 277 final ResolveInfo ri = services.get(i); 278 final ServiceInfo si = ri.serviceInfo; 279 final ComponentName compName = new ComponentName(si.packageName, si.name); 280 if (!android.Manifest.permission.BIND_TEXT_SERVICE.equals(si.permission)) { 281 Slog.w(TAG, "Skipping text service " + compName 282 + ": it does not require the permission " 283 + android.Manifest.permission.BIND_TEXT_SERVICE); 284 continue; 285 } 286 if (DBG) Slog.d(TAG, "Add: " + compName); 287 try { 288 final SpellCheckerInfo sci = new SpellCheckerInfo(context, ri); 289 if (sci.getSubtypeCount() <= 0) { 290 Slog.w(TAG, "Skipping text service " + compName 291 + ": it does not contain subtypes."); 292 continue; 293 } 294 list.add(sci); 295 map.put(sci.getId(), sci); 296 } catch (XmlPullParserException e) { 297 Slog.w(TAG, "Unable to load the spell checker " + compName, e); 298 } catch (IOException e) { 299 Slog.w(TAG, "Unable to load the spell checker " + compName, e); 300 } 301 } 302 if (DBG) { 303 Slog.d(TAG, "buildSpellCheckerMapLocked: " + list.size() + "," + map.size()); 304 } 305 } 306 307 // --------------------------------------------------------------------------------------- 308 // Check whether or not this is a valid IPC. Assumes an IPC is valid when either 309 // 1) it comes from the system process 310 // 2) the calling process' user id is identical to the current user id TSMS thinks. 311 private boolean calledFromValidUser() { 312 final int uid = Binder.getCallingUid(); 313 final int userId = UserHandle.getUserId(uid); 314 if (DBG) { 315 Slog.d(TAG, "--- calledFromForegroundUserOrSystemProcess ? " 316 + "calling uid = " + uid + " system uid = " + Process.SYSTEM_UID 317 + " calling userId = " + userId + ", foreground user id = " 318 + mSettings.getCurrentUserId() + ", calling pid = " + Binder.getCallingPid()); 319 try { 320 final String[] packageNames = AppGlobals.getPackageManager().getPackagesForUid(uid); 321 for (int i = 0; i < packageNames.length; ++i) { 322 if (DBG) { 323 Slog.d(TAG, "--- process name for "+ uid + " = " + packageNames[i]); 324 } 325 } 326 } catch (RemoteException e) { 327 } 328 } 329 330 if (uid == Process.SYSTEM_UID || userId == mSettings.getCurrentUserId()) { 331 return true; 332 } 333 334 // Permits current profile to use TSFM as long as the current text service is the system's 335 // one. This is a tentative solution and should be replaced with fully functional multiuser 336 // support. 337 // TODO: Implement multiuser support in TSMS. 338 final boolean isCurrentProfile = mSettings.isCurrentProfile(userId); 339 if (DBG) { 340 Slog.d(TAG, "--- userId = "+ userId + " isCurrentProfile = " + isCurrentProfile); 341 } 342 if (mSettings.isCurrentProfile(userId)) { 343 final SpellCheckerInfo spellCheckerInfo = getCurrentSpellCheckerWithoutVerification(); 344 if (spellCheckerInfo != null) { 345 final ServiceInfo serviceInfo = spellCheckerInfo.getServiceInfo(); 346 final boolean isSystemSpellChecker = 347 (serviceInfo.applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0; 348 if (DBG) { 349 Slog.d(TAG, "--- current spell checker = "+ spellCheckerInfo.getPackageName() 350 + " isSystem = " + isSystemSpellChecker); 351 } 352 if (isSystemSpellChecker) { 353 return true; 354 } 355 } 356 } 357 358 // Unlike InputMethodManagerService#calledFromValidUser, INTERACT_ACROSS_USERS_FULL isn't 359 // taken into account here. Anyway this method is supposed to be removed once multiuser 360 // support is implemented. 361 if (DBG) { 362 Slog.d(TAG, "--- IPC from userId:" + userId + " is being ignored. \n" 363 + getStackTrace()); 364 } 365 return false; 366 } 367 368 private boolean bindCurrentSpellCheckerService( 369 Intent service, ServiceConnection conn, int flags) { 370 if (service == null || conn == null) { 371 Slog.e(TAG, "--- bind failed: service = " + service + ", conn = " + conn); 372 return false; 373 } 374 return mContext.bindServiceAsUser(service, conn, flags, 375 new UserHandle(mSettings.getCurrentUserId())); 376 } 377 378 private void unbindServiceLocked() { 379 for (SpellCheckerBindGroup scbg : mSpellCheckerBindGroups.values()) { 380 scbg.removeAllLocked(); 381 } 382 mSpellCheckerBindGroups.clear(); 383 } 384 385 private SpellCheckerInfo findAvailSystemSpellCheckerLocked(String prefPackage) { 386 // Filter the spell checker list to remove spell checker services that are not pre-installed 387 ArrayList<SpellCheckerInfo> spellCheckerList = new ArrayList<>(); 388 for (SpellCheckerInfo sci : mSpellCheckerList) { 389 if ((sci.getServiceInfo().applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0) { 390 spellCheckerList.add(sci); 391 } 392 } 393 394 final int spellCheckersCount = spellCheckerList.size(); 395 if (spellCheckersCount == 0) { 396 Slog.w(TAG, "no available spell checker services found"); 397 return null; 398 } 399 if (prefPackage != null) { 400 for (int i = 0; i < spellCheckersCount; ++i) { 401 final SpellCheckerInfo sci = spellCheckerList.get(i); 402 if (prefPackage.equals(sci.getPackageName())) { 403 if (DBG) { 404 Slog.d(TAG, "findAvailSystemSpellCheckerLocked: " + sci.getPackageName()); 405 } 406 return sci; 407 } 408 } 409 } 410 411 // Look up a spell checker based on the system locale. 412 // TODO: Still there is a room to improve in the following logic: e.g., check if the package 413 // is pre-installed or not. 414 final Locale systemLocal = mContext.getResources().getConfiguration().locale; 415 final ArrayList<Locale> suitableLocales = 416 InputMethodUtils.getSuitableLocalesForSpellChecker(systemLocal); 417 if (DBG) { 418 Slog.w(TAG, "findAvailSystemSpellCheckerLocked suitableLocales=" 419 + Arrays.toString(suitableLocales.toArray(new Locale[suitableLocales.size()]))); 420 } 421 final int localeCount = suitableLocales.size(); 422 for (int localeIndex = 0; localeIndex < localeCount; ++localeIndex) { 423 final Locale locale = suitableLocales.get(localeIndex); 424 for (int spellCheckersIndex = 0; spellCheckersIndex < spellCheckersCount; 425 ++spellCheckersIndex) { 426 final SpellCheckerInfo info = spellCheckerList.get(spellCheckersIndex); 427 final int subtypeCount = info.getSubtypeCount(); 428 for (int subtypeIndex = 0; subtypeIndex < subtypeCount; ++subtypeIndex) { 429 final SpellCheckerSubtype subtype = info.getSubtypeAt(subtypeIndex); 430 final Locale subtypeLocale = InputMethodUtils.constructLocaleFromString( 431 subtype.getLocale()); 432 if (locale.equals(subtypeLocale)) { 433 // TODO: We may have more spell checkers that fall into this category. 434 // Ideally we should pick up the most suitable one instead of simply 435 // returning the first found one. 436 return info; 437 } 438 } 439 } 440 } 441 442 if (spellCheckersCount > 1) { 443 Slog.w(TAG, "more than one spell checker service found, picking first"); 444 } 445 return spellCheckerList.get(0); 446 } 447 448 // TODO: Save SpellCheckerService by supported languages. Currently only one spell 449 // checker is saved. 450 @Override 451 public SpellCheckerInfo getCurrentSpellChecker(String locale) { 452 // TODO: Make this work even for non-current users? 453 if (!calledFromValidUser()) { 454 return null; 455 } 456 return getCurrentSpellCheckerWithoutVerification(); 457 } 458 459 private SpellCheckerInfo getCurrentSpellCheckerWithoutVerification() { 460 synchronized (mLock) { 461 final String curSpellCheckerId = mSettings.getSelectedSpellChecker(); 462 if (DBG) { 463 Slog.w(TAG, "getCurrentSpellChecker: " + curSpellCheckerId); 464 } 465 if (TextUtils.isEmpty(curSpellCheckerId)) { 466 return null; 467 } 468 return mSpellCheckerMap.get(curSpellCheckerId); 469 } 470 } 471 472 // TODO: Respect allowImplicitlySelectedSubtype 473 // TODO: Save SpellCheckerSubtype by supported languages by looking at "locale". 474 @Override 475 public SpellCheckerSubtype getCurrentSpellCheckerSubtype( 476 String locale, boolean allowImplicitlySelectedSubtype) { 477 // TODO: Make this work even for non-current users? 478 if (!calledFromValidUser()) { 479 return null; 480 } 481 final int subtypeHashCode; 482 final SpellCheckerInfo sci; 483 final Locale systemLocale; 484 synchronized (mLock) { 485 subtypeHashCode = 486 mSettings.getSelectedSpellCheckerSubtype(SpellCheckerSubtype.SUBTYPE_ID_NONE); 487 if (DBG) { 488 Slog.w(TAG, "getCurrentSpellCheckerSubtype: " + subtypeHashCode); 489 } 490 sci = getCurrentSpellChecker(null); 491 systemLocale = mContext.getResources().getConfiguration().locale; 492 } 493 if (sci == null || sci.getSubtypeCount() == 0) { 494 if (DBG) { 495 Slog.w(TAG, "Subtype not found."); 496 } 497 return null; 498 } 499 if (subtypeHashCode == SpellCheckerSubtype.SUBTYPE_ID_NONE 500 && !allowImplicitlySelectedSubtype) { 501 return null; 502 } 503 String candidateLocale = null; 504 if (subtypeHashCode == 0) { 505 // Spell checker language settings == "auto" 506 final InputMethodManager imm = mContext.getSystemService(InputMethodManager.class); 507 if (imm != null) { 508 final InputMethodSubtype currentInputMethodSubtype = 509 imm.getCurrentInputMethodSubtype(); 510 if (currentInputMethodSubtype != null) { 511 final String localeString = currentInputMethodSubtype.getLocale(); 512 if (!TextUtils.isEmpty(localeString)) { 513 // 1. Use keyboard locale if available in the spell checker 514 candidateLocale = localeString; 515 } 516 } 517 } 518 if (candidateLocale == null) { 519 // 2. Use System locale if available in the spell checker 520 candidateLocale = systemLocale.toString(); 521 } 522 } 523 SpellCheckerSubtype candidate = null; 524 for (int i = 0; i < sci.getSubtypeCount(); ++i) { 525 final SpellCheckerSubtype scs = sci.getSubtypeAt(i); 526 if (subtypeHashCode == 0) { 527 final String scsLocale = scs.getLocale(); 528 if (candidateLocale.equals(scsLocale)) { 529 return scs; 530 } else if (candidate == null) { 531 if (candidateLocale.length() >= 2 && scsLocale.length() >= 2 532 && candidateLocale.startsWith(scsLocale)) { 533 // Fall back to the applicable language 534 candidate = scs; 535 } 536 } 537 } else if (scs.hashCode() == subtypeHashCode) { 538 if (DBG) { 539 Slog.w(TAG, "Return subtype " + scs.hashCode() + ", input= " + locale 540 + ", " + scs.getLocale()); 541 } 542 // 3. Use the user specified spell check language 543 return scs; 544 } 545 } 546 // 4. Fall back to the applicable language and return it if not null 547 // 5. Simply just return it even if it's null which means we could find no suitable 548 // spell check languages 549 return candidate; 550 } 551 552 @Override 553 public void getSpellCheckerService(String sciId, String locale, 554 ITextServicesSessionListener tsListener, ISpellCheckerSessionListener scListener, 555 Bundle bundle) { 556 if (!calledFromValidUser()) { 557 return; 558 } 559 if (!mSystemReady) { 560 return; 561 } 562 if (TextUtils.isEmpty(sciId) || tsListener == null || scListener == null) { 563 Slog.e(TAG, "getSpellCheckerService: Invalid input."); 564 return; 565 } 566 synchronized (mLock) { 567 if (!mSpellCheckerMap.containsKey(sciId)) { 568 return; 569 } 570 final SpellCheckerInfo sci = mSpellCheckerMap.get(sciId); 571 SpellCheckerBindGroup bindGroup = mSpellCheckerBindGroups.get(sciId); 572 final int uid = Binder.getCallingUid(); 573 if (bindGroup == null) { 574 final long ident = Binder.clearCallingIdentity(); 575 try { 576 bindGroup = startSpellCheckerServiceInnerLocked(sci); 577 } finally { 578 Binder.restoreCallingIdentity(ident); 579 } 580 if (bindGroup == null) { 581 // startSpellCheckerServiceInnerLocked failed. 582 return; 583 } 584 } 585 586 // Start getISpellCheckerSession async IPC, or just queue the request until the spell 587 // checker service is bound. 588 bindGroup.getISpellCheckerSessionOrQueueLocked( 589 new SessionRequest(uid, locale, tsListener, scListener, bundle)); 590 } 591 } 592 593 @Override 594 public boolean isSpellCheckerEnabled() { 595 if (!calledFromValidUser()) { 596 return false; 597 } 598 synchronized (mLock) { 599 return isSpellCheckerEnabledLocked(); 600 } 601 } 602 603 @Nullable 604 private SpellCheckerBindGroup startSpellCheckerServiceInnerLocked(SpellCheckerInfo info) { 605 if (DBG) { 606 Slog.w(TAG, "Start spell checker session inner locked."); 607 } 608 final String sciId = info.getId(); 609 final InternalServiceConnection connection = new InternalServiceConnection(sciId); 610 final Intent serviceIntent = new Intent(SpellCheckerService.SERVICE_INTERFACE); 611 serviceIntent.setComponent(info.getComponent()); 612 if (DBG) { 613 Slog.w(TAG, "bind service: " + info.getId()); 614 } 615 if (!bindCurrentSpellCheckerService(serviceIntent, connection, 616 Context.BIND_AUTO_CREATE | Context.BIND_IMPORTANT_BACKGROUND)) { 617 Slog.e(TAG, "Failed to get a spell checker service."); 618 return null; 619 } 620 final SpellCheckerBindGroup group = new SpellCheckerBindGroup(connection); 621 mSpellCheckerBindGroups.put(sciId, group); 622 return group; 623 } 624 625 @Override 626 public SpellCheckerInfo[] getEnabledSpellCheckers() { 627 // TODO: Make this work even for non-current users? 628 if (!calledFromValidUser()) { 629 return null; 630 } 631 if (DBG) { 632 Slog.d(TAG, "getEnabledSpellCheckers: " + mSpellCheckerList.size()); 633 for (int i = 0; i < mSpellCheckerList.size(); ++i) { 634 Slog.d(TAG, "EnabledSpellCheckers: " + mSpellCheckerList.get(i).getPackageName()); 635 } 636 } 637 return mSpellCheckerList.toArray(new SpellCheckerInfo[mSpellCheckerList.size()]); 638 } 639 640 @Override 641 public void finishSpellCheckerService(ISpellCheckerSessionListener listener) { 642 if (!calledFromValidUser()) { 643 return; 644 } 645 if (DBG) { 646 Slog.d(TAG, "FinishSpellCheckerService"); 647 } 648 synchronized (mLock) { 649 final ArrayList<SpellCheckerBindGroup> removeList = new ArrayList<>(); 650 for (SpellCheckerBindGroup group : mSpellCheckerBindGroups.values()) { 651 if (group == null) continue; 652 // Use removeList to avoid modifying mSpellCheckerBindGroups in this loop. 653 removeList.add(group); 654 } 655 final int removeSize = removeList.size(); 656 for (int i = 0; i < removeSize; ++i) { 657 removeList.get(i).removeListener(listener); 658 } 659 } 660 } 661 662 private void setCurrentSpellCheckerLocked(@Nullable SpellCheckerInfo sci) { 663 final String sciId = (sci != null) ? sci.getId() : ""; 664 if (DBG) { 665 Slog.w(TAG, "setCurrentSpellChecker: " + sciId); 666 } 667 final long ident = Binder.clearCallingIdentity(); 668 try { 669 mSettings.putSelectedSpellChecker(sciId); 670 } finally { 671 Binder.restoreCallingIdentity(ident); 672 } 673 } 674 675 private void setCurrentSpellCheckerSubtypeLocked(int hashCode) { 676 if (DBG) { 677 Slog.w(TAG, "setCurrentSpellCheckerSubtype: " + hashCode); 678 } 679 final SpellCheckerInfo sci = getCurrentSpellChecker(null); 680 int tempHashCode = 0; 681 for (int i = 0; sci != null && i < sci.getSubtypeCount(); ++i) { 682 if(sci.getSubtypeAt(i).hashCode() == hashCode) { 683 tempHashCode = hashCode; 684 break; 685 } 686 } 687 final long ident = Binder.clearCallingIdentity(); 688 try { 689 mSettings.putSelectedSpellCheckerSubtype(tempHashCode); 690 } finally { 691 Binder.restoreCallingIdentity(ident); 692 } 693 } 694 695 private boolean isSpellCheckerEnabledLocked() { 696 final long ident = Binder.clearCallingIdentity(); 697 try { 698 final boolean retval = mSettings.isSpellCheckerEnabled(); 699 if (DBG) { 700 Slog.w(TAG, "getSpellCheckerEnabled: " + retval); 701 } 702 return retval; 703 } finally { 704 Binder.restoreCallingIdentity(ident); 705 } 706 } 707 708 @Override 709 protected void dump(FileDescriptor fd, PrintWriter pw, String[] args) { 710 if (!DumpUtils.checkDumpPermission(mContext, TAG, pw)) return; 711 712 synchronized (mLock) { 713 pw.println("Current Text Services Manager state:"); 714 pw.println(" Spell Checkers:"); 715 int spellCheckerIndex = 0; 716 for (final SpellCheckerInfo info : mSpellCheckerMap.values()) { 717 pw.println(" Spell Checker #" + spellCheckerIndex); 718 info.dump(pw, " "); 719 ++spellCheckerIndex; 720 } 721 pw.println(""); 722 pw.println(" Spell Checker Bind Groups:"); 723 for (final Map.Entry<String, SpellCheckerBindGroup> ent 724 : mSpellCheckerBindGroups.entrySet()) { 725 final SpellCheckerBindGroup grp = ent.getValue(); 726 pw.println(" " + ent.getKey() + " " + grp + ":"); 727 pw.println(" " + "mInternalConnection=" + grp.mInternalConnection); 728 pw.println(" " + "mSpellChecker=" + grp.mSpellChecker); 729 pw.println(" " + "mUnbindCalled=" + grp.mUnbindCalled); 730 pw.println(" " + "mConnected=" + grp.mConnected); 731 final int numPendingSessionRequests = grp.mPendingSessionRequests.size(); 732 for (int i = 0; i < numPendingSessionRequests; i++) { 733 final SessionRequest req = grp.mPendingSessionRequests.get(i); 734 pw.println(" " + "Pending Request #" + i + ":"); 735 pw.println(" " + "mTsListener=" + req.mTsListener); 736 pw.println(" " + "mScListener=" + req.mScListener); 737 pw.println(" " + "mScLocale=" + req.mLocale + " mUid=" + req.mUserId); 738 } 739 final int numOnGoingSessionRequests = grp.mOnGoingSessionRequests.size(); 740 for (int i = 0; i < numOnGoingSessionRequests; i++) { 741 final SessionRequest req = grp.mOnGoingSessionRequests.get(i); 742 pw.println(" " + "On going Request #" + i + ":"); 743 ++i; 744 pw.println(" " + "mTsListener=" + req.mTsListener); 745 pw.println(" " + "mScListener=" + req.mScListener); 746 pw.println( 747 " " + "mScLocale=" + req.mLocale + " mUid=" + req.mUserId); 748 } 749 final int N = grp.mListeners.getRegisteredCallbackCount(); 750 for (int i = 0; i < N; i++) { 751 final ISpellCheckerSessionListener mScListener = 752 grp.mListeners.getRegisteredCallbackItem(i); 753 pw.println(" " + "Listener #" + i + ":"); 754 pw.println(" " + "mScListener=" + mScListener); 755 pw.println(" " + "mGroup=" + grp); 756 } 757 } 758 pw.println(""); 759 pw.println(" mSettings:"); 760 mSettings.dumpLocked(pw, " "); 761 } 762 } 763 764 private static final class SessionRequest { 765 @UserIdInt 766 public final int mUserId; 767 @Nullable 768 public final String mLocale; 769 @NonNull 770 public final ITextServicesSessionListener mTsListener; 771 @NonNull 772 public final ISpellCheckerSessionListener mScListener; 773 @Nullable 774 public final Bundle mBundle; 775 776 SessionRequest(@UserIdInt final int userId, @Nullable String locale, 777 @NonNull ITextServicesSessionListener tsListener, 778 @NonNull ISpellCheckerSessionListener scListener, @Nullable Bundle bundle) { 779 mUserId = userId; 780 mLocale = locale; 781 mTsListener = tsListener; 782 mScListener = scListener; 783 mBundle = bundle; 784 } 785 } 786 787 // SpellCheckerBindGroup contains active text service session listeners. 788 // If there are no listeners anymore, the SpellCheckerBindGroup instance will be removed from 789 // mSpellCheckerBindGroups 790 private final class SpellCheckerBindGroup { 791 private final String TAG = SpellCheckerBindGroup.class.getSimpleName(); 792 private final InternalServiceConnection mInternalConnection; 793 private final InternalDeathRecipients mListeners; 794 private boolean mUnbindCalled; 795 private ISpellCheckerService mSpellChecker; 796 private boolean mConnected; 797 private final ArrayList<SessionRequest> mPendingSessionRequests = new ArrayList<>(); 798 private final ArrayList<SessionRequest> mOnGoingSessionRequests = new ArrayList<>(); 799 800 public SpellCheckerBindGroup(InternalServiceConnection connection) { 801 mInternalConnection = connection; 802 mListeners = new InternalDeathRecipients(this); 803 } 804 805 public void onServiceConnectedLocked(ISpellCheckerService spellChecker) { 806 if (DBG) { 807 Slog.d(TAG, "onServiceConnectedLocked"); 808 } 809 810 if (mUnbindCalled) { 811 return; 812 } 813 mSpellChecker = spellChecker; 814 mConnected = true; 815 // Dispatch pending getISpellCheckerSession requests. 816 try { 817 final int size = mPendingSessionRequests.size(); 818 for (int i = 0; i < size; ++i) { 819 final SessionRequest request = mPendingSessionRequests.get(i); 820 mSpellChecker.getISpellCheckerSession( 821 request.mLocale, request.mScListener, request.mBundle, 822 new ISpellCheckerServiceCallbackBinder(this, request)); 823 mOnGoingSessionRequests.add(request); 824 } 825 mPendingSessionRequests.clear(); 826 } catch(RemoteException e) { 827 // The target spell checker service is not available. Better to reset the state. 828 removeAllLocked(); 829 } 830 cleanLocked(); 831 } 832 833 public void onServiceDisconnectedLocked() { 834 if (DBG) { 835 Slog.d(TAG, "onServiceDisconnectedLocked"); 836 } 837 838 mSpellChecker = null; 839 mConnected = false; 840 } 841 842 public void removeListener(ISpellCheckerSessionListener listener) { 843 if (DBG) { 844 Slog.w(TAG, "remove listener: " + listener.hashCode()); 845 } 846 synchronized (mLock) { 847 mListeners.unregister(listener); 848 cleanLocked(); 849 } 850 } 851 852 // cleanLocked may remove elements from mSpellCheckerBindGroups 853 private void cleanLocked() { 854 if (DBG) { 855 Slog.d(TAG, "cleanLocked"); 856 } 857 if (mUnbindCalled) { 858 return; 859 } 860 // If there are no more active listeners, clean up. Only do this once. 861 if (mListeners.getRegisteredCallbackCount() > 0) { 862 return; 863 } 864 if (!mPendingSessionRequests.isEmpty()) { 865 return; 866 } 867 if (!mOnGoingSessionRequests.isEmpty()) { 868 return; 869 } 870 final String sciId = mInternalConnection.mSciId; 871 final SpellCheckerBindGroup cur = mSpellCheckerBindGroups.get(sciId); 872 if (cur == this) { 873 if (DBG) { 874 Slog.d(TAG, "Remove bind group."); 875 } 876 mSpellCheckerBindGroups.remove(sciId); 877 } 878 mContext.unbindService(mInternalConnection); 879 mUnbindCalled = true; 880 } 881 882 public void removeAllLocked() { 883 Slog.e(TAG, "Remove the spell checker bind unexpectedly."); 884 final int size = mListeners.getRegisteredCallbackCount(); 885 for (int i = size - 1; i >= 0; --i) { 886 mListeners.unregister(mListeners.getRegisteredCallbackItem(i)); 887 } 888 mPendingSessionRequests.clear(); 889 mOnGoingSessionRequests.clear(); 890 cleanLocked(); 891 } 892 893 public void getISpellCheckerSessionOrQueueLocked(@NonNull SessionRequest request) { 894 if (mUnbindCalled) { 895 return; 896 } 897 if (!mConnected) { 898 mPendingSessionRequests.add(request); 899 return; 900 } 901 try { 902 mSpellChecker.getISpellCheckerSession( 903 request.mLocale, request.mScListener, request.mBundle, 904 new ISpellCheckerServiceCallbackBinder(this, request)); 905 mOnGoingSessionRequests.add(request); 906 } catch(RemoteException e) { 907 // The target spell checker service is not available. Better to reset the state. 908 removeAllLocked(); 909 } 910 cleanLocked(); 911 } 912 913 void onSessionCreated(@Nullable final ISpellCheckerSession newSession, 914 @NonNull final SessionRequest request) { 915 synchronized (mLock) { 916 if (mUnbindCalled) { 917 return; 918 } 919 if (mOnGoingSessionRequests.remove(request)) { 920 try { 921 request.mTsListener.onServiceConnected(newSession); 922 mListeners.register(request.mScListener); 923 } catch (RemoteException e) { 924 // Technically this can happen if the spell checker client app is already 925 // dead. We can just forget about this request; the request is already 926 // removed from mOnGoingSessionRequests and the death recipient listener is 927 // not yet added to mListeners. There is nothing to release further. 928 } 929 } 930 cleanLocked(); 931 } 932 } 933 } 934 935 private final class InternalServiceConnection implements ServiceConnection { 936 private final String mSciId; 937 public InternalServiceConnection(String id) { 938 mSciId = id; 939 } 940 941 @Override 942 public void onServiceConnected(ComponentName name, IBinder service) { 943 synchronized (mLock) { 944 onServiceConnectedInnerLocked(name, service); 945 } 946 } 947 948 private void onServiceConnectedInnerLocked(ComponentName name, IBinder service) { 949 if (DBG) { 950 Slog.w(TAG, "onServiceConnectedInnerLocked: " + name); 951 } 952 final ISpellCheckerService spellChecker = 953 ISpellCheckerService.Stub.asInterface(service); 954 final SpellCheckerBindGroup group = mSpellCheckerBindGroups.get(mSciId); 955 if (group != null && this == group.mInternalConnection) { 956 group.onServiceConnectedLocked(spellChecker); 957 } 958 } 959 960 @Override 961 public void onServiceDisconnected(ComponentName name) { 962 synchronized (mLock) { 963 onServiceDisconnectedInnerLocked(name); 964 } 965 } 966 967 private void onServiceDisconnectedInnerLocked(ComponentName name) { 968 if (DBG) { 969 Slog.w(TAG, "onServiceDisconnectedInnerLocked: " + name); 970 } 971 final SpellCheckerBindGroup group = mSpellCheckerBindGroups.get(mSciId); 972 if (group != null && this == group.mInternalConnection) { 973 group.onServiceDisconnectedLocked(); 974 } 975 } 976 } 977 978 private static final class InternalDeathRecipients extends 979 RemoteCallbackList<ISpellCheckerSessionListener> { 980 private final SpellCheckerBindGroup mGroup; 981 982 public InternalDeathRecipients(SpellCheckerBindGroup group) { 983 mGroup = group; 984 } 985 986 @Override 987 public void onCallbackDied(ISpellCheckerSessionListener listener) { 988 mGroup.removeListener(listener); 989 } 990 } 991 992 private static final class ISpellCheckerServiceCallbackBinder 993 extends ISpellCheckerServiceCallback.Stub { 994 @NonNull 995 private final SpellCheckerBindGroup mBindGroup; 996 @NonNull 997 private final SessionRequest mRequest; 998 999 ISpellCheckerServiceCallbackBinder(@NonNull final SpellCheckerBindGroup bindGroup, 1000 @NonNull final SessionRequest request) { 1001 mBindGroup = bindGroup; 1002 mRequest = request; 1003 } 1004 1005 @Override 1006 public void onSessionCreated(@Nullable ISpellCheckerSession newSession) { 1007 mBindGroup.onSessionCreated(newSession, mRequest); 1008 } 1009 } 1010 1011 private static final class TextServicesSettings { 1012 private final ContentResolver mResolver; 1013 @UserIdInt 1014 private int mCurrentUserId; 1015 @GuardedBy("mLock") 1016 private int[] mCurrentProfileIds = new int[0]; 1017 private Object mLock = new Object(); 1018 1019 /** 1020 * On-memory data store to emulate when {@link #mCopyOnWrite} is {@code true}. 1021 */ 1022 private final HashMap<String, String> mCopyOnWriteDataStore = new HashMap<>(); 1023 private boolean mCopyOnWrite = false; 1024 1025 public TextServicesSettings(ContentResolver resolver, @UserIdInt int userId, 1026 boolean copyOnWrite) { 1027 mResolver = resolver; 1028 switchCurrentUser(userId, copyOnWrite); 1029 } 1030 1031 /** 1032 * Must be called when the current user is changed. 1033 * 1034 * @param userId The user ID. 1035 * @param copyOnWrite If {@code true}, for each settings key 1036 * (e.g. {@link Settings.Secure#SELECTED_SPELL_CHECKER}) we use the actual settings on the 1037 * {@link Settings.Secure} until we do the first write operation. 1038 */ 1039 public void switchCurrentUser(@UserIdInt int userId, boolean copyOnWrite) { 1040 if (DBG) { 1041 Slog.d(TAG, "--- Swtich the current user from " + mCurrentUserId + " to " 1042 + userId + ", new ime = " + getSelectedSpellChecker()); 1043 } 1044 if (mCurrentUserId != userId || mCopyOnWrite != copyOnWrite) { 1045 mCopyOnWriteDataStore.clear(); 1046 // TODO: mCurrentProfileIds should be cleared here. 1047 } 1048 // TSMS settings are kept per user, so keep track of current user 1049 mCurrentUserId = userId; 1050 mCopyOnWrite = copyOnWrite; 1051 // TODO: mCurrentProfileIds should be updated here. 1052 } 1053 1054 private void putString(final String key, final String str) { 1055 if (mCopyOnWrite) { 1056 mCopyOnWriteDataStore.put(key, str); 1057 } else { 1058 Settings.Secure.putStringForUser(mResolver, key, str, mCurrentUserId); 1059 } 1060 } 1061 1062 @Nullable 1063 private String getString(@NonNull final String key, @Nullable final String defaultValue) { 1064 final String result; 1065 if (mCopyOnWrite && mCopyOnWriteDataStore.containsKey(key)) { 1066 result = mCopyOnWriteDataStore.get(key); 1067 } else { 1068 result = Settings.Secure.getStringForUser(mResolver, key, mCurrentUserId); 1069 } 1070 return result != null ? result : defaultValue; 1071 } 1072 1073 private void putInt(final String key, final int value) { 1074 if (mCopyOnWrite) { 1075 mCopyOnWriteDataStore.put(key, String.valueOf(value)); 1076 } else { 1077 Settings.Secure.putIntForUser(mResolver, key, value, mCurrentUserId); 1078 } 1079 } 1080 1081 private int getInt(final String key, final int defaultValue) { 1082 if (mCopyOnWrite && mCopyOnWriteDataStore.containsKey(key)) { 1083 final String result = mCopyOnWriteDataStore.get(key); 1084 return result != null ? Integer.parseInt(result) : 0; 1085 } 1086 return Settings.Secure.getIntForUser(mResolver, key, defaultValue, mCurrentUserId); 1087 } 1088 1089 private boolean getBoolean(final String key, final boolean defaultValue) { 1090 return getInt(key, defaultValue ? 1 : 0) == 1; 1091 } 1092 1093 public void setCurrentProfileIds(int[] currentProfileIds) { 1094 synchronized (mLock) { 1095 mCurrentProfileIds = currentProfileIds; 1096 } 1097 } 1098 1099 public boolean isCurrentProfile(@UserIdInt int userId) { 1100 synchronized (mLock) { 1101 if (userId == mCurrentUserId) return true; 1102 for (int i = 0; i < mCurrentProfileIds.length; i++) { 1103 if (userId == mCurrentProfileIds[i]) return true; 1104 } 1105 return false; 1106 } 1107 } 1108 1109 @UserIdInt 1110 public int getCurrentUserId() { 1111 return mCurrentUserId; 1112 } 1113 1114 public void putSelectedSpellChecker(@Nullable String sciId) { 1115 putString(Settings.Secure.SELECTED_SPELL_CHECKER, sciId); 1116 } 1117 1118 public void putSelectedSpellCheckerSubtype(int hashCode) { 1119 putInt(Settings.Secure.SELECTED_SPELL_CHECKER_SUBTYPE, hashCode); 1120 } 1121 1122 @NonNull 1123 public String getSelectedSpellChecker() { 1124 return getString(Settings.Secure.SELECTED_SPELL_CHECKER, ""); 1125 } 1126 1127 public int getSelectedSpellCheckerSubtype(final int defaultValue) { 1128 return getInt(Settings.Secure.SELECTED_SPELL_CHECKER_SUBTYPE, defaultValue); 1129 } 1130 1131 public boolean isSpellCheckerEnabled() { 1132 return getBoolean(Settings.Secure.SPELL_CHECKER_ENABLED, true); 1133 } 1134 1135 public void dumpLocked(final PrintWriter pw, final String prefix) { 1136 pw.println(prefix + "mCurrentUserId=" + mCurrentUserId); 1137 pw.println(prefix + "mCurrentProfileIds=" + Arrays.toString(mCurrentProfileIds)); 1138 pw.println(prefix + "mCopyOnWrite=" + mCopyOnWrite); 1139 } 1140 } 1141 1142 // ---------------------------------------------------------------------- 1143 // Utilities for debug 1144 private static String getStackTrace() { 1145 final StringBuilder sb = new StringBuilder(); 1146 try { 1147 throw new RuntimeException(); 1148 } catch (RuntimeException e) { 1149 final StackTraceElement[] frames = e.getStackTrace(); 1150 // Start at 1 because the first frame is here and we don't care about it 1151 for (int j = 1; j < frames.length; ++j) { 1152 sb.append(frames[j].toString() + "\n"); 1153 } 1154 } 1155 return sb.toString(); 1156 } 1157} 1158