android_frameworks_base/services/java/com/android/server/TextServicesManagerService.java
Svetoslav Ganov a00271533f Refactoring of the print sub-system and API clean up.
1. Now a user state has ins own spooler since the spooler app is
   running per user. The user state registers an observer for the state
   of the spooler to get information needed to orchestrate unbinding
   from print serivces that have no work and eventually unbinding from
   the spooler when all no service has any work.

2. Abstracted a remote print service from the perspective of the system
   in a class that is transparently managing binding and unbinding to
   the remote instance.

3. Abstracted the remote print spooler to transparently manage binding
   and unbinding to the remote instance when there is work and when
   there is no work, respectively.

4. Cleaned up the print document adapter (ex-PrintAdapter) APIs to
   enable implementing the all callbacks on a thread of choice. If
   the document is really small, using the main thread makes sense.

   Now if an app that does not need the UI state to layout the printed
   content, it can schedule all the work for allocating resources, laying
   out, writing, and releasing resources on a dedicated thread.

5. Added info class for the printed document that is now propagated
   the the print services. A print service gets an instance of a
   new document class that encapsulates the document info and a method
   to access the document's data.

6. Added APIs for describing the type of a document to the new document
   info class. This allows a print service to do smarts based on the
   doc type. For now we have only photo and document types.

7. Renamed the systemReady method for system services that implement
   it with different semantics to systemRunning. Such methods assume
   the the service can run third-party code which is not the same as
   systemReady.

8. Cleaned up the print job configuration activity.

9. Sigh... code clean up here and there. Factoring out classes to
   improve readability.

Change-Id: I637ba28412793166cbf519273fdf022241159a92
2013-07-16 12:59:59 -07:00

982 lines
41 KiB
Java

