/*
|
* Copyright (C) 2018 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.textclassifier;
|
|
import android.annotation.NonNull;
|
import android.annotation.Nullable;
|
import android.annotation.UserIdInt;
|
import android.content.ComponentName;
|
import android.content.Context;
|
import android.content.Intent;
|
import android.content.ServiceConnection;
|
import android.os.Binder;
|
import android.os.IBinder;
|
import android.os.RemoteException;
|
import android.os.UserHandle;
|
import android.service.textclassifier.ITextClassifierCallback;
|
import android.service.textclassifier.ITextClassifierService;
|
import android.service.textclassifier.TextClassifierService;
|
import android.util.ArrayMap;
|
import android.util.Slog;
|
import android.util.SparseArray;
|
import android.view.textclassifier.ConversationActions;
|
import android.view.textclassifier.SelectionEvent;
|
import android.view.textclassifier.TextClassification;
|
import android.view.textclassifier.TextClassificationContext;
|
import android.view.textclassifier.TextClassificationManager;
|
import android.view.textclassifier.TextClassificationSessionId;
|
import android.view.textclassifier.TextClassifierEvent;
|
import android.view.textclassifier.TextLanguage;
|
import android.view.textclassifier.TextLinks;
|
import android.view.textclassifier.TextSelection;
|
|
import com.android.internal.annotations.GuardedBy;
|
import com.android.internal.util.DumpUtils;
|
import com.android.internal.util.FunctionalUtils;
|
import com.android.internal.util.FunctionalUtils.ThrowingRunnable;
|
import com.android.internal.util.IndentingPrintWriter;
|
import com.android.internal.util.Preconditions;
|
import com.android.server.SystemService;
|
|
import java.io.FileDescriptor;
|
import java.io.PrintWriter;
|
import java.util.ArrayDeque;
|
import java.util.Map;
|
import java.util.Queue;
|
|
/**
|
* A manager for TextClassifier services.
|
* Apps bind to the TextClassificationManagerService for text classification. This service
|
* reroutes calls to it to a {@link TextClassifierService} that it manages.
|
*/
|
public final class TextClassificationManagerService extends ITextClassifierService.Stub {
|
|
private static final String LOG_TAG = "TextClassificationManagerService";
|
|
public static final class Lifecycle extends SystemService {
|
|
private final TextClassificationManagerService mManagerService;
|
|
public Lifecycle(Context context) {
|
super(context);
|
mManagerService = new TextClassificationManagerService(context);
|
}
|
|
@Override
|
public void onStart() {
|
try {
|
publishBinderService(Context.TEXT_CLASSIFICATION_SERVICE, mManagerService);
|
} catch (Throwable t) {
|
// Starting this service is not critical to the running of this device and should
|
// therefore not crash the device. If it fails, log the error and continue.
|
Slog.e(LOG_TAG, "Could not start the TextClassificationManagerService.", t);
|
}
|
}
|
|
@Override
|
public void onStartUser(int userId) {
|
processAnyPendingWork(userId);
|
}
|
|
@Override
|
public void onUnlockUser(int userId) {
|
// Rebind if we failed earlier due to locked encrypted user
|
processAnyPendingWork(userId);
|
}
|
|
private void processAnyPendingWork(int userId) {
|
synchronized (mManagerService.mLock) {
|
mManagerService.getUserStateLocked(userId).bindIfHasPendingRequestsLocked();
|
}
|
}
|
|
@Override
|
public void onStopUser(int userId) {
|
synchronized (mManagerService.mLock) {
|
UserState userState = mManagerService.peekUserStateLocked(userId);
|
if (userState != null) {
|
userState.mConnection.cleanupService();
|
mManagerService.mUserStates.remove(userId);
|
}
|
}
|
}
|
|
}
|
|
private final Context mContext;
|
private final Object mLock;
|
@GuardedBy("mLock")
|
final SparseArray<UserState> mUserStates = new SparseArray<>();
|
@GuardedBy("mLock")
|
private final Map<TextClassificationSessionId, Integer> mSessionUserIds = new ArrayMap<>();
|
|
private TextClassificationManagerService(Context context) {
|
mContext = Preconditions.checkNotNull(context);
|
mLock = new Object();
|
}
|
|
@Override
|
public void onSuggestSelection(
|
@Nullable TextClassificationSessionId sessionId,
|
TextSelection.Request request, ITextClassifierCallback callback)
|
throws RemoteException {
|
Preconditions.checkNotNull(request);
|
Preconditions.checkNotNull(callback);
|
final int userId = request.getUserId();
|
validateInput(mContext, request.getCallingPackageName(), userId);
|
|
synchronized (mLock) {
|
UserState userState = getUserStateLocked(userId);
|
if (!userState.bindLocked()) {
|
callback.onFailure();
|
} else if (userState.isBoundLocked()) {
|
userState.mService.onSuggestSelection(sessionId, request, callback);
|
} else {
|
userState.mPendingRequests.add(new PendingRequest(
|
() -> onSuggestSelection(sessionId, request, callback),
|
callback::onFailure, callback.asBinder(), this, userState));
|
}
|
}
|
}
|
|
@Override
|
public void onClassifyText(
|
@Nullable TextClassificationSessionId sessionId,
|
TextClassification.Request request, ITextClassifierCallback callback)
|
throws RemoteException {
|
Preconditions.checkNotNull(request);
|
Preconditions.checkNotNull(callback);
|
final int userId = request.getUserId();
|
validateInput(mContext, request.getCallingPackageName(), userId);
|
|
synchronized (mLock) {
|
UserState userState = getUserStateLocked(userId);
|
if (!userState.bindLocked()) {
|
callback.onFailure();
|
} else if (userState.isBoundLocked()) {
|
userState.mService.onClassifyText(sessionId, request, callback);
|
} else {
|
userState.mPendingRequests.add(new PendingRequest(
|
() -> onClassifyText(sessionId, request, callback),
|
callback::onFailure, callback.asBinder(), this, userState));
|
}
|
}
|
}
|
|
@Override
|
public void onGenerateLinks(
|
@Nullable TextClassificationSessionId sessionId,
|
TextLinks.Request request, ITextClassifierCallback callback)
|
throws RemoteException {
|
Preconditions.checkNotNull(request);
|
Preconditions.checkNotNull(callback);
|
final int userId = request.getUserId();
|
validateInput(mContext, request.getCallingPackageName(), userId);
|
|
synchronized (mLock) {
|
UserState userState = getUserStateLocked(userId);
|
if (!userState.bindLocked()) {
|
callback.onFailure();
|
} else if (userState.isBoundLocked()) {
|
userState.mService.onGenerateLinks(sessionId, request, callback);
|
} else {
|
userState.mPendingRequests.add(new PendingRequest(
|
() -> onGenerateLinks(sessionId, request, callback),
|
callback::onFailure, callback.asBinder(), this, userState));
|
}
|
}
|
}
|
|
@Override
|
public void onSelectionEvent(
|
@Nullable TextClassificationSessionId sessionId, SelectionEvent event)
|
throws RemoteException {
|
Preconditions.checkNotNull(event);
|
final int userId = event.getUserId();
|
validateInput(mContext, event.getPackageName(), userId);
|
|
synchronized (mLock) {
|
UserState userState = getUserStateLocked(userId);
|
if (userState.isBoundLocked()) {
|
userState.mService.onSelectionEvent(sessionId, event);
|
} else {
|
userState.mPendingRequests.add(new PendingRequest(
|
() -> onSelectionEvent(sessionId, event),
|
null /* onServiceFailure */, null /* binder */, this, userState));
|
}
|
}
|
}
|
@Override
|
public void onTextClassifierEvent(
|
@Nullable TextClassificationSessionId sessionId,
|
TextClassifierEvent event) throws RemoteException {
|
Preconditions.checkNotNull(event);
|
final String packageName = event.getEventContext() == null
|
? null
|
: event.getEventContext().getPackageName();
|
final int userId = event.getEventContext() == null
|
? UserHandle.getCallingUserId()
|
: event.getEventContext().getUserId();
|
validateInput(mContext, packageName, userId);
|
|
synchronized (mLock) {
|
UserState userState = getUserStateLocked(userId);
|
if (userState.isBoundLocked()) {
|
userState.mService.onTextClassifierEvent(sessionId, event);
|
} else {
|
userState.mPendingRequests.add(new PendingRequest(
|
() -> onTextClassifierEvent(sessionId, event),
|
null /* onServiceFailure */, null /* binder */, this, userState));
|
}
|
}
|
}
|
|
@Override
|
public void onDetectLanguage(
|
@Nullable TextClassificationSessionId sessionId,
|
TextLanguage.Request request,
|
ITextClassifierCallback callback) throws RemoteException {
|
Preconditions.checkNotNull(request);
|
Preconditions.checkNotNull(callback);
|
final int userId = request.getUserId();
|
validateInput(mContext, request.getCallingPackageName(), userId);
|
|
synchronized (mLock) {
|
UserState userState = getUserStateLocked(userId);
|
if (!userState.bindLocked()) {
|
callback.onFailure();
|
} else if (userState.isBoundLocked()) {
|
userState.mService.onDetectLanguage(sessionId, request, callback);
|
} else {
|
userState.mPendingRequests.add(new PendingRequest(
|
() -> onDetectLanguage(sessionId, request, callback),
|
callback::onFailure, callback.asBinder(), this, userState));
|
}
|
}
|
}
|
|
@Override
|
public void onSuggestConversationActions(
|
@Nullable TextClassificationSessionId sessionId,
|
ConversationActions.Request request,
|
ITextClassifierCallback callback) throws RemoteException {
|
Preconditions.checkNotNull(request);
|
Preconditions.checkNotNull(callback);
|
final int userId = request.getUserId();
|
validateInput(mContext, request.getCallingPackageName(), userId);
|
|
synchronized (mLock) {
|
UserState userState = getUserStateLocked(userId);
|
if (!userState.bindLocked()) {
|
callback.onFailure();
|
} else if (userState.isBoundLocked()) {
|
userState.mService.onSuggestConversationActions(sessionId, request, callback);
|
} else {
|
userState.mPendingRequests.add(new PendingRequest(
|
() -> onSuggestConversationActions(sessionId, request, callback),
|
callback::onFailure, callback.asBinder(), this, userState));
|
}
|
}
|
}
|
|
@Override
|
public void onCreateTextClassificationSession(
|
TextClassificationContext classificationContext, TextClassificationSessionId sessionId)
|
throws RemoteException {
|
Preconditions.checkNotNull(sessionId);
|
Preconditions.checkNotNull(classificationContext);
|
final int userId = classificationContext.getUserId();
|
validateInput(mContext, classificationContext.getPackageName(), userId);
|
|
synchronized (mLock) {
|
UserState userState = getUserStateLocked(userId);
|
if (userState.isBoundLocked()) {
|
userState.mService.onCreateTextClassificationSession(
|
classificationContext, sessionId);
|
mSessionUserIds.put(sessionId, userId);
|
} else {
|
userState.mPendingRequests.add(new PendingRequest(
|
() -> onCreateTextClassificationSession(classificationContext, sessionId),
|
null /* onServiceFailure */, null /* binder */, this, userState));
|
}
|
}
|
}
|
|
@Override
|
public void onDestroyTextClassificationSession(TextClassificationSessionId sessionId)
|
throws RemoteException {
|
Preconditions.checkNotNull(sessionId);
|
|
synchronized (mLock) {
|
final int userId = mSessionUserIds.containsKey(sessionId)
|
? mSessionUserIds.get(sessionId)
|
: UserHandle.getCallingUserId();
|
validateInput(mContext, null /* packageName */, userId);
|
|
UserState userState = getUserStateLocked(userId);
|
if (userState.isBoundLocked()) {
|
userState.mService.onDestroyTextClassificationSession(sessionId);
|
mSessionUserIds.remove(sessionId);
|
} else {
|
userState.mPendingRequests.add(new PendingRequest(
|
() -> onDestroyTextClassificationSession(sessionId),
|
null /* onServiceFailure */, null /* binder */, this, userState));
|
}
|
}
|
}
|
|
@GuardedBy("mLock")
|
private UserState getUserStateLocked(int userId) {
|
UserState result = mUserStates.get(userId);
|
if (result == null) {
|
result = new UserState(userId, mContext, mLock);
|
mUserStates.put(userId, result);
|
}
|
return result;
|
}
|
|
@GuardedBy("mLock")
|
UserState peekUserStateLocked(int userId) {
|
return mUserStates.get(userId);
|
}
|
|
@Override
|
protected void dump(FileDescriptor fd, PrintWriter fout, String[] args) {
|
if (!DumpUtils.checkDumpPermission(mContext, LOG_TAG, fout)) return;
|
IndentingPrintWriter pw = new IndentingPrintWriter(fout, " ");
|
TextClassificationManager tcm = mContext.getSystemService(TextClassificationManager.class);
|
tcm.dump(pw);
|
|
pw.printPair("context", mContext); pw.println();
|
synchronized (mLock) {
|
int size = mUserStates.size();
|
pw.print("Number user states: "); pw.println(size);
|
if (size > 0) {
|
for (int i = 0; i < size; i++) {
|
pw.increaseIndent();
|
UserState userState = mUserStates.valueAt(i);
|
pw.print(i); pw.print(":"); userState.dump(pw); pw.println();
|
pw.decreaseIndent();
|
}
|
}
|
pw.println("Number of active sessions: " + mSessionUserIds.size());
|
}
|
}
|
|
private static final class PendingRequest implements IBinder.DeathRecipient {
|
|
@Nullable private final IBinder mBinder;
|
@NonNull private final Runnable mRequest;
|
@Nullable private final Runnable mOnServiceFailure;
|
@GuardedBy("mLock")
|
@NonNull private final UserState mOwningUser;
|
@NonNull private final TextClassificationManagerService mService;
|
|
/**
|
* Initializes a new pending request.
|
* @param request action to perform when the service is bound
|
* @param onServiceFailure action to perform when the service dies or disconnects
|
* @param binder binder to the process that made this pending request
|
* @param service
|
* @param owningUser
|
*/
|
PendingRequest(
|
@NonNull ThrowingRunnable request, @Nullable ThrowingRunnable onServiceFailure,
|
@Nullable IBinder binder,
|
TextClassificationManagerService service,
|
UserState owningUser) {
|
mRequest =
|
logOnFailure(Preconditions.checkNotNull(request), "handling pending request");
|
mOnServiceFailure =
|
logOnFailure(onServiceFailure, "notifying callback of service failure");
|
mBinder = binder;
|
mService = service;
|
mOwningUser = owningUser;
|
if (mBinder != null) {
|
try {
|
mBinder.linkToDeath(this, 0);
|
} catch (RemoteException e) {
|
e.printStackTrace();
|
}
|
}
|
}
|
|
@Override
|
public void binderDied() {
|
synchronized (mService.mLock) {
|
// No need to handle this pending request anymore. Remove.
|
removeLocked();
|
}
|
}
|
|
@GuardedBy("mLock")
|
private void removeLocked() {
|
mOwningUser.mPendingRequests.remove(this);
|
if (mBinder != null) {
|
mBinder.unlinkToDeath(this, 0);
|
}
|
}
|
}
|
|
private static Runnable logOnFailure(@Nullable ThrowingRunnable r, String opDesc) {
|
if (r == null) return null;
|
return FunctionalUtils.handleExceptions(r,
|
e -> Slog.d(LOG_TAG, "Error " + opDesc + ": " + e.getMessage()));
|
}
|
|
private static void validateInput(
|
Context context, @Nullable String packageName, @UserIdInt int userId)
|
throws RemoteException {
|
|
try {
|
if (packageName != null) {
|
final int packageUid = context.getPackageManager()
|
.getPackageUidAsUser(packageName, UserHandle.getCallingUserId());
|
final int callingUid = Binder.getCallingUid();
|
Preconditions.checkArgument(callingUid == packageUid
|
// Trust the system process:
|
|| callingUid == android.os.Process.SYSTEM_UID,
|
"Invalid package name. Package=" + packageName
|
+ ", CallingUid=" + callingUid);
|
}
|
|
Preconditions.checkArgument(userId != UserHandle.USER_NULL, "Null userId");
|
final int callingUserId = UserHandle.getCallingUserId();
|
if (callingUserId != userId) {
|
context.enforceCallingOrSelfPermission(
|
android.Manifest.permission.INTERACT_ACROSS_USERS_FULL,
|
"Invalid userId. UserId=" + userId + ", CallingUserId=" + callingUserId);
|
}
|
} catch (Exception e) {
|
throw new RemoteException("Invalid request: " + e.getMessage(), e,
|
/* enableSuppression */ true, /* writableStackTrace */ true);
|
}
|
}
|
|
private static final class UserState {
|
@UserIdInt final int mUserId;
|
final TextClassifierServiceConnection mConnection = new TextClassifierServiceConnection();
|
@GuardedBy("mLock")
|
final Queue<PendingRequest> mPendingRequests = new ArrayDeque<>();
|
@GuardedBy("mLock")
|
ITextClassifierService mService;
|
@GuardedBy("mLock")
|
boolean mBinding;
|
|
private final Context mContext;
|
private final Object mLock;
|
|
private UserState(int userId, Context context, Object lock) {
|
mUserId = userId;
|
mContext = Preconditions.checkNotNull(context);
|
mLock = Preconditions.checkNotNull(lock);
|
}
|
|
@GuardedBy("mLock")
|
boolean isBoundLocked() {
|
return mService != null;
|
}
|
|
@GuardedBy("mLock")
|
private void handlePendingRequestsLocked() {
|
PendingRequest request;
|
while ((request = mPendingRequests.poll()) != null) {
|
if (isBoundLocked()) {
|
request.mRequest.run();
|
} else {
|
if (request.mOnServiceFailure != null) {
|
request.mOnServiceFailure.run();
|
}
|
}
|
|
if (request.mBinder != null) {
|
request.mBinder.unlinkToDeath(request, 0);
|
}
|
}
|
}
|
|
@GuardedBy("mLock")
|
private boolean bindIfHasPendingRequestsLocked() {
|
return !mPendingRequests.isEmpty() && bindLocked();
|
}
|
|
/**
|
* @return true if the service is bound or in the process of being bound.
|
* Returns false otherwise.
|
*/
|
@GuardedBy("mLock")
|
private boolean bindLocked() {
|
if (isBoundLocked() || mBinding) {
|
return true;
|
}
|
|
// TODO: Handle bind timeout.
|
final boolean willBind;
|
final long identity = Binder.clearCallingIdentity();
|
try {
|
ComponentName componentName =
|
TextClassifierService.getServiceComponentName(mContext);
|
if (componentName == null) {
|
// Might happen if the storage is encrypted and the user is not unlocked
|
return false;
|
}
|
Intent serviceIntent = new Intent(TextClassifierService.SERVICE_INTERFACE)
|
.setComponent(componentName);
|
Slog.d(LOG_TAG, "Binding to " + serviceIntent.getComponent());
|
willBind = mContext.bindServiceAsUser(
|
serviceIntent, mConnection,
|
Context.BIND_AUTO_CREATE | Context.BIND_FOREGROUND_SERVICE
|
| Context.BIND_RESTRICT_ASSOCIATIONS,
|
UserHandle.of(mUserId));
|
mBinding = willBind;
|
} finally {
|
Binder.restoreCallingIdentity(identity);
|
}
|
return willBind;
|
}
|
|
private void dump(IndentingPrintWriter pw) {
|
pw.printPair("context", mContext);
|
pw.printPair("userId", mUserId);
|
synchronized (mLock) {
|
pw.printPair("binding", mBinding);
|
pw.printPair("numberRequests", mPendingRequests.size());
|
}
|
}
|
|
private final class TextClassifierServiceConnection implements ServiceConnection {
|
@Override
|
public void onServiceConnected(ComponentName name, IBinder service) {
|
init(ITextClassifierService.Stub.asInterface(service));
|
}
|
|
@Override
|
public void onServiceDisconnected(ComponentName name) {
|
cleanupService();
|
}
|
|
@Override
|
public void onBindingDied(ComponentName name) {
|
cleanupService();
|
}
|
|
@Override
|
public void onNullBinding(ComponentName name) {
|
cleanupService();
|
}
|
|
void cleanupService() {
|
init(null);
|
}
|
|
private void init(@Nullable ITextClassifierService service) {
|
synchronized (mLock) {
|
mService = service;
|
mBinding = false;
|
handlePendingRequestsLocked();
|
}
|
}
|
}
|
}
|
}
|