Create TextServiceManager and SpellCheckerService

Bug: 4176026

This CL inherits https://android-git.corp.google.com/g/112600

Spec of TextServiceManager
- Chooses the most applicable TextService(e.g. SpellCheckerService, WordBreakIteratorService..)
    for each locale

Spec of SpellCheckerService
- Returns whether the given string is a correct word or not
- Returns Suggestions for the given string

Change-Id: Iaa425c7915fe70767ad0b17bf6c6fbcd2a1200b2
diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java
index 8c7e279..76665915 100644
--- a/services/java/com/android/server/SystemServer.java
+++ b/services/java/com/android/server/SystemServer.java
@@ -230,6 +230,7 @@
         WallpaperManagerService wallpaper = null;
         LocationManagerService location = null;
         CountryDetectorService countryDetector = null;
+        TextServicesManagerService tsms = null;
 
         if (factoryTest != SystemServer.FACTORY_TEST_LOW_LEVEL) {
             try {
@@ -273,6 +274,14 @@
             }
 
             try {
+                Slog.i(TAG, "Text Service Manager Service");
+                tsms = new TextServicesManagerService(context);
+                ServiceManager.addService(Context.TEXT_SERVICES_MANAGER_SERVICE, tsms);
+            } catch (Throwable e) {
+                Slog.e(TAG, "Failure starting Text Service Manager Service", e);
+            }
+
+            try {
                 Slog.i(TAG, "NetworkStats Service");
                 networkStats = new NetworkStatsService(context, networkManagement, alarm);
                 ServiceManager.addService(Context.NETWORK_STATS_SERVICE, networkStats);
@@ -538,6 +547,7 @@
         final LocationManagerService locationF = location;
         final CountryDetectorService countryDetectorF = countryDetector;
         final NetworkTimeUpdateService networkTimeUpdaterF = networkTimeUpdater;
+        final TextServicesManagerService textServiceManagerServiceF = tsms;
 
         // We now tell the activity manager it is okay to run third party
         // code.  It will call back into us once it has gotten to the state
@@ -571,6 +581,7 @@
                 if (countryDetectorF != null) countryDetectorF.systemReady();
                 if (throttleF != null) throttleF.systemReady();
                 if (networkTimeUpdaterF != null) networkTimeUpdaterF.systemReady();
+                if (textServiceManagerServiceF != null) textServiceManagerServiceF.systemReady();
             }
         });
 