/*
* 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 org.xmlpull.v1.XmlPullParserException;
import android.app.ActivityManagerNative;
import android.app.AppGlobals;
import android.app.IUserSwitchObserver;
import android.content.ComponentName;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.content.pm.IPackageManager;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.content.pm.ServiceInfo;
import android.os.Binder;
import android.os.Bundle;
import android.os.IBinder;
import android.os.IRemoteCallback;
import android.os.Process;
import android.os.RemoteException;
import android.os.UserHandle;
import android.provider.Settings;
import android.service.textservice.SpellCheckerService;
import android.text.TextUtils;
import android.util.Slog;
import android.view.inputmethod.InputMethodManager;
import android.view.inputmethod.InputMethodSubtype;
import android.view.textservice.SpellCheckerInfo;
import android.view.textservice.SpellCheckerSubtype;
import java.io.FileDescriptor;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CopyOnWriteArrayList;
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>();
private final TextServicesSettings mSettings;
public void systemRunning() {
if (!mSystemReady) {
mSystemReady = true;
}
}
public TextServicesManagerService(Context context) {
mSystemReady = false;
mContext = context;
int userId = UserHandle.USER_OWNER;
try {
ActivityManagerNative.getDefault().registerUserSwitchObserver(
new IUserSwitchObserver.Stub() {
@Override
public void onUserSwitching(int newUserId, IRemoteCallback reply) {
synchronized(mSpellCheckerMap) {
switchUserLocked(newUserId);
}
if (reply != null) {
try {
reply.sendResult(null);
} catch (RemoteException e) {
}
}
}
@Override
public void onUserSwitchComplete(int newUserId) throws RemoteException {
}
});
userId = ActivityManagerNative.getDefault().getCurrentUser().id;
} catch (RemoteException e) {
Slog.w(TAG, "Couldn't get current user ID; guessing it's 0", e);
}
mMonitor = new TextServicesMonitor();
mMonitor.register(context, null, true);
mSettings = new TextServicesSettings(context.getContentResolver(), userId);
// "switchUserLocked" initializes the states for the foreground user
switchUserLocked(userId);
}
private void switchUserLocked(int userId) {
mSettings.setCurrentUserId(userId);
unbindServiceLocked();
buildSpellCheckerMapLocked(mContext, mSpellCheckerList, mSpellCheckerMap, mSettings);
SpellCheckerInfo sci = getCurrentSpellChecker(null);
if (sci == null) {
sci = findAvailSpellCheckerLocked(null, null);
if (sci != null) {
// 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.
setCurrentSpellCheckerLocked(sci.getId());
}
}
}
private class TextServicesMonitor extends PackageMonitor {
private boolean isChangingPackagesOfCurrentUser() {
final int userId = getChangingUserId();
final boolean retval = userId == mSettings.getCurrentUserId();
if (DBG) {
Slog.d(TAG, "--- ignore this call back from a background user: " + userId);
}
return retval;
}
@Override
public void onSomePackagesChanged() {
if (!isChangingPackagesOfCurrentUser()) {
return;
}
synchronized (mSpellCheckerMap) {
buildSpellCheckerMapLocked(
mContext, mSpellCheckerList, mSpellCheckerMap, mSettings);
// TODO: Update for each locale
SpellCheckerInfo sci = getCurrentSpellChecker(null);
if (sci == null) return;
final String packageName = sci.getPackageName();
final int change = isPackageDisappearing(packageName);
if (// Package disappearing
change == PACKAGE_PERMANENT_CHANGE || change == PACKAGE_TEMPORARY_CHANGE
// Package modified
|| isPackageModified(packageName)) {
sci = findAvailSpellCheckerLocked(null, packageName);
if (sci != null) {
setCurrentSpellCheckerLocked(sci.getId());
}
}
}
}
}
private static void buildSpellCheckerMapLocked(Context context,
ArrayList<SpellCheckerInfo> list, HashMap<String, SpellCheckerInfo> map,
TextServicesSettings settings) {
list.clear();
map.clear();
final PackageManager pm = context.getPackageManager();
final List<ResolveInfo> services = pm.queryIntentServicesAsUser(
new Intent(SpellCheckerService.SERVICE_INTERFACE), PackageManager.GET_META_DATA,
settings.getCurrentUserId());
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);
try {
final SpellCheckerInfo sci = new SpellCheckerInfo(context, ri);
if (sci.getSubtypeCount() <= 0) {
Slog.w(TAG, "Skipping text service " + compName
+ ": it does not contain subtypes.");
continue;
}
list.add(sci);
map.put(sci.getId(), sci);
} catch (XmlPullParserException e) {
Slog.w(TAG, "Unable to load the spell checker " + compName, e);
} catch (IOException e) {
Slog.w(TAG, "Unable to load the spell checker " + compName, e);
}
}
if (DBG) {
Slog.d(TAG, "buildSpellCheckerMapLocked: " + list.size() + "," + map.size());
}
}
// ---------------------------------------------------------------------------------------
// Check whether or not this is a valid IPC. Assumes an IPC is valid when either
// 1) it comes from the system process
// 2) the calling process' user id is identical to the current user id TSMS thinks.
private boolean calledFromValidUser() {
final int uid = Binder.getCallingUid();
final int userId = UserHandle.getUserId(uid);
if (DBG) {
Slog.d(TAG, "--- calledFromForegroundUserOrSystemProcess ? "
+ "calling uid = " + uid + " system uid = " + Process.SYSTEM_UID
+ " calling userId = " + userId + ", foreground user id = "
+ mSettings.getCurrentUserId());
try {
final String[] packageNames = AppGlobals.getPackageManager().getPackagesForUid(uid);
for (int i = 0; i < packageNames.length; ++i) {
if (DBG) {
Slog.d(TAG, "--- process name for "+ uid + " = " + packageNames[i]);
}
}
} catch (RemoteException e) {
}
}
if (uid == Process.SYSTEM_UID || userId == mSettings.getCurrentUserId()) {
return true;
} else {
Slog.w(TAG, "--- IPC called from background users. Ignore. \n" + getStackTrace());
return false;
}
}
private boolean bindCurrentSpellCheckerService(
Intent service, ServiceConnection conn, int flags) {
if (service == null || conn == null) {
Slog.e(TAG, "--- bind failed: service = " + service + ", conn = " + conn);
return false;
}
return mContext.bindServiceAsUser(service, conn, flags,
new UserHandle(mSettings.getCurrentUserId()));
}
private void unbindServiceLocked() {
for (SpellCheckerBindGroup scbg : mSpellCheckerBindGroups.values()) {
scbg.removeAll();
}
mSpellCheckerBindGroups.clear();
}
// 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())) {
if (DBG) {
Slog.d(TAG, "findAvailSpellCheckerLocked: " + 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) {
// TODO: Make this work even for non-current users?
if (!calledFromValidUser()) {
return null;
}
synchronized (mSpellCheckerMap) {
final String curSpellCheckerId = mSettings.getSelectedSpellChecker();
if (DBG) {
Slog.w(TAG, "getCurrentSpellChecker: " + curSpellCheckerId);
}
if (TextUtils.isEmpty(curSpellCheckerId)) {
return null;
}
return mSpellCheckerMap.get(curSpellCheckerId);
}
}
// TODO: Respect allowImplicitlySelectedSubtype
// TODO: Save SpellCheckerSubtype by supported languages by looking at "locale".
@Override
public SpellCheckerSubtype getCurrentSpellCheckerSubtype(
String locale, boolean allowImplicitlySelectedSubtype) {
// TODO: Make this work even for non-current users?
if (!calledFromValidUser()) {
return null;
}
synchronized (mSpellCheckerMap) {
final String subtypeHashCodeStr = mSettings.getSelectedSpellCheckerSubtype();
if (DBG) {
Slog.w(TAG, "getCurrentSpellCheckerSubtype: " + subtypeHashCodeStr);
}
final SpellCheckerInfo sci = getCurrentSpellChecker(null);
if (sci == null || sci.getSubtypeCount() == 0) {
if (DBG) {
Slog.w(TAG, "Subtype not found.");
}
return null;
}
final int hashCode;
if (!TextUtils.isEmpty(subtypeHashCodeStr)) {
hashCode = Integer.valueOf(subtypeHashCodeStr);
} else {
hashCode = 0;
}
if (hashCode == 0 && !allowImplicitlySelectedSubtype) {
return null;
}
String candidateLocale = null;
if (hashCode == 0) {
// Spell checker language settings == "auto"
final InputMethodManager imm =
(InputMethodManager)mContext.getSystemService(Context.INPUT_METHOD_SERVICE);
if (imm != null) {
final InputMethodSubtype currentInputMethodSubtype =
imm.getCurrentInputMethodSubtype();
if (currentInputMethodSubtype != null) {
final String localeString = currentInputMethodSubtype.getLocale();
if (!TextUtils.isEmpty(localeString)) {
// 1. Use keyboard locale if available in the spell checker
candidateLocale = localeString;
}
}
}
if (candidateLocale == null) {
// 2. Use System locale if available in the spell checker
candidateLocale = mContext.getResources().getConfiguration().locale.toString();
}
}
SpellCheckerSubtype candidate = null;
for (int i = 0; i < sci.getSubtypeCount(); ++i) {
final SpellCheckerSubtype scs = sci.getSubtypeAt(i);
if (hashCode == 0) {
final String scsLocale = scs.getLocale();
if (candidateLocale.equals(scsLocale)) {
return scs;
} else if (candidate == null) {
if (candidateLocale.length() >= 2 && scsLocale.length() >= 2
&& candidateLocale.startsWith(scsLocale)) {
// Fall back to the applicable language
candidate = scs;
}
}
} else if (scs.hashCode() == hashCode) {
if (DBG) {
Slog.w(TAG, "Return subtype " + scs.hashCode() + ", input= " + locale
+ ", " + scs.getLocale());
}
// 3. Use the user specified spell check language
return scs;
}
}
// 4. Fall back to the applicable language and return it if not null
// 5. Simply just return it even if it's null which means we could find no suitable
// spell check languages
return candidate;
}
}
@Override
public void getSpellCheckerService(String sciId, String locale,
ITextServicesSessionListener tsListener, ISpellCheckerSessionListener scListener,
Bundle bundle) {
if (!calledFromValidUser()) {
return;
}
if (!mSystemReady) {
return;
}
if (TextUtils.isEmpty(sciId) || tsListener == null || scListener == null) {
Slog.e(TAG, "getSpellCheckerService: Invalid input.");
return;
}
synchronized(mSpellCheckerMap) {
if (!mSpellCheckerMap.containsKey(sciId)) {
return;
}
final SpellCheckerInfo sci = mSpellCheckerMap.get(sciId);
final int uid = Binder.getCallingUid();
if (mSpellCheckerBindGroups.containsKey(sciId)) {
final SpellCheckerBindGroup bindGroup = mSpellCheckerBindGroups.get(sciId);
if (bindGroup != null) {
final InternalDeathRecipient recipient =
mSpellCheckerBindGroups.get(sciId).addListener(
tsListener, locale, scListener, uid, bundle);
if (recipient == null) {
if (DBG) {
Slog.w(TAG, "Didn't create a death recipient.");
}
return;
}
if (bindGroup.mSpellChecker == null & bindGroup.mConnected) {
Slog.e(TAG, "The state of the spell checker bind group is illegal.");
bindGroup.removeAll();
} else if (bindGroup.mSpellChecker != null) {
if (DBG) {
Slog.w(TAG, "Existing bind found. Return a spell checker session now. "
+ "Listeners count = " + bindGroup.mListeners.size());
}
try {
final ISpellCheckerSession session =
bindGroup.mSpellChecker.getISpellCheckerSession(
recipient.mScLocale, recipient.mScListener, bundle);
if (session != null) {
tsListener.onServiceConnected(session);
return;
} else {
if (DBG) {
Slog.w(TAG, "Existing bind already expired. ");
}
bindGroup.removeAll();
}
} catch (RemoteException e) {
Slog.e(TAG, "Exception in getting spell checker session: " + e);
bindGroup.removeAll();
}
}
}
}
final long ident = Binder.clearCallingIdentity();
try {
startSpellCheckerServiceInnerLocked(
sci, locale, tsListener, scListener, uid, bundle);
} finally {
Binder.restoreCallingIdentity(ident);
}
}
return;
}
@Override
public boolean isSpellCheckerEnabled() {
if (!calledFromValidUser()) {
return false;
}
synchronized(mSpellCheckerMap) {
return isSpellCheckerEnabledLocked();
}
}
private void startSpellCheckerServiceInnerLocked(SpellCheckerInfo info, String locale,
ITextServicesSessionListener tsListener, ISpellCheckerSessionListener scListener,
int uid, Bundle bundle) {
if (DBG) {
Slog.w(TAG, "Start spell checker session inner locked.");
}
final String sciId = info.getId();
final InternalServiceConnection connection = new InternalServiceConnection(
sciId, locale, bundle);
final Intent serviceIntent = new Intent(SpellCheckerService.SERVICE_INTERFACE);
serviceIntent.setComponent(info.getComponent());
if (DBG) {
Slog.w(TAG, "bind service: " + info.getId());
}
if (!bindCurrentSpellCheckerService(serviceIntent, connection, Context.BIND_AUTO_CREATE)) {
Slog.e(TAG, "Failed to get a spell checker service.");
return;
}
final SpellCheckerBindGroup group = new SpellCheckerBindGroup(
connection, tsListener, locale, scListener, uid, bundle);
mSpellCheckerBindGroups.put(sciId, group);
}
@Override
public SpellCheckerInfo[] getEnabledSpellCheckers() {
// TODO: Make this work even for non-current users?
if (!calledFromValidUser()) {
return null;
}
if (DBG) {
Slog.d(TAG, "getEnabledSpellCheckers: " + mSpellCheckerList.size());
for (int i = 0; i < mSpellCheckerList.size(); ++i) {
Slog.d(TAG, "EnabledSpellCheckers: " + mSpellCheckerList.get(i).getPackageName());
}
}
return mSpellCheckerList.toArray(new SpellCheckerInfo[mSpellCheckerList.size()]);
}
@Override
public void finishSpellCheckerService(ISpellCheckerSessionListener listener) {
if (!calledFromValidUser()) {
return;
}
if (DBG) {
Slog.d(TAG, "FinishSpellCheckerService");
}
synchronized(mSpellCheckerMap) {
final ArrayList<SpellCheckerBindGroup> removeList =
new ArrayList<SpellCheckerBindGroup>();
for (SpellCheckerBindGroup group : mSpellCheckerBindGroups.values()) {
if (group == null) continue;
// Use removeList to avoid modifying mSpellCheckerBindGroups in this loop.
removeList.add(group);
}
final int removeSize = removeList.size();
for (int i = 0; i < removeSize; ++i) {
removeList.get(i).removeListener(listener);
}
}
}
@Override
public void setCurrentSpellChecker(String locale, String sciId) {
if (!calledFromValidUser()) {
return;
}
synchronized(mSpellCheckerMap) {
if (mContext.checkCallingOrSelfPermission(
android.Manifest.permission.WRITE_SECURE_SETTINGS)
!= PackageManager.PERMISSION_GRANTED) {
throw new SecurityException(
"Requires permission "
+ android.Manifest.permission.WRITE_SECURE_SETTINGS);
}
setCurrentSpellCheckerLocked(sciId);
}
}
@Override
public void setCurrentSpellCheckerSubtype(String locale, int hashCode) {
if (!calledFromValidUser()) {
return;
}
synchronized(mSpellCheckerMap) {
if (mContext.checkCallingOrSelfPermission(
android.Manifest.permission.WRITE_SECURE_SETTINGS)
!= PackageManager.PERMISSION_GRANTED) {
throw new SecurityException(
"Requires permission "
+ android.Manifest.permission.WRITE_SECURE_SETTINGS);
}
setCurrentSpellCheckerSubtypeLocked(hashCode);
}
}
@Override
public void setSpellCheckerEnabled(boolean enabled) {
if (!calledFromValidUser()) {
return;
}
synchronized(mSpellCheckerMap) {
if (mContext.checkCallingOrSelfPermission(
android.Manifest.permission.WRITE_SECURE_SETTINGS)
!= PackageManager.PERMISSION_GRANTED) {
throw new SecurityException(
"Requires permission "
+ android.Manifest.permission.WRITE_SECURE_SETTINGS);
}
setSpellCheckerEnabledLocked(enabled);
}
}
private void setCurrentSpellCheckerLocked(String sciId) {
if (DBG) {
Slog.w(TAG, "setCurrentSpellChecker: " + sciId);
}
if (TextUtils.isEmpty(sciId) || !mSpellCheckerMap.containsKey(sciId)) return;
final SpellCheckerInfo currentSci = getCurrentSpellChecker(null);
if (currentSci != null && currentSci.getId().equals(sciId)) {
// Do nothing if the current spell checker is same as new spell checker.
return;
}
final long ident = Binder.clearCallingIdentity();
try {
mSettings.putSelectedSpellChecker(sciId);
setCurrentSpellCheckerSubtypeLocked(0);
} finally {
Binder.restoreCallingIdentity(ident);
}
}
private void setCurrentSpellCheckerSubtypeLocked(int hashCode) {
if (DBG) {
Slog.w(TAG, "setCurrentSpellCheckerSubtype: " + hashCode);
}
final SpellCheckerInfo sci = getCurrentSpellChecker(null);
int tempHashCode = 0;
for (int i = 0; sci != null && i < sci.getSubtypeCount(); ++i) {
if(sci.getSubtypeAt(i).hashCode() == hashCode) {
tempHashCode = hashCode;
break;
}
}
final long ident = Binder.clearCallingIdentity();
try {
mSettings.putSelectedSpellCheckerSubtype(tempHashCode);
} finally {
Binder.restoreCallingIdentity(ident);
}
}
private void setSpellCheckerEnabledLocked(boolean enabled) {
if (DBG) {
Slog.w(TAG, "setSpellCheckerEnabled: " + enabled);
}
final long ident = Binder.clearCallingIdentity();
try {
mSettings.setSpellCheckerEnabled(enabled);
} finally {
Binder.restoreCallingIdentity(ident);
}
}
private boolean isSpellCheckerEnabledLocked() {
final long ident = Binder.clearCallingIdentity();
try {
final boolean retval = mSettings.isSpellCheckerEnabled();
if (DBG) {
Slog.w(TAG, "getSpellCheckerEnabled: " + retval);
}
return retval;
} finally {
Binder.restoreCallingIdentity(ident);
}
}
@Override
protected void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
if (mContext.checkCallingOrSelfPermission(android.Manifest.permission.DUMP)
!= PackageManager.PERMISSION_GRANTED) {
pw.println("Permission Denial: can't dump TextServicesManagerService from from pid="
+ Binder.getCallingPid()
+ ", uid=" + Binder.getCallingUid());
return;
}
synchronized(mSpellCheckerMap) {
pw.println("Current Text Services Manager state:");
pw.println(" Spell Checker Map:");
for (Map.Entry<String, SpellCheckerInfo> ent : mSpellCheckerMap.entrySet()) {
pw.print(" "); pw.print(ent.getKey()); pw.println(":");
SpellCheckerInfo info = ent.getValue();
pw.print(" "); pw.print("id="); pw.println(info.getId());
pw.print(" "); pw.print("comp=");
pw.println(info.getComponent().toShortString());
int NS = info.getSubtypeCount();
for (int i=0; i<NS; i++) {
SpellCheckerSubtype st = info.getSubtypeAt(i);
pw.print(" "); pw.print("Subtype #"); pw.print(i); pw.println(":");
pw.print(" "); pw.print("locale="); pw.println(st.getLocale());
pw.print(" "); pw.print("extraValue=");
pw.println(st.getExtraValue());
}
}
pw.println("");
pw.println(" Spell Checker Bind Groups:");
for (Map.Entry<String, SpellCheckerBindGroup> ent
: mSpellCheckerBindGroups.entrySet()) {
SpellCheckerBindGroup grp = ent.getValue();
pw.print(" "); pw.print(ent.getKey()); pw.print(" ");
pw.print(grp); pw.println(":");
pw.print(" "); pw.print("mInternalConnection=");
pw.println(grp.mInternalConnection);
pw.print(" "); pw.print("mSpellChecker=");
pw.println(grp.mSpellChecker);
pw.print(" "); pw.print("mBound="); pw.print(grp.mBound);
pw.print(" mConnected="); pw.println(grp.mConnected);
int NL = grp.mListeners.size();
for (int i=0; i<NL; i++) {
InternalDeathRecipient listener = grp.mListeners.get(i);
pw.print(" "); pw.print("Listener #"); pw.print(i); pw.println(":");
pw.print(" "); pw.print("mTsListener=");
pw.println(listener.mTsListener);
pw.print(" "); pw.print("mScListener=");
pw.println(listener.mScListener);
pw.print(" "); pw.print("mGroup=");
pw.println(listener.mGroup);
pw.print(" "); pw.print("mScLocale=");
pw.print(listener.mScLocale);
pw.print(" mUid="); pw.println(listener.mUid);
}
}
}
}
// SpellCheckerBindGroup contains active text service session listeners.
// If there are no listeners anymore, the SpellCheckerBindGroup instance will be removed from
// mSpellCheckerBindGroups
private class SpellCheckerBindGroup {
private final String TAG = SpellCheckerBindGroup.class.getSimpleName();
private final InternalServiceConnection mInternalConnection;
private final CopyOnWriteArrayList<InternalDeathRecipient> mListeners =
new CopyOnWriteArrayList<InternalDeathRecipient>();
public boolean mBound;
public ISpellCheckerService mSpellChecker;
public boolean mConnected;
public SpellCheckerBindGroup(InternalServiceConnection connection,
ITextServicesSessionListener listener, String locale,
ISpellCheckerSessionListener scListener, int uid, Bundle bundle) {
mInternalConnection = connection;
mBound = true;
mConnected = false;
addListener(listener, locale, scListener, uid, bundle);
}
public void onServiceConnected(ISpellCheckerService spellChecker) {
if (DBG) {
Slog.d(TAG, "onServiceConnected");
}
for (InternalDeathRecipient listener : mListeners) {
try {
final ISpellCheckerSession session = spellChecker.getISpellCheckerSession(
listener.mScLocale, listener.mScListener, listener.mBundle);
synchronized(mSpellCheckerMap) {
if (mListeners.contains(listener)) {
listener.mTsListener.onServiceConnected(session);
}
}
} catch (RemoteException e) {
Slog.e(TAG, "Exception in getting the spell checker session."
+ "Reconnect to the spellchecker. ", e);
removeAll();
return;
}
}
synchronized(mSpellCheckerMap) {
mSpellChecker = spellChecker;
mConnected = true;
}
}
public InternalDeathRecipient addListener(ITextServicesSessionListener tsListener,
String locale, ISpellCheckerSessionListener scListener, int uid, Bundle bundle) {
if (DBG) {
Slog.d(TAG, "addListener: " + locale);
}
InternalDeathRecipient recipient = null;
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 null;
}
}
recipient = new InternalDeathRecipient(
this, tsListener, locale, scListener, uid, bundle);
scListener.asBinder().linkToDeath(recipient, 0);
mListeners.add(recipient);
} catch(RemoteException e) {
// do nothing
}
cleanLocked();
}
return recipient;
}
public void removeListener(ISpellCheckerSessionListener listener) {
if (DBG) {
Slog.w(TAG, "remove listener: " + listener.hashCode());
}
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)) {
if (DBG) {
Slog.w(TAG, "found existing listener.");
}
removeList.add(tempRecipient);
}
}
final int removeSize = removeList.size();
for (int i = 0; i < removeSize; ++i) {
if (DBG) {
Slog.w(TAG, "Remove " + removeList.get(i));
}
final InternalDeathRecipient idr = removeList.get(i);
idr.mScListener.asBinder().unlinkToDeath(idr, 0);
mListeners.remove(idr);
}
cleanLocked();
}
}
// cleanLocked may remove elements from mSpellCheckerBindGroups
private void cleanLocked() {
if (DBG) {
Slog.d(TAG, "cleanLocked");
}
// If there are no more active listeners, clean up. Only do this
// once.
if (mBound && mListeners.isEmpty()) {
mBound = false;
final String sciId = mInternalConnection.mSciId;
SpellCheckerBindGroup cur = mSpellCheckerBindGroups.get(sciId);
if (cur == this) {
if (DBG) {
Slog.d(TAG, "Remove bind group.");
}
mSpellCheckerBindGroups.remove(sciId);
}
mContext.unbindService(mInternalConnection);
}
}
public void removeAll() {
Slog.e(TAG, "Remove the spell checker bind unexpectedly.");
synchronized(mSpellCheckerMap) {
final int size = mListeners.size();
for (int i = 0; i < size; ++i) {
final InternalDeathRecipient idr = mListeners.get(i);
idr.mScListener.asBinder().unlinkToDeath(idr, 0);
}
mListeners.clear();
cleanLocked();
}
}
}
private class InternalServiceConnection implements ServiceConnection {
private final String mSciId;
private final String mLocale;
private final Bundle mBundle;
public InternalServiceConnection(
String id, String locale, Bundle bundle) {
mSciId = id;
mLocale = locale;
mBundle = bundle;
}
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
synchronized(mSpellCheckerMap) {
onServiceConnectedInnerLocked(name, service);
}
}
private void onServiceConnectedInnerLocked(ComponentName name, IBinder service) {
if (DBG) {
Slog.w(TAG, "onServiceConnected: " + name);
}
final ISpellCheckerService spellChecker =
ISpellCheckerService.Stub.asInterface(service);
final SpellCheckerBindGroup group = mSpellCheckerBindGroups.get(mSciId);
if (group != null && this == group.mInternalConnection) {
group.onServiceConnected(spellChecker);
}
}
@Override
public void onServiceDisconnected(ComponentName name) {
synchronized(mSpellCheckerMap) {
final SpellCheckerBindGroup group = mSpellCheckerBindGroups.get(mSciId);
if (group != null && this == group.mInternalConnection) {
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 final int mUid;
public final Bundle mBundle;
public InternalDeathRecipient(SpellCheckerBindGroup group,
ITextServicesSessionListener tsListener, String scLocale,
ISpellCheckerSessionListener scListener, int uid, Bundle bundle) {
mTsListener = tsListener;
mScListener = scListener;
mScLocale = scLocale;
mGroup = group;
mUid = uid;
mBundle = bundle;
}
public boolean hasSpellCheckerListener(ISpellCheckerSessionListener listener) {
return listener.asBinder().equals(mScListener.asBinder());
}
@Override
public void binderDied() {
mGroup.removeListener(mScListener);
}
}
private static class TextServicesSettings {
private final ContentResolver mResolver;
private int mCurrentUserId;
public TextServicesSettings(ContentResolver resolver, int userId) {
mResolver = resolver;
mCurrentUserId = userId;
}
public void setCurrentUserId(int userId) {
if (DBG) {
Slog.d(TAG, "--- Swtich the current user from " + mCurrentUserId + " to "
+ userId + ", new ime = " + getSelectedSpellChecker());
}
// TSMS settings are kept per user, so keep track of current user
mCurrentUserId = userId;
}
public int getCurrentUserId() {
return mCurrentUserId;
}
public void putSelectedSpellChecker(String sciId) {
Settings.Secure.putStringForUser(mResolver,
Settings.Secure.SELECTED_SPELL_CHECKER, sciId, mCurrentUserId);
}
public void putSelectedSpellCheckerSubtype(int hashCode) {
Settings.Secure.putStringForUser(mResolver,
Settings.Secure.SELECTED_SPELL_CHECKER_SUBTYPE, String.valueOf(hashCode),
mCurrentUserId);
}
public void setSpellCheckerEnabled(boolean enabled) {
Settings.Secure.putIntForUser(mResolver,
Settings.Secure.SPELL_CHECKER_ENABLED, enabled ? 1 : 0, mCurrentUserId);
}
public String getSelectedSpellChecker() {
return Settings.Secure.getStringForUser(mResolver,
Settings.Secure.SELECTED_SPELL_CHECKER, mCurrentUserId);
}
public String getSelectedSpellCheckerSubtype() {
return Settings.Secure.getStringForUser(mResolver,
Settings.Secure.SELECTED_SPELL_CHECKER_SUBTYPE, mCurrentUserId);
}
public boolean isSpellCheckerEnabled() {
return Settings.Secure.getIntForUser(mResolver,
Settings.Secure.SPELL_CHECKER_ENABLED, 1, mCurrentUserId) == 1;
}
}
// ----------------------------------------------------------------------
// Utilities for debug
private static String getStackTrace() {
final StringBuilder sb = new StringBuilder();
try {
throw new RuntimeException();
} catch (RuntimeException e) {
final StackTraceElement[] frames = e.getStackTrace();
// Start at 1 because the first frame is here and we don't care about it
for (int j = 1; j < frames.length; ++j) {
sb.append(frames[j].toString() + "\n");
}
}
return sb.toString();
}
}