blob: dbf1e83f8eacd01db442b5a2ab1934007571e9ab [file] [log] [blame]
/*
* Copyright (C) 2016 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.autofill;
import static android.service.autofill.AutofillService.EXTRA_ACTIVITY_TOKEN;
import static android.service.voice.VoiceInteractionSession.KEY_RECEIVER_EXTRAS;
import static android.service.voice.VoiceInteractionSession.KEY_STRUCTURE;
import static android.view.autofill.AutofillManager.FLAG_START_SESSION;
import static com.android.server.autofill.Helper.DEBUG;
import static com.android.server.autofill.Helper.VERBOSE;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.Activity;
import android.app.ActivityManager;
import android.app.AppGlobals;
import android.app.assist.AssistStructure;
import android.content.ComponentName;
import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.content.pm.ServiceInfo;
import android.graphics.Rect;
import android.os.Binder;
import android.os.Bundle;
import android.os.IBinder;
import android.os.Looper;
import android.os.RemoteCallbackList;
import android.os.RemoteException;
import android.os.UserManager;
import android.provider.Settings;
import android.service.autofill.AutofillService;
import android.service.autofill.AutofillServiceInfo;
import android.service.autofill.IAutoFillService;
import android.text.TextUtils;
import android.util.ArrayMap;
import android.util.LocalLog;
import android.util.Log;
import android.util.PrintWriterPrinter;
import android.util.Slog;
import android.view.autofill.AutofillId;
import android.view.autofill.AutofillValue;
import android.view.autofill.IAutoFillManagerClient;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.os.HandlerCaller;
import com.android.internal.os.IResultReceiver;
import com.android.server.autofill.ui.AutoFillUI;
import java.io.PrintWriter;
import java.util.ArrayList;
/**
* Bridge between the {@code system_server}'s {@link AutofillManagerService} and the
* app's {@link IAutoFillService} implementation.
*
*/
final class AutofillManagerServiceImpl {
private static final String TAG = "AutofillManagerServiceImpl";
static final int MSG_SERVICE_SAVE = 1;
private final int mUserId;
private final Context mContext;
private final Object mLock;
private final AutoFillUI mUi;
private RemoteCallbackList<IAutoFillManagerClient> mClients;
private AutofillServiceInfo mInfo;
private final LocalLog mRequestsHistory;
/**
* Whether service was disabled for user due to {@link UserManager} restrictions.
*/
private boolean mDisabled;
private final HandlerCaller.Callback mHandlerCallback = (msg) -> {
switch (msg.what) {
case MSG_SERVICE_SAVE:
handleSessionSave((IBinder) msg.obj);
break;
default:
Slog.w(TAG, "invalid msg on handler: " + msg);
}
};
private final HandlerCaller mHandlerCaller = new HandlerCaller(null, Looper.getMainLooper(),
mHandlerCallback, true);
/**
* Cache of pending {@link Session}s, keyed by {@code activityToken}.
*
* <p>They're kept until the {@link AutofillService} finished handling a request, an error
* occurs, or the session times out.
*/
// TODO(b/33197203): need to make sure service is bound while callback is pending and/or
// use WeakReference
@GuardedBy("mLock")
private final ArrayMap<IBinder, Session> mSessions = new ArrayMap<>();
/**
* Receiver of assist data from the app's {@link Activity}.
*/
private final IResultReceiver mAssistReceiver = new IResultReceiver.Stub() {
@Override
public void send(int resultCode, Bundle resultData) throws RemoteException {
if (VERBOSE) {
Slog.v(TAG, "resultCode on mAssistReceiver: " + resultCode);
}
final AssistStructure structure = resultData.getParcelable(KEY_STRUCTURE);
if (structure == null) {
Slog.wtf(TAG, "no assist structure for id " + resultCode);
return;
}
final Bundle receiverExtras = resultData.getBundle(KEY_RECEIVER_EXTRAS);
if (receiverExtras == null) {
Slog.wtf(TAG, "No " + KEY_RECEIVER_EXTRAS + " on receiver");
return;
}
final IBinder activityToken = receiverExtras.getBinder(EXTRA_ACTIVITY_TOKEN);
final Session session;
synchronized (mLock) {
session = mSessions.get(activityToken);
if (session == null) {
Slog.w(TAG, "no server session for activityToken " + activityToken);
return;
}
// TODO(b/33197203): since service is fetching the data (to use for save later),
// we should optimize what's sent (for example, remove layout containers,
// color / font info, etc...)
session.mStructure = structure;
}
// TODO(b/33197203, b/33269702): Must fetch the data so it's available later on
// handleSave(), even if if the activity is gone by then, but structure.ensureData()
// gives a ONE_WAY warning because system_service could block on app calls.
// We need to change AssistStructure so it provides a "one-way" writeToParcel()
// method that sends all the data
structure.ensureData();
// Sanitize structure before it's sent to service.
structure.sanitizeForParceling(true);
// TODO(b/33197203): Need to pipe the bundle
session.mRemoteFillService.onFillRequest(structure, null, session.mFlags);
}
};
AutofillManagerServiceImpl(Context context, Object lock, LocalLog requestsHistory,
int userId, AutoFillUI ui, boolean disabled) {
mContext = context;
mLock = lock;
mRequestsHistory = requestsHistory;
mUserId = userId;
mUi = ui;
updateLocked(disabled);
}
CharSequence getServiceName() {
if (mInfo == null) {
return null;
}
final ComponentName serviceComponent = mInfo.getServiceInfo().getComponentName();
final String packageName = serviceComponent.getPackageName();
try {
final PackageManager pm = mContext.getPackageManager();
final ApplicationInfo info = pm.getApplicationInfo(packageName, 0);
return pm.getApplicationLabel(info);
} catch (Exception e) {
Slog.e(TAG, "Could not get label for " + packageName + ": " + e);
return packageName;
}
}
void updateLocked(boolean disabled) {
final boolean wasEnabled = isEnabled();
mDisabled = disabled;
ComponentName serviceComponent = null;
ServiceInfo serviceInfo = null;
final String componentName = Settings.Secure.getStringForUser(
mContext.getContentResolver(), Settings.Secure.AUTOFILL_SERVICE, mUserId);
if (!TextUtils.isEmpty(componentName)) {
try {
serviceComponent = ComponentName.unflattenFromString(componentName);
serviceInfo = AppGlobals.getPackageManager().getServiceInfo(serviceComponent,
0, mUserId);
} catch (RuntimeException | RemoteException e) {
Slog.e(TAG, "Bad autofill service name " + componentName + ": " + e);
return;
}
}
try {
if (serviceInfo != null) {
mInfo = new AutofillServiceInfo(mContext.getPackageManager(),
serviceComponent, mUserId);
} else {
mInfo = null;
}
if (wasEnabled != isEnabled()) {
if (!isEnabled()) {
final int sessionCount = mSessions.size();
for (int i = sessionCount - 1; i >= 0; i--) {
final Session session = mSessions.valueAt(i);
session.removeSelfLocked();
}
}
sendStateToClients();
}
} catch (PackageManager.NameNotFoundException e) {
Slog.e(TAG, "Bad autofill service name " + componentName + ": " + e);
}
}
/**
* Used by {@link AutofillManagerServiceShellCommand} to request save for the current top app.
*/
void requestSaveForUserLocked(IBinder activityToken) {
if (!isEnabled()) {
return;
}
final Session session = mSessions.get(activityToken);
if (session == null) {
Slog.w(TAG, "requestSaveForUserLocked(): no session for " + activityToken);
return;
}
session.callSaveLocked();
}
boolean addClientLocked(IAutoFillManagerClient client) {
if (mClients == null) {
mClients = new RemoteCallbackList<>();
}
mClients.register(client);
return isEnabled();
}
void setAuthenticationResultLocked(Bundle data, IBinder activityToken) {
if (!isEnabled()) {
return;
}
final Session session = mSessions.get(activityToken);
if (session != null) {
session.setAuthenticationResultLocked(data);
}
}
void setHasCallback(IBinder activityToken, boolean hasIt) {
if (!isEnabled()) {
return;
}
final Session session = mSessions.get(activityToken);
if (session != null) {
session.setHasCallback(hasIt);
}
}
void startSessionLocked(@NonNull IBinder activityToken, @Nullable IBinder windowToken,
@NonNull IBinder appCallbackToken, @NonNull AutofillId autofillId,
@NonNull Rect virtualBounds, @Nullable AutofillValue value, boolean hasCallback,
int flags, @NonNull String packageName) {
if (!isEnabled()) {
return;
}
final String historyItem = "s=" + mInfo.getServiceInfo().packageName
+ " u=" + mUserId + " a=" + activityToken
+ " i=" + autofillId + " b=" + virtualBounds + " hc=" + hasCallback
+ " f=" + flags;
mRequestsHistory.log(historyItem);
// TODO(b/33197203): Handle scenario when user forced autofill after app was already
// autofilled.
final Session session = mSessions.get(activityToken);
if (session != null) {
// Already started...
return;
}
final Session newSession = createSessionByTokenLocked(activityToken,
windowToken, appCallbackToken, hasCallback, flags, packageName);
newSession.updateLocked(autofillId, virtualBounds, value, FLAG_START_SESSION);
}
void finishSessionLocked(IBinder activityToken) {
if (!isEnabled()) {
return;
}
final Session session = mSessions.get(activityToken);
if (session == null) {
Slog.w(TAG, "finishSessionLocked(): no session for " + activityToken);
return;
}
final boolean finished = session.showSaveLocked();
if (DEBUG) {
Log.d(TAG, "finishSessionLocked(): session finished on save? " + finished);
}
if (finished) {
session.removeSelf();
}
}
void cancelSessionLocked(IBinder activityToken) {
if (!isEnabled()) {
return;
}
final Session session = mSessions.get(activityToken);
if (session == null) {
Slog.w(TAG, "cancelSessionLocked(): no session for " + activityToken);
return;
}
session.removeSelfLocked();
}
private Session createSessionByTokenLocked(@NonNull IBinder activityToken,
@Nullable IBinder windowToken, @NonNull IBinder appCallbackToken, boolean hasCallback,
int flags, @NonNull String packageName) {
final Session newSession = new Session(this, mUi, mContext, mHandlerCaller, mUserId, mLock,
activityToken, windowToken, appCallbackToken, hasCallback, flags,
mInfo.getServiceInfo().getComponentName(), packageName);
mSessions.put(activityToken, newSession);
/*
* TODO(b/33197203): apply security checks below:
* - checks if disabled by secure settings / device policy
* - log operation using noteOp()
* - check flags
* - display disclosure if needed
*/
try {
final Bundle receiverExtras = new Bundle();
receiverExtras.putBinder(EXTRA_ACTIVITY_TOKEN, activityToken);
final long identity = Binder.clearCallingIdentity();
try {
if (!ActivityManager.getService().requestAutofillData(mAssistReceiver,
receiverExtras, activityToken)) {
Slog.w(TAG, "failed to request autofill data for " + activityToken);
}
} finally {
Binder.restoreCallingIdentity(identity);
}
} catch (RemoteException e) {
// Should not happen, it's a local call.
}
return newSession;
}
void updateSessionLocked(IBinder activityToken, AutofillId autofillId, Rect virtualBounds,
AutofillValue value, int flags) {
final Session session = mSessions.get(activityToken);
if (session == null) {
if (VERBOSE) {
Slog.v(TAG, "updateSessionLocked(): session gone for " + activityToken);
}
return;
}
session.updateLocked(autofillId, virtualBounds, value, flags);
}
void removeSessionLocked(IBinder activityToken) {
mSessions.remove(activityToken);
}
private void handleSessionSave(IBinder activityToken) {
synchronized (mLock) {
final Session session = mSessions.get(activityToken);
if (session == null) {
Slog.w(TAG, "handleSessionSave(): already gone: " + activityToken);
return;
}
session.callSaveLocked();
}
}
void destroyLocked() {
if (VERBOSE) {
Slog.v(TAG, "destroyLocked()");
}
for (Session session : mSessions.values()) {
session.destroyLocked();
}
mSessions.clear();
}
void disableSelf() {
final long identity = Binder.clearCallingIdentity();
try {
final String autoFillService = Settings.Secure.getStringForUser(
mContext.getContentResolver(), Settings.Secure.AUTOFILL_SERVICE, mUserId);
if (mInfo.getServiceInfo().getComponentName().equals(
ComponentName.unflattenFromString(autoFillService))) {
Settings.Secure.putStringForUser(mContext.getContentResolver(),
Settings.Secure.AUTOFILL_SERVICE, null, mUserId);
}
} finally {
Binder.restoreCallingIdentity(identity);
}
}
CharSequence getServiceLabel() {
return mInfo.getServiceInfo().loadLabel(mContext.getPackageManager());
}
void dumpLocked(String prefix, PrintWriter pw) {
final String prefix2 = prefix + " ";
pw.print(prefix); pw.print("Component:"); pw.println(mInfo != null
? mInfo.getServiceInfo().getComponentName() : null);
pw.print(prefix); pw.print("Disabled:"); pw.println(mDisabled);
if (VERBOSE && mInfo != null) {
// ServiceInfo dump is too noisy and redundant (it can be obtained through other dumps)
pw.print(prefix); pw.println("ServiceInfo:");
mInfo.getServiceInfo().dump(new PrintWriterPrinter(pw), prefix + prefix);
}
final int size = mSessions.size();
if (size == 0) {
pw.print(prefix); pw.println("No sessions");
} else {
pw.print(prefix); pw.print(size); pw.println(" sessions:");
for (int i = 0; i < size; i++) {
pw.print(prefix); pw.print("#"); pw.println(i + 1);
mSessions.valueAt(i).dumpLocked(prefix2, pw);
}
}
}
void destroySessionsLocked() {
for (Session session : mSessions.values()) {
session.removeSelf();
}
}
void listSessionsLocked(ArrayList<String> output) {
for (IBinder activityToken : mSessions.keySet()) {
output.add((mInfo != null ? mInfo.getServiceInfo().getComponentName()
: null) + ":" + activityToken);
}
}
private void sendStateToClients() {
final RemoteCallbackList<IAutoFillManagerClient> clients;
final int userClientCount;
synchronized (mLock) {
if (mClients == null) {
return;
}
clients = mClients;
userClientCount = clients.beginBroadcast();
}
try {
for (int i = 0; i < userClientCount; i++) {
final IAutoFillManagerClient client = clients.getBroadcastItem(i);
try {
client.setState(isEnabled());
} catch (RemoteException re) {
/* ignore */
}
}
} finally {
clients.finishBroadcast();
}
}
private boolean isEnabled() {
return mInfo != null && !mDisabled;
}
@Override
public String toString() {
return "AutofillManagerServiceImpl: [userId=" + mUserId
+ ", component=" + (mInfo != null
? mInfo.getServiceInfo().getComponentName() : null) + "]";
}
}