Merge "Avoid sync IPCs from TSMS to SpellCheckerService"

This commit is contained in:
Yohei Yukawa
2017-02-09 05:11:01 +00:00
committed by Android (Google) Code Review
6 changed files with 260 additions and 147 deletions

View File

@@ -370,6 +370,7 @@ LOCAL_SRC_FILES += \
core/java/com/android/internal/statusbar/IStatusBar.aidl \
core/java/com/android/internal/statusbar/IStatusBarService.aidl \
core/java/com/android/internal/textservice/ISpellCheckerService.aidl \
core/java/com/android/internal/textservice/ISpellCheckerServiceCallback.aidl \
core/java/com/android/internal/textservice/ISpellCheckerSession.aidl \
core/java/com/android/internal/textservice/ISpellCheckerSessionListener.aidl \
core/java/com/android/internal/textservice/ITextServicesManager.aidl \

View File

@@ -17,6 +17,7 @@
package android.service.textservice;
import com.android.internal.textservice.ISpellCheckerService;
import com.android.internal.textservice.ISpellCheckerServiceCallback;
import com.android.internal.textservice.ISpellCheckerSession;
import com.android.internal.textservice.ISpellCheckerSessionListener;
@@ -311,16 +312,39 @@ public abstract class SpellCheckerService extends Service {
mInternalServiceRef = new WeakReference<SpellCheckerService>(service);
}
/**
* Called from the system when an application is requesting a new spell checker session.
*
* <p>Note: This is an internal protocol used by the system to establish spell checker
* sessions, which is not guaranteed to be stable and is subject to change.</p>
*
* @param locale locale to be returned from {@link Session#getLocale()}
* @param listener IPC channel object to be used to implement
* {@link Session#onGetSuggestionsMultiple(TextInfo[], int, boolean)} and
* {@link Session#onGetSuggestions(TextInfo, int)}
* @param bundle bundle to be returned from {@link Session#getBundle()}
* @param callback IPC channel to return the result to the caller in an asynchronous manner
*/
@Override
public ISpellCheckerSession getISpellCheckerSession(
String locale, ISpellCheckerSessionListener listener, Bundle bundle) {
public void getISpellCheckerSession(
String locale, ISpellCheckerSessionListener listener, Bundle bundle,
ISpellCheckerServiceCallback callback) {
final SpellCheckerService service = mInternalServiceRef.get();
if (service == null) return null;
final Session session = service.createSession();
final InternalISpellCheckerSession internalSession =
new InternalISpellCheckerSession(locale, listener, bundle, session);
session.onCreate();
return internalSession;
final InternalISpellCheckerSession internalSession;
if (service == null) {
// If the owner SpellCheckerService object was already destroyed and got GC-ed,
// the weak-reference returns null and we should just ignore this request.
internalSession = null;
} else {
final Session session = service.createSession();
internalSession =
new InternalISpellCheckerSession(locale, listener, bundle, session);
session.onCreate();
}
try {
callback.onSessionCreated(internalSession);
} catch (RemoteException e) {
}
}
}

View File

