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