blob: 853c7eb51b84cfb1e9ad3e168e2383799c68ad1a [file] [log] [blame]
/*
* 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.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.os.Binder;
import android.os.Handler;
import android.os.IBinder;
import android.os.RemoteException;
import android.util.Slog;
import android.service.textclassifier.ITextClassifierService;
import android.service.textclassifier.ITextClassificationCallback;
import android.service.textclassifier.ITextLinksCallback;
import android.service.textclassifier.ITextSelectionCallback;
import android.service.textclassifier.TextClassifierService;
import android.view.textclassifier.TextClassification;
import android.view.textclassifier.TextClassifier;
import android.view.textclassifier.TextLinks;
import android.view.textclassifier.TextSelection;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.util.Preconditions;
import com.android.server.SystemService;
import java.util.LinkedList;
import java.util.Queue;
import java.util.concurrent.Callable;
import java.util.concurrent.TimeUnit;
/**
* 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";
// How long after the last interaction with the service we would unbind
private static final long TIMEOUT_IDLE_BIND_MILLIS = TimeUnit.MINUTES.toMillis(1);
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);
}
}
}
private final Context mContext;
private final Handler mHandler;
private final Intent mServiceIntent;
private final ServiceConnection mConnection;
private final Runnable mUnbind;
private final Object mLock;
@GuardedBy("mLock")
private final Queue<PendingRequest> mPendingRequests;
@GuardedBy("mLock")
private ITextClassifierService mService;
@GuardedBy("mLock")
private boolean mBinding;
private TextClassificationManagerService(Context context) {
mContext = Preconditions.checkNotNull(context);
mHandler = new Handler();
mServiceIntent = new Intent(TextClassifierService.SERVICE_INTERFACE)
.setComponent(TextClassifierService.getServiceComponentName(mContext));
mConnection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
synchronized (mLock) {
mService = ITextClassifierService.Stub.asInterface(service);
setBindingLocked(false);
handlePendingRequestsLocked();
}
}
@Override
public void onServiceDisconnected(ComponentName name) {
cleanupService();
}
@Override
public void onBindingDied(ComponentName name) {
cleanupService();
}
@Override
public void onNullBinding(ComponentName name) {
cleanupService();
}
private void cleanupService() {
synchronized (mLock) {
mService = null;
setBindingLocked(false);
handlePendingRequestsLocked();
}
}
};
mPendingRequests = new LinkedList<>();
mUnbind = this::unbind;
mLock = new Object();
}
@Override
public void onSuggestSelection(
CharSequence text, int selectionStartIndex, int selectionEndIndex,
TextSelection.Options options, ITextSelectionCallback callback)
throws RemoteException {
// TODO(b/72481438): All remote calls need to take userId.
validateInput(text, selectionStartIndex, selectionEndIndex, callback);
if (!bind()) {
callback.onFailure();
return;
}
synchronized (mLock) {
if (isBoundLocked()) {
mService.onSuggestSelection(
text, selectionStartIndex, selectionEndIndex, options, callback);
scheduleUnbindLocked();
} else {
final Callable<Void> request = () -> {
onSuggestSelection(
text, selectionStartIndex, selectionEndIndex,
options, callback);
return null;
};
final Callable<Void> onServiceFailure = () -> {
callback.onFailure();
return null;
};
enqueueRequestLocked(request, onServiceFailure, callback.asBinder());
}
}
}
@Override
public void onClassifyText(
CharSequence text, int startIndex, int endIndex,
TextClassification.Options options, ITextClassificationCallback callback)
throws RemoteException {
validateInput(text, startIndex, endIndex, callback);
if (!bind()) {
callback.onFailure();
return;
}
synchronized (mLock) {
if (isBoundLocked()) {
mService.onClassifyText(text, startIndex, endIndex, options, callback);
scheduleUnbindLocked();
} else {
final Callable<Void> request = () -> {
onClassifyText(text, startIndex, endIndex, options, callback);
return null;
};
final Callable<Void> onServiceFailure = () -> {
callback.onFailure();
return null;
};
enqueueRequestLocked(request, onServiceFailure, callback.asBinder());
}
}
}
@Override
public void onGenerateLinks(
CharSequence text, TextLinks.Options options, ITextLinksCallback callback)
throws RemoteException {
validateInput(text, callback);
if (!bind()) {
callback.onFailure();
return;
}
synchronized (mLock) {
if (isBoundLocked()) {
mService.onGenerateLinks(text, options, callback);
scheduleUnbindLocked();
} else {
final Callable<Void> request = () -> {
onGenerateLinks(text, options, callback);
return null;
};
final Callable<Void> onServiceFailure = () -> {
callback.onFailure();
return null;
};
enqueueRequestLocked(request, onServiceFailure, callback.asBinder());
}
}
}
/**
* @return true if the service is bound or in the process of being bound.
* Returns false otherwise.
*/
private boolean bind() {
synchronized (mLock) {
if (isBoundLocked() || isBindingLocked()) {
return true;
}
// TODO: Handle bind timeout.
final boolean willBind;
final long identity = Binder.clearCallingIdentity();
try {
Slog.d(LOG_TAG, "Binding to " + mServiceIntent.getComponent());
willBind = mContext.bindServiceAsUser(
mServiceIntent, mConnection,
Context.BIND_AUTO_CREATE | Context.BIND_FOREGROUND_SERVICE,
Binder.getCallingUserHandle());
setBindingLocked(willBind);
} finally {
Binder.restoreCallingIdentity(identity);
}
return willBind;
}
}
@GuardedBy("mLock")
private boolean isBoundLocked() {
return mService != null;
}
@GuardedBy("mLock")
private boolean isBindingLocked() {
return mBinding;
}
@GuardedBy("mLock")
private void setBindingLocked(boolean binding) {
mBinding = binding;
}
private void unbind() {
synchronized (mLock) {
if (!isBoundLocked()) {
return;
}
Slog.d(LOG_TAG, "Unbinding from " + mServiceIntent.getComponent());
mContext.unbindService(mConnection);
synchronized (mLock) {
mService = null;
}
}
}
@GuardedBy("mLock")
private void scheduleUnbindLocked() {
mHandler.removeCallbacks(mUnbind);
mHandler.postDelayed(mUnbind, TIMEOUT_IDLE_BIND_MILLIS);
}
@GuardedBy("mLock")
private void enqueueRequestLocked(
Callable<Void> request, Callable<Void> onServiceFailure, IBinder binder) {
mPendingRequests.add(new PendingRequest(request, onServiceFailure, binder));
}
@GuardedBy("mLock")
private void handlePendingRequestsLocked() {
// TODO(b/72481146): Implement PendingRequest similar to that in RemoteFillService.
final PendingRequest[] pendingRequests =
mPendingRequests.toArray(new PendingRequest[mPendingRequests.size()]);
for (PendingRequest pendingRequest : pendingRequests) {
if (isBoundLocked()) {
pendingRequest.executeLocked();
} else {
pendingRequest.notifyServiceFailureLocked();
}
}
}
private final class PendingRequest implements IBinder.DeathRecipient {
private final Callable<Void> mRequest;
private final Callable<Void> mOnServiceFailure;
private final IBinder mBinder;
/**
* 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
*/
PendingRequest(
@NonNull Callable<Void> request, @NonNull Callable<Void> onServiceFailure,
@NonNull IBinder binder) {
mRequest = Preconditions.checkNotNull(request);
mOnServiceFailure = Preconditions.checkNotNull(onServiceFailure);
mBinder = Preconditions.checkNotNull(binder);
try {
mBinder.linkToDeath(this, 0);
} catch (RemoteException e) {
e.printStackTrace();
}
}
@GuardedBy("mLock")
void executeLocked() {
removeLocked();
try {
mRequest.call();
} catch (Exception e) {
Slog.d(LOG_TAG, "Error handling pending request: " + e.getMessage());
}
}
@GuardedBy("mLock")
void notifyServiceFailureLocked() {
removeLocked();
try {
mOnServiceFailure.call();
} catch (Exception e) {
Slog.d(LOG_TAG, "Error notifying callback of service failure: "
+ e.getMessage());
}
}
@Override
public void binderDied() {
synchronized (mLock) {
// No need to handle this pending request anymore. Remove.
removeLocked();
}
}
@GuardedBy("mLock")
private void removeLocked() {
mPendingRequests.remove(this);
mBinder.unlinkToDeath(this, 0);
}
}
private static void validateInput(
CharSequence text, int startIndex, int endIndex, Object callback)
throws RemoteException {
try {
TextClassifier.Utils.validate(text, startIndex, endIndex, true /* allowInMainThread */);
Preconditions.checkNotNull(callback);
} catch (IllegalArgumentException | NullPointerException e) {
throw new RemoteException(e.getMessage());
}
}
private static void validateInput(CharSequence text, Object callback) throws RemoteException {
try {
TextClassifier.Utils.validate(text, true /* allowInMainThread */);
Preconditions.checkNotNull(callback);
} catch (IllegalArgumentException | NullPointerException e) {
throw new RemoteException(e.getMessage());
}
}
}