@@ -16,16 +16,32 @@
package com.android.internal.textservice;
import com.android.internal.textservice.ISpellCheckerServiceCallback;
import com.android.internal.textservice.ISpellCheckerSession;
import com.android.internal.textservice.ISpellCheckerSessionListener;
import android.os.Bundle;
/**
* Public interface to the global spell checker.
* IPC channels from TextServicesManagerService to SpellCheckerService.
* @hide
*/
interface ISpellCheckerService {
ISpellCheckerSession getISpellCheckerSession(
String locale, ISpellCheckerSessionListener listener, in Bundle bundle);
oneway interface ISpellCheckerService {
/**
* Called from the system when an application is requesting a new spell checker session.
*
* <p>Note: This is an internal protocol used by the system to establish spell checker sessions,
* which is not guaranteed to be stable and is subject to change.</p>
*
* @param locale locale to be returned from
* {@link android.service.textservice.SpellCheckerService.Session#getLocale()}
* @param listener IPC channel object to be used to implement
* {@link android.service.textservice.SpellCheckerService.Session#onGetSuggestionsMultiple(TextInfo[], int, boolean)} and
* {@link android.service.textservice.SpellCheckerService.Session#onGetSuggestions(TextInfo, int)}
* @param bundle bundle to be returned from {@link android.service.textservice.SpellCheckerService.Session#getBundle()}
* @param callback IPC channel to return the result to the caller in an asynchronous manner
*/
void getISpellCheckerSession(
String locale, ISpellCheckerSessionListener listener, in Bundle bundle,
ISpellCheckerServiceCallback callback);
}

View File

@@ -0,0 +1,35 @@
/*
* Copyright (C) 2017 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.internal.textservice;
import com.android.internal.textservice.ISpellCheckerSession;
import com.android.internal.textservice.ISpellCheckerSessionListener;
import android.os.Bundle;
/**
* IPC channels from SpellCheckerService to TextServicesManagerService.
* @hide
*/
oneway interface ISpellCheckerServiceCallback {
// TODO: Currently SpellCheckerSession just ignores null newSession and continues waiting for
// the next onSessionCreated with non-null newSession, which is supposed to never happen if
// the system is working normally. We should at least free up resources in SpellCheckerSession.
// Note: This method is called from non-system processes, in theory we cannot assume that
// this method is always be called only once with non-null value.
void onSessionCreated(ISpellCheckerSession newSession);
}

View File

@@ -21,7 +21,7 @@ import com.android.internal.textservice.ISpellCheckerSession;
import android.view.textservice.SpellCheckerInfo;
/**
* Interface to the text service session.
* (Per-session) IPC channels from TextServicesManagerService to spell checker client applications.
* @hide
*/
interface ITextServicesSessionListener {

View File

@@ -20,6 +20,7 @@ import com.android.internal.annotations.GuardedBy;
import com.android.internal.content.PackageMonitor;
import com.android.internal.inputmethod.InputMethodUtils;
import com.android.internal.textservice.ISpellCheckerService;
import com.android.internal.textservice.ISpellCheckerServiceCallback;
import com.android.internal.textservice.ISpellCheckerSession;
import com.android.internal.textservice.ISpellCheckerSessionListener;
import com.android.internal.textservice.ITextServicesManager;
@@ -68,7 +69,6 @@ import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.CopyOnWriteArrayList;
public class TextServicesManagerService extends ITextServicesManager.Stub {
private static final String TAG = TextServicesManagerService.class.getSimpleName();
@@ -549,56 +549,26 @@ public class TextServicesManagerService extends ITextServicesManager.Stub {
return;
}
final SpellCheckerInfo sci = mSpellCheckerMap.get(sciId);
SpellCheckerBindGroup bindGroup = mSpellCheckerBindGroups.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();
}
}
if (bindGroup == null) {
final long ident = Binder.clearCallingIdentity();
try {
bindGroup = startSpellCheckerServiceInnerLocked(sci);
} finally {
Binder.restoreCallingIdentity(ident);
}
if (bindGroup == null) {
// startSpellCheckerServiceInnerLocked failed.
return;
}
}
final long ident = Binder.clearCallingIdentity();
try {
startSpellCheckerServiceInnerLocked(
sci, locale, tsListener, scListener, uid, bundle);
} finally {
Binder.restoreCallingIdentity(ident);
}
// Start getISpellCheckerSession async IPC, or just queue the request until the spell
// checker service is bound.
bindGroup.getISpellCheckerSessionOrQueueLocked(
new SessionRequest(uid, locale, tsListener, scListener, bundle));
}
return;
}
@Override
@@ -611,9 +581,8 @@ public class TextServicesManagerService extends ITextServicesManager.Stub {
}
}
private void startSpellCheckerServiceInnerLocked(SpellCheckerInfo info, String locale,
ITextServicesSessionListener tsListener, ISpellCheckerSessionListener scListener,
int uid, Bundle bundle) {
@Nullable
private SpellCheckerBindGroup startSpellCheckerServiceInnerLocked(SpellCheckerInfo info) {
if (DBG) {
Slog.w(TAG, "Start spell checker session inner locked.");
}
@@ -627,11 +596,11 @@ public class TextServicesManagerService extends ITextServicesManager.Stub {
if (!bindCurrentSpellCheckerService(serviceIntent, connection,
Context.BIND_AUTO_CREATE | Context.BIND_FOREGROUND_SERVICE_WHILE_AWAKE)) {
Slog.e(TAG, "Failed to get a spell checker service.");
return;
return null;
}
final SpellCheckerBindGroup group = new SpellCheckerBindGroup(
connection, tsListener, locale, scListener, uid, bundle);
final SpellCheckerBindGroup group = new SpellCheckerBindGroup(connection);
mSpellCheckerBindGroups.put(sciId, group);
return group;
}
@Override
@@ -814,16 +783,32 @@ public class TextServicesManagerService extends ITextServicesManager.Stub {
pw.println(" " + ent.getKey() + " " + grp + ":");
pw.println(" " + "mInternalConnection=" + grp.mInternalConnection);
pw.println(" " + "mSpellChecker=" + grp.mSpellChecker);
pw.println(" " + "mBound=" + grp.mBound + " mConnected=" + grp.mConnected);
pw.println(" " + "mUnbindCalled=" + grp.mUnbindCalled);
pw.println(" " + "mConnected=" + grp.mConnected);
final int numPendingSessionRequests = grp.mPendingSessionRequests.size();
for (int i = 0; i < numPendingSessionRequests; i++) {
final SessionRequest req = grp.mPendingSessionRequests.get(i);
pw.println(" " + "Pending Request #" + i + ":");
pw.println(" " + "mTsListener=" + req.mTsListener);
pw.println(" " + "mScListener=" + req.mScListener);
pw.println(" " + "mScLocale=" + req.mLocale + " mUid=" + req.mUserId);
}
final int numOnGoingSessionRequests = grp.mOnGoingSessionRequests.size();
for (int i = 0; i < numOnGoingSessionRequests; i++) {
final SessionRequest req = grp.mOnGoingSessionRequests.get(i);
pw.println(" " + "On going Request #" + i + ":");
++i;
pw.println(" " + "mTsListener=" + req.mTsListener);
pw.println(" " + "mScListener=" + req.mScListener);
pw.println(
" " + "mScLocale=" + req.mLocale + " mUid=" + req.mUserId);
}
final int N = grp.mListeners.size();
for (int i = 0; i < N; i++) {
final InternalDeathRecipient listener = grp.mListeners.get(i);
pw.println(" " + "Listener #" + i + ":");
pw.println(" " + "mTsListener=" + listener.mTsListener);
pw.println(" " + "mScListener=" + listener.mScListener);
pw.println(" " + "mGroup=" + listener.mGroup);
pw.println(" " + "mScLocale=" + listener.mScLocale
+ " mUid=" + listener.mUid);
}
}
pw.println("");
@@ -832,25 +817,44 @@ public class TextServicesManagerService extends ITextServicesManager.Stub {
}
}
private static final class SessionRequest {
@UserIdInt
public final int mUserId;
@Nullable
public final String mLocale;
@NonNull
public final ITextServicesSessionListener mTsListener;
@NonNull
public final ISpellCheckerSessionListener mScListener;
@Nullable
public final Bundle mBundle;
SessionRequest(@UserIdInt final int userId, @Nullable String locale,
@NonNull ITextServicesSessionListener tsListener,
@NonNull ISpellCheckerSessionListener scListener, @Nullable Bundle bundle) {
mUserId = userId;
mLocale = locale;
mTsListener = tsListener;
mScListener = scListener;
mBundle = bundle;
}
}
// SpellCheckerBindGroup contains active text service session listeners.
// If there are no listeners anymore, the SpellCheckerBindGroup instance will be removed from
// mSpellCheckerBindGroups
private final class SpellCheckerBindGroup {
private final String TAG = SpellCheckerBindGroup.class.getSimpleName();
private final InternalServiceConnection mInternalConnection;
private final CopyOnWriteArrayList<InternalDeathRecipient> mListeners =
new CopyOnWriteArrayList<>();
public boolean mBound;
public ISpellCheckerService mSpellChecker;
public boolean mConnected;
private final ArrayList<InternalDeathRecipient> mListeners = new ArrayList<>();
private boolean mUnbindCalled;
private ISpellCheckerService mSpellChecker;
private boolean mConnected;
private final ArrayList<SessionRequest> mPendingSessionRequests = new ArrayList<>();
private final ArrayList<SessionRequest> mOnGoingSessionRequests = new ArrayList<>();
public SpellCheckerBindGroup(InternalServiceConnection connection,
ITextServicesSessionListener listener, String locale,
ISpellCheckerSessionListener scListener, int uid, Bundle bundle) {
public SpellCheckerBindGroup(InternalServiceConnection connection) {
mInternalConnection = connection;
mBound = true;
mConnected = false;
addListener(listener, locale, scListener, uid, bundle);
}
public void onServiceConnected(ISpellCheckerService spellChecker) {
@@ -858,55 +862,15 @@ public class TextServicesManagerService extends ITextServicesManager.Stub {
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;
// Dispatch pending getISpellCheckerSession requests.
mPendingSessionRequests.forEach(this::getISpellCheckerSessionLocked);
mPendingSessionRequests.clear();
}
}
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());
@@ -941,20 +905,29 @@ public class TextServicesManagerService extends ITextServicesManager.Stub {
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);
if (mUnbindCalled) {
return;
}
// If there are no more active listeners, clean up. Only do this once.
if (!mListeners.isEmpty()) {
return;
}
if (!mPendingSessionRequests.isEmpty()) {
return;
}
if (!mOnGoingSessionRequests.isEmpty()) {
return;
}
final String sciId = mInternalConnection.mSciId;
final SpellCheckerBindGroup cur = mSpellCheckerBindGroups.get(sciId);
if (cur == this) {
if (DBG) {
Slog.d(TAG, "Remove bind group.");
}
mSpellCheckerBindGroups.remove(sciId);
}
mContext.unbindService(mInternalConnection);
mUnbindCalled = true;
}
public void removeAll() {
@@ -966,6 +939,59 @@ public class TextServicesManagerService extends ITextServicesManager.Stub {
idr.mScListener.asBinder().unlinkToDeath(idr, 0);
}
mListeners.clear();
mPendingSessionRequests.clear();
mOnGoingSessionRequests.clear();
cleanLocked();
}
}
public void getISpellCheckerSessionOrQueueLocked(@NonNull SessionRequest request) {
if (mUnbindCalled) {
return;
}
if (!mConnected) {
mPendingSessionRequests.add(request);
return;
}
getISpellCheckerSessionLocked(request);
}
private void getISpellCheckerSessionLocked(@NonNull SessionRequest request) {
if (mUnbindCalled) {
return;
}
try {
mSpellChecker.getISpellCheckerSession(
request.mLocale, request.mScListener, request.mBundle,
new ISpellCheckerServiceCallbackBinder(this, request));
mOnGoingSessionRequests.add(request);
} catch(RemoteException e) {
// The target spell checker service is not available. Better to reset the state.
removeAll();
}
cleanLocked();
}
void onSessionCreated(@Nullable final ISpellCheckerSession newSession,
@NonNull final SessionRequest request) {
synchronized (mSpellCheckerMap) {
if (mUnbindCalled) {
return;
}
if (mOnGoingSessionRequests.remove(request)) {
final InternalDeathRecipient recipient =
new InternalDeathRecipient(this, request.mScListener);
try {
request.mTsListener.onServiceConnected(newSession);
request.mScListener.asBinder().linkToDeath(recipient, 0);
mListeners.add(recipient);
} catch (RemoteException e) {
// Technically this can happen if the spell checker client app is already
// dead. We can just forget about this request; the request is already
// removed from mOnGoingSessionRequests and the death recipient listener is
// not yet added to mListeners. There is nothing to release further.
}
}
cleanLocked();
}
}
@@ -1008,21 +1034,13 @@ public class TextServicesManagerService extends ITextServicesManager.Stub {
}
private static final 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;
ISpellCheckerSessionListener scListener) {
mScListener = scListener;
mScLocale = scLocale;
mGroup = group;
mUid = uid;
mBundle = bundle;
}
public boolean hasSpellCheckerListener(ISpellCheckerSessionListener listener) {
@@ -1035,6 +1053,25 @@ public class TextServicesManagerService extends ITextServicesManager.Stub {
}
}
private static final class ISpellCheckerServiceCallbackBinder
extends ISpellCheckerServiceCallback.Stub {
@NonNull
private final SpellCheckerBindGroup mBindGroup;
@NonNull
private final SessionRequest mRequest;
ISpellCheckerServiceCallbackBinder(@NonNull final SpellCheckerBindGroup bindGroup,
@NonNull final SessionRequest request) {
mBindGroup = bindGroup;
mRequest = request;
}
@Override
public void onSessionCreated(@Nullable ISpellCheckerSession newSession) {
mBindGroup.onSessionCreated(newSession, mRequest);
}
}
private static final class TextServicesSettings {
private final ContentResolver mResolver;
@UserIdInt