blob: 6a1613113c1014e6bd38afbcdda9684c0ed526f3 [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.Manifest.permission.MANAGE_AUTO_FILL;
import static android.content.Context.AUTO_FILL_MANAGER_SERVICE;
import static android.view.View.ASSIST_FLAG_SANITIZED_TEXT;
import static android.view.View.ASSIST_FLAG_NON_SANITIZED_TEXT;
import android.Manifest;
import android.app.AppGlobals;
import android.app.Notification;
import android.app.Notification.Action;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.content.pm.ServiceInfo;
import android.content.pm.UserInfo;
import android.database.ContentObserver;
import android.net.Uri;
import android.os.Binder;
import android.os.Handler;
import android.os.IBinder;
import android.os.Message;
import android.os.RemoteException;
import android.os.ResultReceiver;
import android.os.ShellCallback;
import android.os.SystemClock;
import android.os.UserHandle;
import android.os.UserManager;
import android.provider.Settings;
import android.service.autofill.IAutoFillManagerService;
import android.text.TextUtils;
import android.text.format.DateUtils;
import android.util.Log;
import android.util.Slog;
import android.util.SparseArray;
import android.util.TimeUtils;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.os.BackgroundThread;
import com.android.server.FgThread;
import com.android.server.SystemService;
import java.io.FileDescriptor;
import java.io.PrintWriter;
import java.util.List;
/**
* Entry point service for auto-fill management.
*
* <p>This service provides the {@link IAutoFillManagerService} implementation and keeps a list of
* {@link AutoFillManagerServiceImpl} per user; the real work is done by
* {@link AutoFillManagerServiceImpl} itself.
*/
public final class AutoFillManagerService extends SystemService {
private static final String TAG = "AutoFillManagerService";
static final boolean DEBUG = true; // TODO: change to false once stable
private static final long SERVICE_BINDING_LIFETIME_MS = 5 * DateUtils.MINUTE_IN_MILLIS;
private static final int ARG_NOT_USED = 0;
protected static final int MSG_UNBIND = 1;
private final AutoFillManagerServiceStub mServiceStub;
private final Context mContext;
private final ContentResolver mResolver;
private final Object mLock = new Object();
private final Handler mHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case MSG_UNBIND:
removeStaleServiceForUser(msg.arg1);
return;
case MSG_SHOW_ALL_NOTIFICATIONS:
showAllNotifications();
return;
default:
Slog.w(TAG, "Invalid message: " + msg);
}
}
};
/**
* Cache of {@link AutoFillManagerServiceImpl} per user id.
* <p>
* It has to be mapped by user id because the same current user could have simultaneous sessions
* associated to different user profiles (for example, in a multi-window environment or when
* device has work profiles).
* <p>
* Entries on this cache are added on demand and removed when:
* <ol>
* <li>An auto-fill service app is removed.
* <li>The {@link android.provider.Settings.Secure#AUTO_FILL_SERVICE} for an user change.
* <li>It has not been interacted with for {@link #SERVICE_BINDING_LIFETIME_MS} ms.
* </ol>
*/
@GuardedBy("mLock")
private SparseArray<AutoFillManagerServiceImpl> mServicesCache = new SparseArray<>();
public AutoFillManagerService(Context context) {
super(context);
mContext = context;
mResolver = context.getContentResolver();
mServiceStub = new AutoFillManagerServiceStub();
}
@Override
public void onStart() {
if (DEBUG) Slog.d(TAG, "onStart(): binding as " + AUTO_FILL_MANAGER_SERVICE);
publishBinderService(AUTO_FILL_MANAGER_SERVICE, mServiceStub);
}
@Override
public void onBootPhase(int phase) {
if (phase == PHASE_THIRD_PARTY_APPS_CAN_START) {
new SettingsObserver(BackgroundThread.getHandler());
}
if (phase == PHASE_BOOT_COMPLETED) {
// TODO: if sent right away, the notification is not displayed. Since the notification
// mechanism is a temporary approach anyways, just delay it..
if (DEBUG)
Slog.d(TAG, "Showing notifications in " + SHOW_ALL_NOTIFICATIONS_DELAY_MS + "ms");
mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG_SHOW_ALL_NOTIFICATIONS),
SHOW_ALL_NOTIFICATIONS_DELAY_MS);
}
}
private AutoFillManagerServiceImpl newServiceForUser(int userId) {
ComponentName serviceComponent = null;
ServiceInfo serviceInfo = null;
final String componentName = Settings.Secure.getStringForUser(
mResolver, Settings.Secure.AUTO_FILL_SERVICE, userId);
if (!TextUtils.isEmpty(componentName)) {
try {
serviceComponent = ComponentName.unflattenFromString(componentName);
serviceInfo =
AppGlobals.getPackageManager().getServiceInfo(serviceComponent, 0, userId);
} catch (RuntimeException | RemoteException e) {
Slog.wtf(TAG, "Bad auto-fill service name " + componentName, e);
return null;
}
}
if (DEBUG) Slog.d(TAG, "getServiceComponentForUser(" + userId + "): component="
+ serviceComponent + ", info: " + serviceInfo);
if (serviceInfo == null) {
Slog.w(TAG, "no service info for " + serviceComponent);
return null;
}
return new AutoFillManagerServiceImpl(this, mContext, mLock, FgThread.getHandler(), userId,
serviceInfo.applicationInfo.uid, serviceComponent, SERVICE_BINDING_LIFETIME_MS);
}
/**
* Gets the service instance for an user.
*
* <p>First it tries to return the existing instance from the cache; if it's not cached, it
* creates a new instance and caches it.
*/
private AutoFillManagerServiceImpl getServiceForUserLocked(int userId) {
AutoFillManagerServiceImpl service = mServicesCache.get(userId);
if (service != null) {
if (DEBUG) Log.d(TAG, "reusing cached service for userId " + userId);
service.setLifeExpectancy(SERVICE_BINDING_LIFETIME_MS);
} else {
service = newServiceForUser(userId);
if (service == null) {
// Already logged
return null;
}
if (DEBUG) Log.d(TAG, "creating new cached service for userId " + userId);
service.startLocked();
mServicesCache.put(userId, service);
}
// Keep service connection alive for a while, in case user needs to interact with it
// (for example, to save the data that was inputted in)
mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG_UNBIND, userId, ARG_NOT_USED),
SERVICE_BINDING_LIFETIME_MS);
return service;
}
/**
* Removes a cached service, but respecting its TTL.
*/
private void removeStaleServiceForUser(int userId) {
synchronized (mLock) {
removeCachedService(userId, false);
}
}
/**
* Removes a cached service, even if it has TTL.
*/
void removeCachedServiceForUserLocked(int userId) {
removeCachedService(userId, true);
}
private void removeCachedService(int userId, boolean force) {
if (DEBUG) Log.d(TAG, "removing cached service for userId " + userId);
final AutoFillManagerServiceImpl service = mServicesCache.get(userId);
if (service == null) {
Log.w(TAG, "removeCachedServiceForUser(): no cached service for userId " + userId);
return;
}
if (!force) {
// Check TTL first.
final long now = SystemClock.uptimeMillis();
if (service.mEstimateTimeOfDeath > now) {
if (DEBUG) {
final StringBuilder msg = new StringBuilder("service has some TTL left: ");
TimeUtils.formatDuration(service.mEstimateTimeOfDeath - now, msg);
Log.d(TAG, msg.toString());
}
return;
}
}
mServicesCache.delete(userId);
service.stopLocked();
}
final class AutoFillManagerServiceStub extends IAutoFillManagerService.Stub {
@Override
public void requestAutoFill(IBinder activityToken, int userId, int flags) {
if (DEBUG) Slog.d(TAG, "requestAutoFill: flags=" + flags + ", userId=" + userId);
mContext.enforceCallingPermission(MANAGE_AUTO_FILL, TAG);
synchronized (mLock) {
final AutoFillManagerServiceImpl service = getServiceForUserLocked(userId);
if (service != null) {
service.requestAutoFill(activityToken, flags);
}
}
}
@Override
public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
if (mContext.checkCallingPermission(
Manifest.permission.DUMP) != PackageManager.PERMISSION_GRANTED) {
pw.println("Permission Denial: can't dump autofill from from pid="
+ Binder.getCallingPid()
+ ", uid=" + Binder.getCallingUid());
return;
}
synchronized (mLock) {
final int size = mServicesCache.size();
pw.print("Cached services: ");
if (size == 0) {
pw.println("none");
} else {
pw.println(size);
for (int i = 0; i < size; i++) {
pw.print("\nService at index "); pw.println(i);
final AutoFillManagerServiceImpl impl = mServicesCache.valueAt(i);
impl.dumpLocked(" ", pw);
}
}
}
}
@Override
public void onShellCommand(FileDescriptor in, FileDescriptor out, FileDescriptor err,
String[] args, ShellCallback callback, ResultReceiver resultReceiver) {
(new AutoFillManagerServiceShellCommand(this)).exec(
this, in, out, err, args, callback, resultReceiver);
}
}
private final class SettingsObserver extends ContentObserver {
SettingsObserver(Handler handler) {
super(handler);
ContentResolver resolver = mContext.getContentResolver();
resolver.registerContentObserver(Settings.Secure.getUriFor(
Settings.Secure.AUTO_FILL_SERVICE), false, this, UserHandle.USER_ALL);
}
@Override
public void onChange(boolean selfChange, Uri uri, int userId) {
if (DEBUG) Slog.d(TAG, "settings (" + uri + " changed for " + userId);
synchronized (mLock) {
removeCachedServiceForUserLocked(userId);
final ComponentName serviceComponent = getProviderForUser(userId);
if (serviceComponent == null) {
cancelNotificationLocked(userId);
} else {
showNotification(serviceComponent, userId);
}
}
}
}
////////////////////////////////////////////////////////////////////////////
// TODO: temporary code using a notification to request auto-fill. //
// Will be removed once UX decide the right way to present it to the user //
////////////////////////////////////////////////////////////////////////////
// TODO: remove from frameworks/base/core/res/AndroidManifest.xml once it's not used anymore
private static final String NOTIFICATION_AUTO_FILL_INTENT =
"com.android.internal.autofill.action.REQUEST_AUTOFILL";
private static final String EXTRA_USER_ID = "user_id";
private static final String EXTRA_FLAGS = "flags";
private static final int MSG_SHOW_ALL_NOTIFICATIONS = 42;
private static final int SHOW_ALL_NOTIFICATIONS_DELAY_MS = 5000;
private BroadcastReceiver mNotificationReceiver;
final class NotificationReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
final int userId = intent.getIntExtra(EXTRA_USER_ID, -1);
final int flags = intent.getIntExtra(EXTRA_FLAGS, 0);
if (DEBUG) Slog.d(TAG, "Requesting autofill by notification for user " + userId);
synchronized (mLock) {
final AutoFillManagerServiceImpl service = getServiceForUserLocked(userId);
if (service == null) {
Slog.w(TAG, "no auto-fill service for user " + userId);
} else {
service.requestAutoFill(null, flags);
}
}
}
}
private ComponentName getProviderForUser(int userId) {
ComponentName serviceComponent = null;
ServiceInfo serviceInfo = null;
final String componentName = Settings.Secure.getStringForUser(
mResolver, Settings.Secure.AUTO_FILL_SERVICE, userId);
if (!TextUtils.isEmpty(componentName)) {
try {
serviceComponent = ComponentName.unflattenFromString(componentName);
serviceInfo =
AppGlobals.getPackageManager().getServiceInfo(serviceComponent, 0, userId);
} catch (RuntimeException | RemoteException e) {
Slog.wtf(TAG, "Bad auto-fill service name " + componentName, e);
return null;
}
}
if (DEBUG) Slog.d(TAG, "getServiceComponentForUser(" + userId + "): component="
+ serviceComponent + ", info: " + serviceInfo);
if (serviceInfo == null) {
Slog.w(TAG, "no service info for " + serviceComponent);
return null;
}
return serviceComponent;
}
private void showAllNotifications() {
final UserManager userManager =
(UserManager) mContext.getSystemService(Context.USER_SERVICE);
final List<UserInfo> allUsers = userManager.getUsers(true);
for (UserInfo user : allUsers) {
final ComponentName serviceComponent = getProviderForUser(user.id);
if (serviceComponent != null) {
showNotification(serviceComponent, user.id);
}
}
}
private void showNotification(ComponentName serviceComponent, int userId) {
if (DEBUG) Log.d(TAG, "showNotification() for " + userId + ": " + serviceComponent);
synchronized (mLock) {
if (mNotificationReceiver == null) {
mNotificationReceiver = new NotificationReceiver();
mContext.registerReceiver(mNotificationReceiver,
new IntentFilter(NOTIFICATION_AUTO_FILL_INTENT));
}
}
final Intent fillIntent = new Intent(NOTIFICATION_AUTO_FILL_INTENT);
fillIntent.putExtra(EXTRA_USER_ID, userId);
fillIntent.putExtra(EXTRA_FLAGS, ASSIST_FLAG_SANITIZED_TEXT);
final PendingIntent fillPendingIntent = PendingIntent.getBroadcast(mContext,
ASSIST_FLAG_SANITIZED_TEXT, fillIntent, PendingIntent.FLAG_UPDATE_CURRENT);
final Action fillAction = new Action.Builder(null, "FILL", fillPendingIntent).build();
final Intent saveIntent = new Intent(NOTIFICATION_AUTO_FILL_INTENT);
saveIntent.putExtra(EXTRA_USER_ID, userId);
saveIntent.putExtra(EXTRA_FLAGS, ASSIST_FLAG_NON_SANITIZED_TEXT);
final PendingIntent savePendingIntent = PendingIntent.getBroadcast(mContext,
ASSIST_FLAG_NON_SANITIZED_TEXT, saveIntent, PendingIntent.FLAG_UPDATE_CURRENT);
final Action saveAction = new Action.Builder(null, "SAVE", savePendingIntent).build();
final String packageName = serviceComponent.getPackageName();
String providerName = null;
final PackageManager pm = mContext.getPackageManager();
try {
final ApplicationInfo info = pm.getApplicationInfoAsUser(packageName, 0, userId);
if (info != null) {
providerName = pm.getApplicationLabel(info).toString();
}
} catch (Exception e) {
providerName = packageName;
}
final String title = "AutoFill actions";
final String subTitle = "Provider: " + providerName + "\n" + "User: " + userId;
final Notification notification = new Notification.Builder(mContext)
.setCategory(Notification.CATEGORY_SYSTEM)
.setOngoing(true)
.setSmallIcon(com.android.internal.R.drawable.stat_sys_adb)
.setLocalOnly(true)
.setColor(mContext.getColor(
com.android.internal.R.color.system_notification_accent_color))
.setContentTitle(title)
.setStyle(new Notification.BigTextStyle().bigText(subTitle))
.setActions(fillAction, saveAction)
.build();
NotificationManager.from(mContext).notify(userId, notification);
}
private void cancelNotificationLocked(int userId) {
if (DEBUG) Log.d(TAG, "cancelNotificationLocked(): " + userId);
NotificationManager.from(mContext).cancel(userId);
}
/////////////////////////////////////////
// End of temporary notification code. //
/////////////////////////////////////////
}