diff --git a/services/java/com/android/server/TextServicesManagerService.java b/services/java/com/android/server/TextServicesManagerService.java
new file mode 100644
index 0000000..4a0c837
--- /dev/null
+++ b/services/java/com/android/server/TextServicesManagerService.java
@@ -0,0 +1,346 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server;
+
+import com.android.internal.content.PackageMonitor;
+import com.android.internal.textservice.ISpellCheckerService;
+import com.android.internal.textservice.ISpellCheckerSession;
+import com.android.internal.textservice.ISpellCheckerSessionListener;
+import com.android.internal.textservice.ITextServicesManager;
+import com.android.internal.textservice.ITextServicesSessionListener;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.content.pm.ServiceInfo;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.os.SystemClock;
+import android.provider.Settings;
+import android.text.TextUtils;
+import android.service.textservice.SpellCheckerService;
+import android.util.Log;
+import android.util.Slog;
+import android.view.textservice.SpellCheckerInfo;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+
+public class TextServicesManagerService extends ITextServicesManager.Stub {
+    private static final String TAG = TextServicesManagerService.class.getSimpleName();
+    private static final boolean DBG = false;
+
+    private final Context mContext;
+    private boolean mSystemReady;
+    private final TextServicesMonitor mMonitor;
+    private final HashMap<String, SpellCheckerInfo> mSpellCheckerMap =
+            new HashMap<String, SpellCheckerInfo>();
+    private final ArrayList<SpellCheckerInfo> mSpellCheckerList = new ArrayList<SpellCheckerInfo>();
+    private final HashMap<String, SpellCheckerBindGroup> mSpellCheckerBindGroups =
+            new HashMap<String, SpellCheckerBindGroup>();
+
+    public void systemReady() {
+        if (!mSystemReady) {
+            mSystemReady = true;
+        }
+    }
+
+    public TextServicesManagerService(Context context) {
+        mSystemReady = false;
+        mContext = context;
+        mMonitor = new TextServicesMonitor();
+        mMonitor.register(context, true);
+        synchronized (mSpellCheckerMap) {
+            buildSpellCheckerMapLocked(context, mSpellCheckerList, mSpellCheckerMap);
+        }
+    }
+
+    private class TextServicesMonitor extends PackageMonitor {
+        @Override
+        public void onSomePackagesChanged() {
+            synchronized (mSpellCheckerMap) {
+                buildSpellCheckerMapLocked(mContext, mSpellCheckerList, mSpellCheckerMap);
+                // TODO: Update for each locale
+                SpellCheckerInfo sci = getCurrentSpellChecker(null);
+                if (sci == null) {
+                    sci = findAvailSpellCheckerLocked(null, null);
+                    if (sci == null) return;
+                    // Set the current spell checker if there is one or more spell checkers
+                    // available. In this case, "sci" is the first one in the available spell
+                    // checkers.
+                    setCurrentSpellChecker(sci);
+                }
+                final String packageName = sci.getPackageName();
+                final int change = isPackageDisappearing(packageName);
+                if (change == PACKAGE_PERMANENT_CHANGE || change == PACKAGE_TEMPORARY_CHANGE) {
+                    // Package disappearing
+                    setCurrentSpellChecker(findAvailSpellCheckerLocked(null, packageName));
+                } else if (isPackageModified(packageName)) {
+                    // Package modified
+                    setCurrentSpellChecker(findAvailSpellCheckerLocked(null, packageName));
+                }
+            }
+        }
+    }
+
+    private static void buildSpellCheckerMapLocked(Context context,
+            ArrayList<SpellCheckerInfo> list, HashMap<String, SpellCheckerInfo> map) {
+        list.clear();
+        map.clear();
+        final PackageManager pm = context.getPackageManager();
+        List<ResolveInfo> services = pm.queryIntentServices(
+                new Intent(SpellCheckerService.SERVICE_INTERFACE), PackageManager.GET_META_DATA);
+        final int N = services.size();
+        for (int i = 0; i < N; ++i) {
+            final ResolveInfo ri = services.get(i);
+            final ServiceInfo si = ri.serviceInfo;
+            final ComponentName compName = new ComponentName(si.packageName, si.name);
+            if (!android.Manifest.permission.BIND_TEXT_SERVICE.equals(si.permission)) {
+                Slog.w(TAG, "Skipping text service " + compName
+                        + ": it does not require the permission "
+                        + android.Manifest.permission.BIND_TEXT_SERVICE);
+                continue;
+            }
+            if (DBG) Slog.d(TAG, "Add: " + compName);
+            final SpellCheckerInfo sci = new SpellCheckerInfo(context, ri);
+            list.add(sci);
+            map.put(sci.getId(), sci);
+        }
+    }
+
+    // TODO: find an appropriate spell checker for specified locale
+    private SpellCheckerInfo findAvailSpellCheckerLocked(String locale, String prefPackage) {
+        final int spellCheckersCount = mSpellCheckerList.size();
+        if (spellCheckersCount == 0) {
+            Slog.w(TAG, "no available spell checker services found");
+            return null;
+        }
+        if (prefPackage != null) {
+            for (int i = 0; i < spellCheckersCount; ++i) {
+                final SpellCheckerInfo sci = mSpellCheckerList.get(i);
+                if (prefPackage.equals(sci.getPackageName())) {
+                    return sci;
+                }
+            }
+        }
+        if (spellCheckersCount > 1) {
+            Slog.w(TAG, "more than one spell checker service found, picking first");
+        }
+        return mSpellCheckerList.get(0);
+    }
+
+    // TODO: Save SpellCheckerService by supported languages. Currently only one spell
+    // checker is saved.
+    @Override
+    public SpellCheckerInfo getCurrentSpellChecker(String locale) {
+        synchronized (mSpellCheckerMap) {
+            final String curSpellCheckerId =
+                    Settings.Secure.getString(mContext.getContentResolver(),
+                            Settings.Secure.SPELL_CHECKER_SERVICE);
+            if (TextUtils.isEmpty(curSpellCheckerId)) {
+                return null;
+            }
+            return mSpellCheckerMap.get(curSpellCheckerId);
+        }
+    }
+
+    @Override
+    public void getSpellCheckerService(SpellCheckerInfo info, String locale,
+            ITextServicesSessionListener tsListener, ISpellCheckerSessionListener scListener) {
+        if (!mSystemReady) {
+            return;
+        }
+        if (info == null || tsListener == null) {
+            Slog.e(TAG, "getSpellCheckerService: Invalid input.");
+            return;
+        }
+        final String sciId = info.getId();
+        synchronized(mSpellCheckerMap) {
+            if (!mSpellCheckerMap.containsKey(sciId)) {
+                return;
+            }
+            if (mSpellCheckerBindGroups.containsKey(sciId)) {
+                mSpellCheckerBindGroups.get(sciId).addListener(tsListener, locale, scListener);
+                return;
+            }
+            final InternalServiceConnection connection = new InternalServiceConnection(
+                    sciId, locale, scListener);
+            final Intent serviceIntent = new Intent(SpellCheckerService.SERVICE_INTERFACE);
+            serviceIntent.setComponent(info.getComponent());
+            if (!mContext.bindService(serviceIntent, connection, Context.BIND_AUTO_CREATE)) {
+                Slog.e(TAG, "Failed to get a spell checker service.");
+                return;
+            }
+            final SpellCheckerBindGroup group = new SpellCheckerBindGroup(
+                    connection, tsListener, locale, scListener);
+            mSpellCheckerBindGroups.put(sciId, group);
+        }
+        return;
+    }
+
+    @Override
+    public void finishSpellCheckerService(ISpellCheckerSessionListener listener) {
+        synchronized(mSpellCheckerMap) {
+            for (SpellCheckerBindGroup group : mSpellCheckerBindGroups.values()) {
+                if (group == null) continue;
+                group.removeListener(listener);
+            }
+        }
+    }
+
+    private void setCurrentSpellChecker(SpellCheckerInfo sci) {
+        if (sci == null || mSpellCheckerMap.containsKey(sci.getId())) return;
+        Settings.Secure.putString(mContext.getContentResolver(),
+                Settings.Secure.SPELL_CHECKER_SERVICE, sci == null ? "" : sci.getId());
+    }
+
+    // SpellCheckerBindGroup contains active text service session listeners.
+    // If there are no listeners anymore, the SpellCheckerBindGroup instance will be removed from
+    // mSpellCheckerBindGroups
+    private class SpellCheckerBindGroup {
+        final InternalServiceConnection mInternalConnection;
+        final ArrayList<InternalDeathRecipient> mListeners =
+                new ArrayList<InternalDeathRecipient>();
+
+        public SpellCheckerBindGroup(InternalServiceConnection connection,
+                ITextServicesSessionListener listener, String locale,
+                ISpellCheckerSessionListener scListener) {
+            mInternalConnection = connection;
+            addListener(listener, locale, scListener);
+        }
+
+        public void onServiceConnected(ISpellCheckerService spellChecker) {
+            synchronized(mSpellCheckerMap) {
+                for (InternalDeathRecipient listener : mListeners) {
+                    try {
+                        final ISpellCheckerSession session = spellChecker.getISpellCheckerSession(
+                                listener.mScLocale, listener.mScListener);
+                        listener.mTsListener.onServiceConnected(session);
+                    } catch (RemoteException e) {
+                    }
+                }
+            }
+        }
+
+        public void addListener(ITextServicesSessionListener tsListener, String locale,
+                ISpellCheckerSessionListener scListener) {
+            synchronized(mSpellCheckerMap) {
+                try {
+                    final int size = mListeners.size();
+                    for (int i = 0; i < size; ++i) {
+                        if (mListeners.get(i).hasSpellCheckerListener(scListener)) {
+                            // do not add the lister if the group already contains this.
+                            return;
+                        }
+                    }
+                    final InternalDeathRecipient recipient = new InternalDeathRecipient(
+                            this, tsListener, locale, scListener);
+                    scListener.asBinder().linkToDeath(recipient, 0);
+                    mListeners.add(new InternalDeathRecipient(
+                            this, tsListener, locale, scListener));
+                } catch(RemoteException e) {
+                    // do nothing
+                }
+                cleanLocked();
+            }
+        }
+
+        public void removeListener(ISpellCheckerSessionListener listener) {
+            synchronized(mSpellCheckerMap) {
+                final int size = mListeners.size();
+                final ArrayList<InternalDeathRecipient> removeList =
+                        new ArrayList<InternalDeathRecipient>();
+                for (int i = 0; i < size; ++i) {
+                    final InternalDeathRecipient tempRecipient = mListeners.get(i);
+                    if(tempRecipient.hasSpellCheckerListener(listener)) {
+                        removeList.add(tempRecipient);
+                    }
+                }
+                final int removeSize = removeList.size();
+                for (int i = 0; i < removeSize; ++i) {
+                    mListeners.remove(removeList.get(i));
+                }
+                cleanLocked();
+            }
+        }
+
+        private void cleanLocked() {
+            if (mListeners.isEmpty()) {
+                mSpellCheckerBindGroups.remove(this);
+                // Unbind service when there is no active clients.
+                mContext.unbindService(mInternalConnection);
+            }
+        }
+    }
+
+    private class InternalServiceConnection implements ServiceConnection {
+        private final ISpellCheckerSessionListener mListener;
+        private final String mSciId;
+        private final String mLocale;
+        public InternalServiceConnection(
+                String id, String locale, ISpellCheckerSessionListener listener) {
+            mSciId = id;
+            mLocale = locale;
+            mListener = listener;
+        }
+
+        @Override
+        public void onServiceConnected(ComponentName name, IBinder service) {
+            synchronized(mSpellCheckerMap) {
+                ISpellCheckerService spellChecker = ISpellCheckerService.Stub.asInterface(service);
+                final SpellCheckerBindGroup group = mSpellCheckerBindGroups.get(mSciId);
+                if (group != null) {
+                    group.onServiceConnected(spellChecker);
+                }
+            }
+        }
+
+        @Override
+        public void onServiceDisconnected(ComponentName name) {
+            mSpellCheckerBindGroups.remove(mSciId);
+        }
+    }
+
+    private class InternalDeathRecipient implements IBinder.DeathRecipient {
+        public final ITextServicesSessionListener mTsListener;
+        public final ISpellCheckerSessionListener mScListener;
+        public final String mScLocale;
+        private final SpellCheckerBindGroup mGroup;
+        public InternalDeathRecipient(SpellCheckerBindGroup group,
+                ITextServicesSessionListener tsListener, String scLocale,
+                ISpellCheckerSessionListener scListener) {
+            mTsListener = tsListener;
+            mScListener = scListener;
+            mScLocale = scLocale;
+            mGroup = group;
+        }
+
+        public boolean hasSpellCheckerListener(ISpellCheckerSessionListener listener) {
+            return mScListener.equals(listener);
+        }
+
+        @Override
+        public void binderDied() {
+            mGroup.removeListener(mScListener);
+        }
+    }
+}