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