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