/* * Copyright (C) 2011 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.server; import static android.view.textservice.TextServicesManager.DISABLE_PER_PROFILE_SPELL_CHECKER; import com.android.internal.annotations.GuardedBy; import com.android.internal.content.PackageMonitor; import com.android.internal.inputmethod.InputMethodUtils; import com.android.internal.textservice.ISpellCheckerService; import com.android.internal.textservice.ISpellCheckerServiceCallback; import com.android.internal.textservice.ISpellCheckerSession; import com.android.internal.textservice.ISpellCheckerSessionListener; import com.android.internal.textservice.ITextServicesManager; import com.android.internal.textservice.ITextServicesSessionListener; import com.android.internal.textservice.LazyIntToIntMap; import com.android.internal.util.DumpUtils; import org.xmlpull.v1.XmlPullParserException; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.UserIdInt; import android.content.ComponentName; import android.content.ContentResolver; import android.content.Context; import android.content.Intent; import android.content.ServiceConnection; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; import android.content.pm.ServiceInfo; import android.content.pm.UserInfo; import android.os.Binder; import android.os.Bundle; import android.os.IBinder; import android.os.RemoteCallbackList; import android.os.RemoteException; import android.os.UserHandle; import android.os.UserManager; import android.provider.Settings; import android.service.textservice.SpellCheckerService; import android.text.TextUtils; import android.util.Slog; import android.util.SparseArray; import android.view.inputmethod.InputMethodManager; import android.view.inputmethod.InputMethodSubtype; import android.view.textservice.SpellCheckerInfo; import android.view.textservice.SpellCheckerSubtype; import java.io.FileDescriptor; import java.io.IOException; import java.io.PrintWriter; import java.util.Arrays; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.function.Predicate; public class TextServicesManagerService extends ITextServicesManager.Stub { private static final String TAG = TextServicesManagerService.class.getSimpleName(); private static final boolean DBG = false; private final Context mContext; private final TextServicesMonitor mMonitor; private final SparseArray mUserData = new SparseArray<>(); @NonNull private final UserManager mUserManager; private final Object mLock = new Object(); @NonNull @GuardedBy("mLock") private final LazyIntToIntMap mSpellCheckerOwnerUserIdMap; private static class TextServicesData { @UserIdInt private final int mUserId; private final HashMap mSpellCheckerMap; private final ArrayList mSpellCheckerList; private final HashMap mSpellCheckerBindGroups; private final Context mContext; private final ContentResolver mResolver; public int mUpdateCount = 0; public TextServicesData(@UserIdInt int userId, @NonNull Context context) { mUserId = userId; mSpellCheckerMap = new HashMap<>(); mSpellCheckerList = new ArrayList<>(); mSpellCheckerBindGroups = new HashMap<>(); mContext = context; mResolver = context.getContentResolver(); } private void putString(final String key, final String str) { Settings.Secure.putStringForUser(mResolver, key, str, mUserId); } @Nullable private String getString(@NonNull final String key, @Nullable final String defaultValue) { final String result; result = Settings.Secure.getStringForUser(mResolver, key, mUserId); return result != null ? result : defaultValue; } private void putInt(final String key, final int value) { Settings.Secure.putIntForUser(mResolver, key, value, mUserId); } private int getInt(final String key, final int defaultValue) { return Settings.Secure.getIntForUser(mResolver, key, defaultValue, mUserId); } private boolean getBoolean(final String key, final boolean defaultValue) { return getInt(key, defaultValue ? 1 : 0) == 1; } private void putSelectedSpellChecker(@Nullable String sciId) { putString(Settings.Secure.SELECTED_SPELL_CHECKER, sciId); } private void putSelectedSpellCheckerSubtype(int hashCode) { putInt(Settings.Secure.SELECTED_SPELL_CHECKER_SUBTYPE, hashCode); } @NonNull private String getSelectedSpellChecker() { return getString(Settings.Secure.SELECTED_SPELL_CHECKER, ""); } public int getSelectedSpellCheckerSubtype(final int defaultValue) { return getInt(Settings.Secure.SELECTED_SPELL_CHECKER_SUBTYPE, defaultValue); } public boolean isSpellCheckerEnabled() { return getBoolean(Settings.Secure.SPELL_CHECKER_ENABLED, true); } @Nullable public SpellCheckerInfo getCurrentSpellChecker() { final String curSpellCheckerId = getSelectedSpellChecker(); if (TextUtils.isEmpty(curSpellCheckerId)) { return null; } return mSpellCheckerMap.get(curSpellCheckerId); } public void setCurrentSpellChecker(@Nullable SpellCheckerInfo sci) { if (sci != null) { putSelectedSpellChecker(sci.getId()); } else { putSelectedSpellChecker(""); } putSelectedSpellCheckerSubtype(SpellCheckerSubtype.SUBTYPE_ID_NONE); } private void initializeTextServicesData() { if (DBG) { Slog.d(TAG, "initializeTextServicesData for user: " + mUserId); } mSpellCheckerList.clear(); mSpellCheckerMap.clear(); mUpdateCount++; final PackageManager pm = mContext.getPackageManager(); // Note: We do not specify PackageManager.MATCH_ENCRYPTION_* flags here because the // default behavior of PackageManager is exactly what we want. It by default picks up // appropriate services depending on the unlock state for the specified user. final List services = pm.queryIntentServicesAsUser( new Intent(SpellCheckerService.SERVICE_INTERFACE), PackageManager.GET_META_DATA, mUserId); final int N = services.size(); for (int i = 0; i < N; ++i) { final ResolveInfo ri = services.get(i); final ServiceInfo si = ri.serviceInfo; final ComponentName compName = new ComponentName(si.packageName, si.name); if (!android.Manifest.permission.BIND_TEXT_SERVICE.equals(si.permission)) { Slog.w(TAG, "Skipping text service " + compName + ": it does not require the permission " + android.Manifest.permission.BIND_TEXT_SERVICE); continue; } if (DBG) Slog.d(TAG, "Add: " + compName + " for user: " + mUserId); try { final SpellCheckerInfo sci = new SpellCheckerInfo(mContext, ri); if (sci.getSubtypeCount() <= 0) { Slog.w(TAG, "Skipping text service " + compName + ": it does not contain subtypes."); continue; } mSpellCheckerList.add(sci); mSpellCheckerMap.put(sci.getId(), sci); } catch (XmlPullParserException e) { Slog.w(TAG, "Unable to load the spell checker " + compName, e); } catch (IOException e) { Slog.w(TAG, "Unable to load the spell checker " + compName, e); } } if (DBG) { Slog.d(TAG, "initializeSpellCheckerMap: " + mSpellCheckerList.size() + "," + mSpellCheckerMap.size()); } } private void dump(PrintWriter pw) { int spellCheckerIndex = 0; pw.println(" User #" + mUserId); pw.println(" Spell Checkers:"); pw.println(" Spell Checkers: " + "mUpdateCount=" + mUpdateCount); for (final SpellCheckerInfo info : mSpellCheckerMap.values()) { pw.println(" Spell Checker #" + spellCheckerIndex); info.dump(pw, " "); ++spellCheckerIndex; } pw.println(""); pw.println(" Spell Checker Bind Groups:"); HashMap spellCheckerBindGroups = mSpellCheckerBindGroups; for (final Map.Entry ent : spellCheckerBindGroups.entrySet()) { final SpellCheckerBindGroup grp = ent.getValue(); pw.println(" " + ent.getKey() + " " + grp + ":"); pw.println(" " + "mInternalConnection=" + grp.mInternalConnection); pw.println(" " + "mSpellChecker=" + grp.mSpellChecker); pw.println(" " + "mUnbindCalled=" + grp.mUnbindCalled); pw.println(" " + "mConnected=" + grp.mConnected); final int numPendingSessionRequests = grp.mPendingSessionRequests.size(); for (int j = 0; j < numPendingSessionRequests; j++) { final SessionRequest req = grp.mPendingSessionRequests.get(j); pw.println(" " + "Pending Request #" + j + ":"); pw.println(" " + "mTsListener=" + req.mTsListener); pw.println(" " + "mScListener=" + req.mScListener); pw.println( " " + "mScLocale=" + req.mLocale + " mUid=" + req.mUid); } final int numOnGoingSessionRequests = grp.mOnGoingSessionRequests.size(); for (int j = 0; j < numOnGoingSessionRequests; j++) { final SessionRequest req = grp.mOnGoingSessionRequests.get(j); pw.println(" " + "On going Request #" + j + ":"); ++j; pw.println(" " + "mTsListener=" + req.mTsListener); pw.println(" " + "mScListener=" + req.mScListener); pw.println( " " + "mScLocale=" + req.mLocale + " mUid=" + req.mUid); } final int N = grp.mListeners.getRegisteredCallbackCount(); for (int j = 0; j < N; j++) { final ISpellCheckerSessionListener mScListener = grp.mListeners.getRegisteredCallbackItem(j); pw.println(" " + "Listener #" + j + ":"); pw.println(" " + "mScListener=" + mScListener); pw.println(" " + "mGroup=" + grp); } } } } public static final class Lifecycle extends SystemService { private TextServicesManagerService mService; public Lifecycle(Context context) { super(context); mService = new TextServicesManagerService(context); } @Override public void onStart() { publishBinderService(Context.TEXT_SERVICES_MANAGER_SERVICE, mService); } @Override public void onStopUser(@UserIdInt int userHandle) { if (DBG) { Slog.d(TAG, "onStopUser userId: " + userHandle); } mService.onStopUser(userHandle); } @Override public void onUnlockUser(@UserIdInt int userHandle) { if(DBG) { Slog.d(TAG, "onUnlockUser userId: " + userHandle); } // Called on the system server's main looper thread. // TODO: Dispatch this to a worker thread as needed. mService.onUnlockUser(userHandle); } } void onStopUser(@UserIdInt int userId) { synchronized (mLock) { // Clear user ID mapping table. mSpellCheckerOwnerUserIdMap.delete(userId); // Clean per-user data TextServicesData tsd = mUserData.get(userId); if (tsd == null) return; unbindServiceLocked(tsd); // Remove bind groups first mUserData.remove(userId); // This needs to be done after bind groups are all removed } } void onUnlockUser(@UserIdInt int userId) { synchronized (mLock) { // Initialize internal state for the given user initializeInternalStateLocked(userId); } } public TextServicesManagerService(Context context) { mContext = context; mUserManager = mContext.getSystemService(UserManager.class); mSpellCheckerOwnerUserIdMap = new LazyIntToIntMap(callingUserId -> { if (DISABLE_PER_PROFILE_SPELL_CHECKER) { final long token = Binder.clearCallingIdentity(); try { final UserInfo parent = mUserManager.getProfileParent(callingUserId); return (parent != null) ? parent.id : callingUserId; } finally { Binder.restoreCallingIdentity(token); } } else { return callingUserId; } }); mMonitor = new TextServicesMonitor(); mMonitor.register(context, null, UserHandle.ALL, true); } private void initializeInternalStateLocked(@UserIdInt int userId) { // When DISABLE_PER_PROFILE_SPELL_CHECKER is true, we make sure here that work profile users // will never have non-null TextServicesData for their user ID. if (DISABLE_PER_PROFILE_SPELL_CHECKER && userId != mSpellCheckerOwnerUserIdMap.get(userId)) { return; } TextServicesData tsd = mUserData.get(userId); if (tsd == null) { tsd = new TextServicesData(userId, mContext); mUserData.put(userId, tsd); } tsd.initializeTextServicesData(); SpellCheckerInfo sci = tsd.getCurrentSpellChecker(); if (sci == null) { sci = findAvailSystemSpellCheckerLocked(null, tsd); // Set the current spell checker if there is one or more system spell checkers // available. In this case, "sci" is the first one in the available spell // checkers. setCurrentSpellCheckerLocked(sci, tsd); } } private final class TextServicesMonitor extends PackageMonitor { @Override public void onSomePackagesChanged() { int userId = getChangingUserId(); if(DBG) { Slog.d(TAG, "onSomePackagesChanged: " + userId); } synchronized (mLock) { TextServicesData tsd = mUserData.get(userId); if (tsd == null) return; // TODO: Update for each locale SpellCheckerInfo sci = tsd.getCurrentSpellChecker(); tsd.initializeTextServicesData(); // If spell checker is disabled, just return. The user should explicitly // enable the spell checker. if (!tsd.isSpellCheckerEnabled()) return; if (sci == null) { sci = findAvailSystemSpellCheckerLocked(null, tsd); // Set the current spell checker if there is one or more system spell checkers // available. In this case, "sci" is the first one in the available spell // checkers. setCurrentSpellCheckerLocked(sci, tsd); } else { final String packageName = sci.getPackageName(); final int change = isPackageDisappearing(packageName); if (DBG) Slog.d(TAG, "Changing package name: " + packageName); if (change == PACKAGE_PERMANENT_CHANGE || change == PACKAGE_TEMPORARY_CHANGE) { SpellCheckerInfo availSci = findAvailSystemSpellCheckerLocked(packageName, tsd); // Set the spell checker settings if different than before if (availSci == null || (availSci != null && !availSci.getId().equals(sci.getId()))) { setCurrentSpellCheckerLocked(availSci, tsd); } } } } } } private boolean bindCurrentSpellCheckerService( Intent service, ServiceConnection conn, int flags, @UserIdInt int userId) { if (service == null || conn == null) { Slog.e(TAG, "--- bind failed: service = " + service + ", conn = " + conn + ", userId =" + userId); return false; } return mContext.bindServiceAsUser(service, conn, flags, UserHandle.of(userId)); } private void unbindServiceLocked(TextServicesData tsd) { HashMap spellCheckerBindGroups = tsd.mSpellCheckerBindGroups; for (SpellCheckerBindGroup scbg : spellCheckerBindGroups.values()) { scbg.removeAllLocked(); } spellCheckerBindGroups.clear(); } private SpellCheckerInfo findAvailSystemSpellCheckerLocked(String prefPackage, TextServicesData tsd) { // Filter the spell checker list to remove spell checker services that are not pre-installed ArrayList spellCheckerList = new ArrayList<>(); for (SpellCheckerInfo sci : tsd.mSpellCheckerList) { if ((sci.getServiceInfo().applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0) { spellCheckerList.add(sci); } } final int spellCheckersCount = spellCheckerList.size(); if (spellCheckersCount == 0) { Slog.w(TAG, "no available spell checker services found"); return null; } if (prefPackage != null) { for (int i = 0; i < spellCheckersCount; ++i) { final SpellCheckerInfo sci = spellCheckerList.get(i); if (prefPackage.equals(sci.getPackageName())) { if (DBG) { Slog.d(TAG, "findAvailSystemSpellCheckerLocked: " + sci.getPackageName()); } return sci; } } } // Look up a spell checker based on the system locale. // TODO: Still there is a room to improve in the following logic: e.g., check if the package // is pre-installed or not. final Locale systemLocal = mContext.getResources().getConfiguration().locale; final ArrayList suitableLocales = InputMethodUtils.getSuitableLocalesForSpellChecker(systemLocal); if (DBG) { Slog.w(TAG, "findAvailSystemSpellCheckerLocked suitableLocales=" + Arrays.toString(suitableLocales.toArray(new Locale[suitableLocales.size()]))); } final int localeCount = suitableLocales.size(); for (int localeIndex = 0; localeIndex < localeCount; ++localeIndex) { final Locale locale = suitableLocales.get(localeIndex); for (int spellCheckersIndex = 0; spellCheckersIndex < spellCheckersCount; ++spellCheckersIndex) { final SpellCheckerInfo info = spellCheckerList.get(spellCheckersIndex); final int subtypeCount = info.getSubtypeCount(); for (int subtypeIndex = 0; subtypeIndex < subtypeCount; ++subtypeIndex) { final SpellCheckerSubtype subtype = info.getSubtypeAt(subtypeIndex); final Locale subtypeLocale = InputMethodUtils.constructLocaleFromString( subtype.getLocale()); if (locale.equals(subtypeLocale)) { // TODO: We may have more spell checkers that fall into this category. // Ideally we should pick up the most suitable one instead of simply // returning the first found one. return info; } } } } if (spellCheckersCount > 1) { Slog.w(TAG, "more than one spell checker service found, picking first"); } return spellCheckerList.get(0); } // TODO: Save SpellCheckerService by supported languages. Currently only one spell // checker is saved. @Override public SpellCheckerInfo getCurrentSpellChecker(String locale) { int userId = UserHandle.getCallingUserId(); synchronized (mLock) { final TextServicesData tsd = getDataFromCallingUserIdLocked(userId); if (tsd == null) return null; return tsd.getCurrentSpellChecker(); } } // TODO: Respect allowImplicitlySelectedSubtype // TODO: Save SpellCheckerSubtype by supported languages by looking at "locale". @Override public SpellCheckerSubtype getCurrentSpellCheckerSubtype( String locale, boolean allowImplicitlySelectedSubtype) { final int subtypeHashCode; final SpellCheckerInfo sci; final Locale systemLocale; final int userId = UserHandle.getCallingUserId(); synchronized (mLock) { final TextServicesData tsd = getDataFromCallingUserIdLocked(userId); if (tsd == null) return null; subtypeHashCode = tsd.getSelectedSpellCheckerSubtype(SpellCheckerSubtype.SUBTYPE_ID_NONE); if (DBG) { Slog.w(TAG, "getCurrentSpellCheckerSubtype: " + subtypeHashCode); } sci = tsd.getCurrentSpellChecker(); systemLocale = mContext.getResources().getConfiguration().locale; } if (sci == null || sci.getSubtypeCount() == 0) { if (DBG) { Slog.w(TAG, "Subtype not found."); } return null; } if (subtypeHashCode == SpellCheckerSubtype.SUBTYPE_ID_NONE && !allowImplicitlySelectedSubtype) { return null; } String candidateLocale = null; if (subtypeHashCode == 0) { // Spell checker language settings == "auto" final InputMethodManager imm = mContext.getSystemService(InputMethodManager.class); if (imm != null) { final InputMethodSubtype currentInputMethodSubtype = imm.getCurrentInputMethodSubtype(); if (currentInputMethodSubtype != null) { final String localeString = currentInputMethodSubtype.getLocale(); if (!TextUtils.isEmpty(localeString)) { // 1. Use keyboard locale if available in the spell checker candidateLocale = localeString; } } } if (candidateLocale == null) { // 2. Use System locale if available in the spell checker candidateLocale = systemLocale.toString(); } } SpellCheckerSubtype candidate = null; for (int i = 0; i < sci.getSubtypeCount(); ++i) { final SpellCheckerSubtype scs = sci.getSubtypeAt(i); if (subtypeHashCode == 0) { final String scsLocale = scs.getLocale(); if (candidateLocale.equals(scsLocale)) { return scs; } else if (candidate == null) { if (candidateLocale.length() >= 2 && scsLocale.length() >= 2 && candidateLocale.startsWith(scsLocale)) { // Fall back to the applicable language candidate = scs; } } } else if (scs.hashCode() == subtypeHashCode) { if (DBG) { Slog.w(TAG, "Return subtype " + scs.hashCode() + ", input= " + locale + ", " + scs.getLocale()); } // 3. Use the user specified spell check language return scs; } } // 4. Fall back to the applicable language and return it if not null // 5. Simply just return it even if it's null which means we could find no suitable // spell check languages return candidate; } @Override public void getSpellCheckerService(String sciId, String locale, ITextServicesSessionListener tsListener, ISpellCheckerSessionListener scListener, Bundle bundle) { if (TextUtils.isEmpty(sciId) || tsListener == null || scListener == null) { Slog.e(TAG, "getSpellCheckerService: Invalid input."); return; } int callingUserId = UserHandle.getCallingUserId(); synchronized (mLock) { final TextServicesData tsd = getDataFromCallingUserIdLocked(callingUserId); if (tsd == null) return; HashMap spellCheckerMap = tsd.mSpellCheckerMap; if (!spellCheckerMap.containsKey(sciId)) { return; } final SpellCheckerInfo sci = spellCheckerMap.get(sciId); HashMap spellCheckerBindGroups = tsd.mSpellCheckerBindGroups; SpellCheckerBindGroup bindGroup = spellCheckerBindGroups.get(sciId); final int uid = Binder.getCallingUid(); if (bindGroup == null) { final long ident = Binder.clearCallingIdentity(); try { bindGroup = startSpellCheckerServiceInnerLocked(sci, tsd); } finally { Binder.restoreCallingIdentity(ident); } if (bindGroup == null) { // startSpellCheckerServiceInnerLocked failed. return; } } // Start getISpellCheckerSession async IPC, or just queue the request until the spell // checker service is bound. bindGroup.getISpellCheckerSessionOrQueueLocked( new SessionRequest(uid, locale, tsListener, scListener, bundle)); } } @Override public boolean isSpellCheckerEnabled() { int userId = UserHandle.getCallingUserId(); synchronized (mLock) { final TextServicesData tsd = getDataFromCallingUserIdLocked(userId); if (tsd == null) return false; return tsd.isSpellCheckerEnabled(); } } @Nullable private SpellCheckerBindGroup startSpellCheckerServiceInnerLocked(SpellCheckerInfo info, TextServicesData tsd) { if (DBG) { Slog.w(TAG, "Start spell checker session inner locked."); } final String sciId = info.getId(); final InternalServiceConnection connection = new InternalServiceConnection(sciId, tsd.mSpellCheckerBindGroups); final Intent serviceIntent = new Intent(SpellCheckerService.SERVICE_INTERFACE); serviceIntent.setComponent(info.getComponent()); if (DBG) { Slog.w(TAG, "bind service: " + info.getId()); } if (!bindCurrentSpellCheckerService(serviceIntent, connection, Context.BIND_AUTO_CREATE | Context.BIND_IMPORTANT_BACKGROUND, tsd.mUserId)) { Slog.e(TAG, "Failed to get a spell checker service."); return null; } final SpellCheckerBindGroup group = new SpellCheckerBindGroup(connection); tsd.mSpellCheckerBindGroups.put(sciId, group); return group; } @Override public SpellCheckerInfo[] getEnabledSpellCheckers() { int callingUserId = UserHandle.getCallingUserId(); synchronized (mLock) { final TextServicesData tsd = getDataFromCallingUserIdLocked(callingUserId); if (tsd == null) return null; ArrayList spellCheckerList = tsd.mSpellCheckerList; if (DBG) { Slog.d(TAG, "getEnabledSpellCheckers: " + spellCheckerList.size()); for (int i = 0; i < spellCheckerList.size(); ++i) { Slog.d(TAG, "EnabledSpellCheckers: " + spellCheckerList.get(i).getPackageName()); } } return spellCheckerList.toArray(new SpellCheckerInfo[spellCheckerList.size()]); } } @Override public void finishSpellCheckerService(ISpellCheckerSessionListener listener) { if (DBG) { Slog.d(TAG, "FinishSpellCheckerService"); } int userId = UserHandle.getCallingUserId(); synchronized (mLock) { final TextServicesData tsd = getDataFromCallingUserIdLocked(userId); if (tsd == null) return; final ArrayList removeList = new ArrayList<>(); HashMap spellCheckerBindGroups = tsd.mSpellCheckerBindGroups; for (SpellCheckerBindGroup group : spellCheckerBindGroups.values()) { if (group == null) continue; // Use removeList to avoid modifying mSpellCheckerBindGroups in this loop. removeList.add(group); } final int removeSize = removeList.size(); for (int i = 0; i < removeSize; ++i) { removeList.get(i).removeListener(listener); } } } private void setCurrentSpellCheckerLocked(@Nullable SpellCheckerInfo sci, TextServicesData tsd) { final String sciId = (sci != null) ? sci.getId() : ""; if (DBG) { Slog.w(TAG, "setCurrentSpellChecker: " + sciId); } final long ident = Binder.clearCallingIdentity(); try { tsd.setCurrentSpellChecker(sci); } finally { Binder.restoreCallingIdentity(ident); } } @Override protected void dump(FileDescriptor fd, PrintWriter pw, String[] args) { if (!DumpUtils.checkDumpPermission(mContext, TAG, pw)) return; if (args.length == 0 || (args.length == 1 && args[0].equals("-a"))) { // Dump all users' data synchronized (mLock) { pw.println("Current Text Services Manager state:"); pw.println(" Users:"); final int numOfUsers = mUserData.size(); for (int i = 0; i < numOfUsers; i++) { TextServicesData tsd = mUserData.valueAt(i); tsd.dump(pw); } } } else { // Dump a given user's data if (args.length != 2 || !args[0].equals("--user")) { pw.println("Invalid arguments to text services." ); return; } else { int userId = Integer.parseInt(args[1]); UserInfo userInfo = mUserManager.getUserInfo(userId); if (userInfo == null) { pw.println("Non-existent user."); return; } TextServicesData tsd = mUserData.get(userId); if (tsd == null) { pw.println("User needs to unlock first." ); return; } synchronized (mLock) { pw.println("Current Text Services Manager state:"); pw.println(" User " + userId + ":"); tsd.dump(pw); } } } } /** * @param callingUserId user ID of the calling process * @return {@link TextServicesData} for the given user. {@code null} if spell checker is not * temporarily / permanently available for the specified user */ @Nullable private TextServicesData getDataFromCallingUserIdLocked(@UserIdInt int callingUserId) { final int spellCheckerOwnerUserId = mSpellCheckerOwnerUserIdMap.get(callingUserId); final TextServicesData data = mUserData.get(spellCheckerOwnerUserId); if (DISABLE_PER_PROFILE_SPELL_CHECKER) { if (spellCheckerOwnerUserId != callingUserId) { // Calling process is running under child profile. if (data == null) { return null; } final SpellCheckerInfo info = data.getCurrentSpellChecker(); if (info == null) { return null; } final ServiceInfo serviceInfo = info.getServiceInfo(); if ((serviceInfo.applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) == 0) { // To be conservative, non pre-installed spell checker services are not allowed // to be used for child profiles. return null; } } } return data; } private static final class SessionRequest { public final int mUid; @Nullable public final String mLocale; @NonNull public final ITextServicesSessionListener mTsListener; @NonNull public final ISpellCheckerSessionListener mScListener; @Nullable public final Bundle mBundle; SessionRequest(int uid, @Nullable String locale, @NonNull ITextServicesSessionListener tsListener, @NonNull ISpellCheckerSessionListener scListener, @Nullable Bundle bundle) { mUid = uid; mLocale = locale; mTsListener = tsListener; mScListener = scListener; mBundle = bundle; } } // SpellCheckerBindGroup contains active text service session listeners. // If there are no listeners anymore, the SpellCheckerBindGroup instance will be removed from // mSpellCheckerBindGroups private final class SpellCheckerBindGroup { private final String TAG = SpellCheckerBindGroup.class.getSimpleName(); private final InternalServiceConnection mInternalConnection; private final InternalDeathRecipients mListeners; private boolean mUnbindCalled; private ISpellCheckerService mSpellChecker; private boolean mConnected; private final ArrayList mPendingSessionRequests = new ArrayList<>(); private final ArrayList mOnGoingSessionRequests = new ArrayList<>(); @NonNull HashMap mSpellCheckerBindGroups; public SpellCheckerBindGroup(InternalServiceConnection connection) { mInternalConnection = connection; mListeners = new InternalDeathRecipients(this); mSpellCheckerBindGroups = connection.mSpellCheckerBindGroups; } public void onServiceConnectedLocked(ISpellCheckerService spellChecker) { if (DBG) { Slog.d(TAG, "onServiceConnectedLocked"); } if (mUnbindCalled) { return; } mSpellChecker = spellChecker; mConnected = true; // Dispatch pending getISpellCheckerSession requests. try { final int size = mPendingSessionRequests.size(); for (int i = 0; i < size; ++i) { final SessionRequest request = mPendingSessionRequests.get(i); mSpellChecker.getISpellCheckerSession( request.mLocale, request.mScListener, request.mBundle, new ISpellCheckerServiceCallbackBinder(this, request)); mOnGoingSessionRequests.add(request); } mPendingSessionRequests.clear(); } catch(RemoteException e) { // The target spell checker service is not available. Better to reset the state. removeAllLocked(); } cleanLocked(); } public void onServiceDisconnectedLocked() { if (DBG) { Slog.d(TAG, "onServiceDisconnectedLocked"); } mSpellChecker = null; mConnected = false; } public void removeListener(ISpellCheckerSessionListener listener) { if (DBG) { Slog.w(TAG, "remove listener: " + listener.hashCode()); } synchronized (mLock) { mListeners.unregister(listener); final IBinder scListenerBinder = listener.asBinder(); final Predicate removeCondition = request -> request.mScListener.asBinder() == scListenerBinder; mPendingSessionRequests.removeIf(removeCondition); mOnGoingSessionRequests.removeIf(removeCondition); cleanLocked(); } } // cleanLocked may remove elements from mSpellCheckerBindGroups private void cleanLocked() { if (DBG) { Slog.d(TAG, "cleanLocked"); } if (mUnbindCalled) { return; } // If there are no more active listeners, clean up. Only do this once. if (mListeners.getRegisteredCallbackCount() > 0) { return; } if (!mPendingSessionRequests.isEmpty()) { return; } if (!mOnGoingSessionRequests.isEmpty()) { return; } final String sciId = mInternalConnection.mSciId; final SpellCheckerBindGroup cur = mSpellCheckerBindGroups.get(sciId); if (cur == this) { if (DBG) { Slog.d(TAG, "Remove bind group."); } mSpellCheckerBindGroups.remove(sciId); } mContext.unbindService(mInternalConnection); mUnbindCalled = true; } public void removeAllLocked() { Slog.e(TAG, "Remove the spell checker bind unexpectedly."); final int size = mListeners.getRegisteredCallbackCount(); for (int i = size - 1; i >= 0; --i) { mListeners.unregister(mListeners.getRegisteredCallbackItem(i)); } mPendingSessionRequests.clear(); mOnGoingSessionRequests.clear(); cleanLocked(); } public void getISpellCheckerSessionOrQueueLocked(@NonNull SessionRequest request) { if (mUnbindCalled) { return; } mListeners.register(request.mScListener); if (!mConnected) { mPendingSessionRequests.add(request); return; } try { mSpellChecker.getISpellCheckerSession( request.mLocale, request.mScListener, request.mBundle, new ISpellCheckerServiceCallbackBinder(this, request)); mOnGoingSessionRequests.add(request); } catch(RemoteException e) { // The target spell checker service is not available. Better to reset the state. removeAllLocked(); } cleanLocked(); } void onSessionCreated(@Nullable final ISpellCheckerSession newSession, @NonNull final SessionRequest request) { synchronized (mLock) { if (mUnbindCalled) { return; } if (mOnGoingSessionRequests.remove(request)) { try { request.mTsListener.onServiceConnected(newSession); } catch (RemoteException e) { // Technically this can happen if the spell checker client app is already // dead. We can just forget about this request; the request is already // removed from mOnGoingSessionRequests and the death recipient listener is // not yet added to mListeners. There is nothing to release further. } } cleanLocked(); } } } private final class InternalServiceConnection implements ServiceConnection { private final String mSciId; @NonNull private final HashMap mSpellCheckerBindGroups; public InternalServiceConnection(String id, @NonNull HashMap spellCheckerBindGroups) { mSciId = id; mSpellCheckerBindGroups = spellCheckerBindGroups; } @Override public void onServiceConnected(ComponentName name, IBinder service) { synchronized (mLock) { onServiceConnectedInnerLocked(name, service); } } private void onServiceConnectedInnerLocked(ComponentName name, IBinder service) { if (DBG) { Slog.w(TAG, "onServiceConnectedInnerLocked: " + name); } final ISpellCheckerService spellChecker = ISpellCheckerService.Stub.asInterface(service); final SpellCheckerBindGroup group = mSpellCheckerBindGroups.get(mSciId); if (group != null && this == group.mInternalConnection) { group.onServiceConnectedLocked(spellChecker); } } @Override public void onServiceDisconnected(ComponentName name) { synchronized (mLock) { onServiceDisconnectedInnerLocked(name); } } private void onServiceDisconnectedInnerLocked(ComponentName name) { if (DBG) { Slog.w(TAG, "onServiceDisconnectedInnerLocked: " + name); } final SpellCheckerBindGroup group = mSpellCheckerBindGroups.get(mSciId); if (group != null && this == group.mInternalConnection) { group.onServiceDisconnectedLocked(); } } } private static final class InternalDeathRecipients extends RemoteCallbackList { private final SpellCheckerBindGroup mGroup; public InternalDeathRecipients(SpellCheckerBindGroup group) { mGroup = group; } @Override public void onCallbackDied(ISpellCheckerSessionListener listener) { mGroup.removeListener(listener); } } private static final class ISpellCheckerServiceCallbackBinder extends ISpellCheckerServiceCallback.Stub { @NonNull private final SpellCheckerBindGroup mBindGroup; @NonNull private final SessionRequest mRequest; ISpellCheckerServiceCallbackBinder(@NonNull final SpellCheckerBindGroup bindGroup, @NonNull final SessionRequest request) { mBindGroup = bindGroup; mRequest = request; } @Override public void onSessionCreated(@Nullable ISpellCheckerSession newSession) { mBindGroup.onSessionCreated(newSession, mRequest); } } }