blob: 62bb9f155c340e0b23ec20f8348caea6cab06569 [file] [log] [blame]
/*
* 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.server.companion;
import static android.Manifest.permission.MANAGE_COMPANION_DEVICES;
import static android.content.pm.PackageManager.CERT_INPUT_SHA256;
import static android.content.pm.PackageManager.PERMISSION_GRANTED;
import static android.os.Process.SYSTEM_UID;
import static android.os.UserHandle.getCallingUserId;
import static com.android.internal.util.CollectionUtils.any;
import static com.android.internal.util.Preconditions.checkState;
import static com.android.internal.util.function.pooled.PooledLambda.obtainMessage;
import static com.android.server.companion.AssociationStore.CHANGE_TYPE_UPDATED_ADDRESS_UNCHANGED;
import static com.android.server.companion.MetricUtils.logCreateAssociation;
import static com.android.server.companion.MetricUtils.logRemoveAssociation;
import static com.android.server.companion.PackageUtils.enforceUsesCompanionDeviceFeature;
import static com.android.server.companion.PackageUtils.getPackageInfo;
import static com.android.server.companion.PermissionsUtils.checkCallerCanManageCompanionDevice;
import static com.android.server.companion.PermissionsUtils.enforceCallerCanManageAssociationsForPackage;
import static com.android.server.companion.PermissionsUtils.enforceCallerCanManageCompanionDevice;
import static com.android.server.companion.PermissionsUtils.enforceCallerIsSystemOr;
import static com.android.server.companion.PermissionsUtils.enforceCallerIsSystemOrCanInteractWithUserId;
import static com.android.server.companion.PermissionsUtils.sanitizeWithCallerChecks;
import static com.android.server.companion.RolesUtils.addRoleHolderForAssociation;
import static com.android.server.companion.RolesUtils.removeRoleHolderForAssociation;
import static java.util.Objects.requireNonNull;
import static java.util.concurrent.TimeUnit.DAYS;
import static java.util.concurrent.TimeUnit.MINUTES;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.SuppressLint;
import android.annotation.UserIdInt;
import android.app.ActivityManagerInternal;
import android.app.AppOpsManager;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.companion.AssociationInfo;
import android.companion.AssociationRequest;
import android.companion.DeviceNotAssociatedException;
import android.companion.IAssociationRequestCallback;
import android.companion.ICompanionDeviceManager;
import android.companion.IOnAssociationsChangedListener;
import android.content.ComponentName;
import android.content.Context;
import android.content.SharedPreferences;
import android.content.pm.PackageInfo;
import android.content.pm.PackageItemInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManagerInternal;
import android.content.pm.UserInfo;
import android.net.MacAddress;
import android.net.NetworkPolicyManager;
import android.os.Binder;
import android.os.Environment;
import android.os.Handler;
import android.os.Message;
import android.os.Parcel;
import android.os.PowerWhitelistManager;
import android.os.RemoteCallbackList;
import android.os.RemoteException;
import android.os.ResultReceiver;
import android.os.ServiceManager;
import android.os.ShellCallback;
import android.os.UserHandle;
import android.os.UserManager;
import android.text.BidiFormatter;
import android.util.ArraySet;
import android.util.ExceptionUtils;
import android.util.Log;
import android.util.Slog;
import android.util.SparseArray;
import android.util.SparseBooleanArray;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.app.IAppOpsService;
import com.android.internal.content.PackageMonitor;
import com.android.internal.notification.NotificationAccessConfirmationActivityContract;
import com.android.internal.os.BackgroundThread;
import com.android.internal.util.ArrayUtils;
import com.android.internal.util.DumpUtils;
import com.android.server.FgThread;
import com.android.server.LocalServices;
import com.android.server.SystemService;
import com.android.server.companion.presence.CompanionDevicePresenceMonitor;
import com.android.server.pm.UserManagerInternal;
import java.io.File;
import java.io.FileDescriptor;
import java.io.PrintWriter;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
@SuppressLint("LongLogTag")
public class CompanionDeviceManagerService extends SystemService {
static final String TAG = "CompanionDeviceManagerService";
static final boolean DEBUG = false;
/** Range of Association IDs allocated for a user.*/
private static final int ASSOCIATIONS_IDS_PER_USER_RANGE = 100000;
private static final long PAIR_WITHOUT_PROMPT_WINDOW_MS = 10 * 60 * 1000; // 10 min
private static final String PREF_FILE_NAME = "companion_device_preferences.xml";
private static final String PREF_KEY_AUTO_REVOKE_GRANTS_DONE = "auto_revoke_grants_done";
private static final long ASSOCIATION_CLEAN_UP_TIME_WINDOW = DAYS.toMillis(3 * 30); // 3 months
private PersistentDataStore mPersistentStore;
private final PersistUserStateHandler mUserPersistenceHandler;
private final AssociationStoreImpl mAssociationStore;
private AssociationRequestsProcessor mAssociationRequestsProcessor;
private CompanionDevicePresenceMonitor mDevicePresenceMonitor;
private CompanionApplicationController mCompanionAppController;
private final ActivityManagerInternal mAmInternal;
private final IAppOpsService mAppOpsManager;
private final PowerWhitelistManager mPowerWhitelistManager;
private final UserManager mUserManager;
final PackageManagerInternal mPackageManagerInternal;
/**
* A structure that consists of two nested maps, and effectively maps (userId + packageName) to
* a list of IDs that have been previously assigned to associations for that package.
* We maintain this structure so that we never re-use association IDs for the same package
* (until it's uninstalled).
*/
@GuardedBy("mPreviouslyUsedIds")
private final SparseArray<Map<String, Set<Integer>>> mPreviouslyUsedIds = new SparseArray<>();
private final RemoteCallbackList<IOnAssociationsChangedListener> mListeners =
new RemoteCallbackList<>();
public CompanionDeviceManagerService(Context context) {
super(context);
mPowerWhitelistManager = context.getSystemService(PowerWhitelistManager.class);
mAppOpsManager = IAppOpsService.Stub.asInterface(
ServiceManager.getService(Context.APP_OPS_SERVICE));
mAmInternal = LocalServices.getService(ActivityManagerInternal.class);
mPackageManagerInternal = LocalServices.getService(PackageManagerInternal.class);
mUserManager = context.getSystemService(UserManager.class);
mUserPersistenceHandler = new PersistUserStateHandler();
mAssociationStore = new AssociationStoreImpl();
}
@Override
public void onStart() {
mPersistentStore = new PersistentDataStore();
loadAssociationsFromDisk();
mAssociationStore.registerListener(mAssociationStoreChangeListener);
mDevicePresenceMonitor = new CompanionDevicePresenceMonitor(
mAssociationStore, mDevicePresenceCallback);
mAssociationRequestsProcessor = new AssociationRequestsProcessor(
/* cdmService */this, mAssociationStore);
final Context context = getContext();
mCompanionAppController = new CompanionApplicationController(
context, mApplicationControllerCallback);
// Publish "binder" service.
final CompanionDeviceManagerImpl impl = new CompanionDeviceManagerImpl();
publishBinderService(Context.COMPANION_DEVICE_SERVICE, impl);
// Publish "local" service.
LocalServices.addService(CompanionDeviceManagerServiceInternal.class, new LocalService());
}
void loadAssociationsFromDisk() {
final Set<AssociationInfo> allAssociations = new ArraySet<>();
synchronized (mPreviouslyUsedIds) {
// The data is stored in DE directories, so we can read the data for all users now
// (which would not be possible if the data was stored to CE directories).
mPersistentStore.readStateForUsers(
mUserManager.getAliveUsers(), allAssociations, mPreviouslyUsedIds);
}
mAssociationStore.setAssociations(allAssociations);
}
@Override
public void onBootPhase(int phase) {
final Context context = getContext();
if (phase == PHASE_SYSTEM_SERVICES_READY) {
// WARNING: moving PackageMonitor to another thread (Looper) may introduce significant
// delays (even in case of the Main Thread). It may be fine overall, but would require
// updating the tests (adding a delay there).
mPackageMonitor.register(context, FgThread.get().getLooper(), UserHandle.ALL, true);
mDevicePresenceMonitor.init(context);
} else if (phase == PHASE_BOOT_COMPLETED) {
// Run the Association CleanUp job service daily.
AssociationCleanUpService.schedule(getContext());
}
}
@Override
public void onUserUnlocking(@NonNull TargetUser user) {
final int userId = user.getUserIdentifier();
final List<AssociationInfo> associations = mAssociationStore.getAssociationsForUser(userId);
if (associations.isEmpty()) return;
updateAtm(userId, associations);
BackgroundThread.getHandler().sendMessageDelayed(
obtainMessage(CompanionDeviceManagerService::maybeGrantAutoRevokeExemptions, this),
MINUTES.toMillis(10));
}
@Nullable
AssociationInfo getAssociationWithCallerChecks(
@UserIdInt int userId, @NonNull String packageName, @NonNull String macAddress) {
final AssociationInfo association = mAssociationStore.getAssociationsForPackageWithAddress(
userId, packageName, macAddress);
return sanitizeWithCallerChecks(getContext(), association);
}
@Nullable
AssociationInfo getAssociationWithCallerChecks(int associationId) {
final AssociationInfo association = mAssociationStore.getAssociationById(associationId);
return sanitizeWithCallerChecks(getContext(), association);
}
private void onDeviceAppearedInternal(int associationId) {
if (DEBUG) Log.i(TAG, "onDevice_Appeared_Internal() id=" + associationId);
final AssociationInfo association = mAssociationStore.getAssociationById(associationId);
if (DEBUG) Log.d(TAG, " association=" + associationId);
if (!association.shouldBindWhenPresent()) return;
final int userId = association.getUserId();
final String packageName = association.getPackageName();
// Set bindImportant to true when the association is self-managed to avoid the target
// service being killed.
final boolean bindImportant = association.isSelfManaged();
if (!mCompanionAppController.isCompanionApplicationBound(userId, packageName)) {
mCompanionAppController.bindCompanionApplication(userId, packageName, bindImportant);
} else if (DEBUG) {
Log.i(TAG, "u" + userId + "\\" + packageName + " is already bound");
}
mCompanionAppController.notifyCompanionApplicationDeviceAppeared(association);
}
private void onDeviceDisappearedInternal(int associationId) {
if (DEBUG) Log.i(TAG, "onDevice_Disappeared_Internal() id=" + associationId);
final AssociationInfo association = mAssociationStore.getAssociationById(associationId);
if (DEBUG) Log.d(TAG, " association=" + associationId);
final int userId = association.getUserId();
final String packageName = association.getPackageName();
if (!mCompanionAppController.isCompanionApplicationBound(userId, packageName)) {
if (DEBUG) Log.w(TAG, "u" + userId + "\\" + packageName + " is NOT bound");
return;
}
if (association.shouldBindWhenPresent()) {
mCompanionAppController.notifyCompanionApplicationDeviceDisappeared(association);
}
// Check if there are other devices associated to the app that are present.
if (shouldBindPackage(userId, packageName)) return;
mCompanionAppController.unbindCompanionApplication(userId, packageName);
}
private boolean onCompanionApplicationBindingDiedInternal(
@UserIdInt int userId, @NonNull String packageName) {
// Update the current connected devices sets when binderDied, so that application is able
// to call notifyDeviceAppeared after re-launch the application.
for (AssociationInfo ai :
mAssociationStore.getAssociationsForPackage(userId, packageName)) {
int id = ai.getId();
Slog.i(TAG, "Removing association id: " + id + " for package: "
+ packageName + " due to binderDied.");
mDevicePresenceMonitor.removeDeviceFromMonitoring(id);
}
// TODO(b/218613015): implement.
return false;
}
private void onRebindCompanionApplicationTimeoutInternal(
@UserIdInt int userId, @NonNull String packageName) {
// TODO(b/218613015): implement.
}
/**
* @return whether the package should be bound (i.e. at least one of the devices associated with
* the package is currently present).
*/
private boolean shouldBindPackage(@UserIdInt int userId, @NonNull String packageName) {
final List<AssociationInfo> packageAssociations =
mAssociationStore.getAssociationsForPackage(userId, packageName);
for (AssociationInfo association : packageAssociations) {
if (!association.shouldBindWhenPresent()) continue;
if (mDevicePresenceMonitor.isDevicePresent(association.getId())) return true;
}
return false;
}
private void onAssociationChangedInternal(
@AssociationStore.ChangeType int changeType, AssociationInfo association) {
final int id = association.getId();
final int userId = association.getUserId();
final String packageName = association.getPackageName();
if (changeType == AssociationStore.CHANGE_TYPE_REMOVED) {
markIdAsPreviouslyUsedForPackage(id, userId, packageName);
}
final List<AssociationInfo> updatedAssociations =
mAssociationStore.getAssociationsForUser(userId);
mUserPersistenceHandler.postPersistUserState(userId);
// Notify listeners if ADDED, REMOVED or UPDATED_ADDRESS_CHANGED.
// Do NOT notify when UPDATED_ADDRESS_UNCHANGED, which means a minor tweak in association's
// configs, which "listeners" won't (and shouldn't) be able to see.
if (changeType != CHANGE_TYPE_UPDATED_ADDRESS_UNCHANGED) {
notifyListeners(userId, updatedAssociations);
}
updateAtm(userId, updatedAssociations);
}
private void persistStateForUser(@UserIdInt int userId) {
final List<AssociationInfo> updatedAssociations =
mAssociationStore.getAssociationsForUser(userId);
final Map<String, Set<Integer>> usedIdsForUser = getPreviouslyUsedIdsForUser(userId);
mPersistentStore.persistStateForUser(userId, updatedAssociations, usedIdsForUser);
}
private void notifyListeners(
@UserIdInt int userId, @NonNull List<AssociationInfo> associations) {
mListeners.broadcast((listener, callbackUserId) -> {
if ((int) callbackUserId == userId) {
try {
listener.onAssociationsChanged(associations);
} catch (RemoteException ignored) {
}
}
});
}
private void markIdAsPreviouslyUsedForPackage(
int associationId, @UserIdInt int userId, @NonNull String packageName) {
synchronized (mPreviouslyUsedIds) {
Map<String, Set<Integer>> usedIdsForUser = mPreviouslyUsedIds.get(userId);
if (usedIdsForUser == null) {
usedIdsForUser = new HashMap<>();
mPreviouslyUsedIds.put(userId, usedIdsForUser);
}
final Set<Integer> usedIdsForPackage =
usedIdsForUser.computeIfAbsent(packageName, it -> new HashSet<>());
usedIdsForPackage.add(associationId);
}
}
private void onPackageRemoveOrDataClearedInternal(
@UserIdInt int userId, @NonNull String packageName) {
if (DEBUG) {
Log.i(TAG, "onPackageRemove_Or_DataCleared() u" + userId + "/"
+ packageName);
}
// Clear associations.
final List<AssociationInfo> associationsForPackage =
mAssociationStore.getAssociationsForPackage(userId, packageName);
for (AssociationInfo association : associationsForPackage) {
mAssociationStore.removeAssociation(association.getId());
}
mCompanionAppController.onPackagesChanged(userId);
}
private void onPackageModifiedInternal(@UserIdInt int userId, @NonNull String packageName) {
if (DEBUG) Log.i(TAG, "onPackageModified() u" + userId + "/" + packageName);
final List<AssociationInfo> associationsForPackage =
mAssociationStore.getAssociationsForPackage(userId, packageName);
for (AssociationInfo association : associationsForPackage) {
updateSpecialAccessPermissionForAssociatedPackage(association);
}
mCompanionAppController.onPackagesChanged(userId);
}
// Revoke associations if the selfManaged companion device does not connect for 3
// months for specific profile.
private void associationCleanUp(String profile) {
for (AssociationInfo ai : mAssociationStore.getAssociations()) {
if (ai.isSelfManaged()
&& profile.equals(ai.getDeviceProfile())
&& System.currentTimeMillis() - ai.getLastTimeConnectedMs()
>= ASSOCIATION_CLEAN_UP_TIME_WINDOW) {
Slog.i(TAG, "Removing the association for associationId: "
+ ai.getId()
+ " due to the device does not connect for 3 months.");
disassociateInternal(ai.getId());
}
}
}
class CompanionDeviceManagerImpl extends ICompanionDeviceManager.Stub {
@Override
public boolean onTransact(int code, Parcel data, Parcel reply, int flags)
throws RemoteException {
try {
return super.onTransact(code, data, reply, flags);
} catch (Throwable e) {
Slog.e(TAG, "Error during IPC", e);
throw ExceptionUtils.propagate(e, RemoteException.class);
}
}
@Override
public void associate(AssociationRequest request, IAssociationRequestCallback callback,
String packageName, int userId) throws RemoteException {
Slog.i(TAG, "associate() "
+ "request=" + request + ", "
+ "package=u" + userId + "/" + packageName);
enforceCallerCanManageAssociationsForPackage(getContext(), userId, packageName,
"create associations");
mAssociationRequestsProcessor.processNewAssociationRequest(
request, packageName, userId, callback);
}
@Override
public List<AssociationInfo> getAssociations(String packageName, int userId) {
enforceCallerCanManageAssociationsForPackage(getContext(), userId, packageName,
"get associations");
if (!checkCallerCanManageCompanionDevice(getContext())) {
// If the caller neither is system nor holds MANAGE_COMPANION_DEVICES: it needs to
// request the feature (also: the caller is the app itself).
enforceUsesCompanionDeviceFeature(getContext(), userId, packageName);
}
return mAssociationStore.getAssociationsForPackage(userId, packageName);
}
@Override
public List<AssociationInfo> getAllAssociationsForUser(int userId) throws RemoteException {
enforceCallerIsSystemOrCanInteractWithUserId(getContext(), userId);
enforceCallerCanManageCompanionDevice(getContext(), "getAllAssociationsForUser");
return mAssociationStore.getAssociationsForUser(userId);
}
@Override
public void addOnAssociationsChangedListener(IOnAssociationsChangedListener listener,
int userId) {
enforceCallerIsSystemOrCanInteractWithUserId(getContext(), userId);
enforceCallerCanManageCompanionDevice(getContext(),
"addOnAssociationsChangedListener");
mListeners.register(listener, userId);
}
@Override
public void removeOnAssociationsChangedListener(IOnAssociationsChangedListener listener,
int userId) {
enforceCallerIsSystemOrCanInteractWithUserId(getContext(), userId);
enforceCallerCanManageCompanionDevice(
getContext(), "removeOnAssociationsChangedListener");
mListeners.unregister(listener);
}
@Override
public void legacyDisassociate(String deviceMacAddress, String packageName, int userId) {
if (DEBUG) {
Log.i(TAG, "legacyDisassociate() pkg=u" + userId + "/" + packageName
+ ", macAddress=" + deviceMacAddress);
}
requireNonNull(deviceMacAddress);
requireNonNull(packageName);
final AssociationInfo association =
getAssociationWithCallerChecks(userId, packageName, deviceMacAddress);
if (association == null) {
throw new IllegalArgumentException("Association does not exist "
+ "or the caller does not have permissions to manage it "
+ "(ie. it belongs to a different package or a different user).");
}
disassociateInternal(association.getId());
}
@Override
public void disassociate(int associationId) {
if (DEBUG) Log.i(TAG, "disassociate() associationId=" + associationId);
final AssociationInfo association = getAssociationWithCallerChecks(associationId);
if (association == null) {
throw new IllegalArgumentException("Association with ID " + associationId + " "
+ "does not exist "
+ "or belongs to a different package "
+ "or belongs to a different user");
}
disassociateInternal(associationId);
}
@Override
public PendingIntent requestNotificationAccess(ComponentName component, int userId)
throws RemoteException {
String callingPackage = component.getPackageName();
checkCanCallNotificationApi(callingPackage);
// TODO: check userId.
String packageTitle = BidiFormatter.getInstance().unicodeWrap(
getPackageInfo(getContext(), userId, callingPackage)
.applicationInfo
.loadSafeLabel(getContext().getPackageManager(),
PackageItemInfo.DEFAULT_MAX_LABEL_SIZE_PX,
PackageItemInfo.SAFE_LABEL_FLAG_TRIM
| PackageItemInfo.SAFE_LABEL_FLAG_FIRST_LINE)
.toString());
final long identity = Binder.clearCallingIdentity();
try {
return PendingIntent.getActivityAsUser(getContext(),
0 /* request code */,
NotificationAccessConfirmationActivityContract.launcherIntent(
getContext(), userId, component, packageTitle),
PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_ONE_SHOT
| PendingIntent.FLAG_CANCEL_CURRENT,
null /* options */,
new UserHandle(userId));
} finally {
Binder.restoreCallingIdentity(identity);
}
}
/**
* @deprecated Use
* {@link NotificationManager#isNotificationListenerAccessGranted(ComponentName)} instead.
*/
@Deprecated
@Override
public boolean hasNotificationAccess(ComponentName component) throws RemoteException {
checkCanCallNotificationApi(component.getPackageName());
NotificationManager nm = getContext().getSystemService(NotificationManager.class);
return nm.isNotificationListenerAccessGranted(component);
}
@Override
public boolean isDeviceAssociatedForWifiConnection(String packageName, String macAddress,
int userId) {
getContext().enforceCallingOrSelfPermission(
MANAGE_COMPANION_DEVICES, "isDeviceAssociated");
boolean bypassMacPermission = getContext().getPackageManager().checkPermission(
android.Manifest.permission.COMPANION_APPROVE_WIFI_CONNECTIONS, packageName)
== PERMISSION_GRANTED;
if (bypassMacPermission) {
return true;
}
return any(mAssociationStore.getAssociationsForPackage(userId, packageName),
a -> a.isLinkedTo(macAddress));
}
@Override
public void registerDevicePresenceListenerService(String deviceAddress,
String callingPackage, int userId) throws RemoteException {
// TODO: take the userId into account.
registerDevicePresenceListenerActive(callingPackage, deviceAddress, true);
}
@Override
public void unregisterDevicePresenceListenerService(String deviceAddress,
String callingPackage, int userId) throws RemoteException {
// TODO: take the userId into account.
registerDevicePresenceListenerActive(callingPackage, deviceAddress, false);
}
@Override
public void dispatchMessage(int messageId, int associationId, byte[] message)
throws RemoteException {
// TODO(b/199427116): implement.
}
@Override
public void notifyDeviceAppeared(int associationId) {
if (DEBUG) Log.i(TAG, "notifyDevice_Appeared() id=" + associationId);
AssociationInfo association = getAssociationWithCallerChecks(associationId);
if (association == null) {
throw new IllegalArgumentException("Association with ID " + associationId + " "
+ "does not exist "
+ "or belongs to a different package "
+ "or belongs to a different user");
}
if (!association.isSelfManaged()) {
throw new IllegalArgumentException("Association with ID " + associationId
+ " is not self-managed. notifyDeviceAppeared(int) can only be called for"
+ " self-managed associations.");
}
// AssociationInfo class is immutable: create a new AssociationInfo object with updated
// timestamp.
association = AssociationInfo.builder(association)
.setLastTimeConnected(System.currentTimeMillis())
.build();
mAssociationStore.updateAssociation(association);
mDevicePresenceMonitor.onSelfManagedDeviceConnected(associationId);
}
@Override
public void notifyDeviceDisappeared(int associationId) {
if (DEBUG) Log.i(TAG, "notifyDevice_Disappeared() id=" + associationId);
final AssociationInfo association = getAssociationWithCallerChecks(associationId);
if (association == null) {
throw new IllegalArgumentException("Association with ID " + associationId + " "
+ "does not exist "
+ "or belongs to a different package "
+ "or belongs to a different user");
}
if (!association.isSelfManaged()) {
throw new IllegalArgumentException("Association with ID " + associationId
+ " is not self-managed. notifyDeviceAppeared(int) can only be called for"
+ " self-managed associations.");
}
mDevicePresenceMonitor.onSelfManagedDeviceDisconnected(associationId);
}
private void registerDevicePresenceListenerActive(String packageName, String deviceAddress,
boolean active) throws RemoteException {
getContext().enforceCallingOrSelfPermission(
android.Manifest.permission.REQUEST_OBSERVE_COMPANION_DEVICE_PRESENCE,
"[un]registerDevicePresenceListenerService");
final int userId = getCallingUserId();
enforceCallerIsSystemOr(userId, packageName);
AssociationInfo association = mAssociationStore.getAssociationsForPackageWithAddress(
userId, packageName, deviceAddress);
if (association == null) {
throw new RemoteException(new DeviceNotAssociatedException("App " + packageName
+ " is not associated with device " + deviceAddress
+ " for user " + userId));
}
// AssociationInfo class is immutable: create a new AssociationInfo object with updated
// flag.
association = AssociationInfo.builder(association)
.setNotifyOnDeviceNearby(active)
.build();
// Do not need to call {@link BleCompanionDeviceScanner#restartScan()} since it will
// trigger {@link BleCompanionDeviceScanner#restartScan(int, AssociationInfo)} when
// an application sets/unsets the mNotifyOnDeviceNearby flag.
mAssociationStore.updateAssociation(association);
// TODO(b/218615198): correctly handle the case when the device is currently present.
}
@Override
public void createAssociation(String packageName, String macAddress, int userId,
byte[] certificate) {
if (!getContext().getPackageManager().hasSigningCertificate(
packageName, certificate, CERT_INPUT_SHA256)) {
Slog.e(TAG, "Given certificate doesn't match the package certificate.");
return;
}
getContext().enforceCallingOrSelfPermission(
android.Manifest.permission.ASSOCIATE_COMPANION_DEVICES, "createAssociation");
legacyCreateAssociation(userId, macAddress, packageName, null);
}
private void checkCanCallNotificationApi(String callingPackage) {
final int userId = getCallingUserId();
enforceCallerIsSystemOr(userId, callingPackage);
if (getCallingUid() == SYSTEM_UID) return;
enforceUsesCompanionDeviceFeature(getContext(), userId, callingPackage);
checkState(!ArrayUtils.isEmpty(
mAssociationStore.getAssociationsForPackage(userId, callingPackage)),
"App must have an association before calling this API");
}
@Override
public boolean canPairWithoutPrompt(String packageName, String macAddress, int userId) {
final AssociationInfo association =
mAssociationStore.getAssociationsForPackageWithAddress(
userId, packageName, macAddress);
if (association == null) {
return false;
}
return System.currentTimeMillis() - association.getTimeApprovedMs()
< PAIR_WITHOUT_PROMPT_WINDOW_MS;
}
@Override
public void onShellCommand(FileDescriptor in, FileDescriptor out, FileDescriptor err,
String[] args, ShellCallback callback, ResultReceiver resultReceiver)
throws RemoteException {
enforceCallerCanManageCompanionDevice(getContext(), "onShellCommand");
new CompanionDeviceShellCommand(
CompanionDeviceManagerService.this, mAssociationStore)
.exec(this, in, out, err, args, callback, resultReceiver);
}
@Override
public void dump(@NonNull FileDescriptor fd, @NonNull PrintWriter out,
@Nullable String[] args) {
if (!DumpUtils.checkDumpAndUsageStatsPermission(getContext(), TAG, out)) {
return;
}
// TODO(b/218615185): mAssociationStore.dump() instead
out.append("Companion Device Associations:").append('\n');
for (AssociationInfo a : mAssociationStore.getAssociations()) {
out.append(" ").append(a.toString()).append('\n');
}
// TODO(b/218615185): mDevicePresenceMonitor.dump()
// TODO(b/218615185): mCompanionAppController.dump()
}
}
/**
* @deprecated use
* {@link #createAssociation(int, String, MacAddress, CharSequence, String, boolean)}
*/
@Deprecated
void legacyCreateAssociation(@UserIdInt int userId, @NonNull String deviceMacAddress,
@NonNull String packageName, @Nullable String deviceProfile) {
final MacAddress macAddress = MacAddress.fromString(deviceMacAddress);
createAssociation(userId, packageName, macAddress, null, deviceProfile, false);
}
AssociationInfo createAssociation(@UserIdInt int userId, @NonNull String packageName,
@Nullable MacAddress macAddress, @Nullable CharSequence displayName,
@Nullable String deviceProfile, boolean selfManaged) {
final int id = getNewAssociationIdForPackage(userId, packageName);
final long timestamp = System.currentTimeMillis();
final AssociationInfo association = new AssociationInfo(id, userId, packageName,
macAddress, displayName, deviceProfile, selfManaged, false, timestamp,
Long.MAX_VALUE);
Slog.i(TAG, "New CDM association created=" + association);
mAssociationStore.addAssociation(association);
// If the "Device Profile" is specified, make the companion application a holder of the
// corresponding role.
if (deviceProfile != null) {
addRoleHolderForAssociation(getContext(), association);
}
updateSpecialAccessPermissionForAssociatedPackage(association);
logCreateAssociation(deviceProfile);
return association;
}
@NonNull
private Map<String, Set<Integer>> getPreviouslyUsedIdsForUser(@UserIdInt int userId) {
synchronized (mPreviouslyUsedIds) {
return getPreviouslyUsedIdsForUserLocked(userId);
}
}
@GuardedBy("mPreviouslyUsedIds")
@NonNull
private Map<String, Set<Integer>> getPreviouslyUsedIdsForUserLocked(@UserIdInt int userId) {
final Map<String, Set<Integer>> usedIdsForUser = mPreviouslyUsedIds.get(userId);
if (usedIdsForUser == null) {
return Collections.emptyMap();
}
return deepUnmodifiableCopy(usedIdsForUser);
}
@GuardedBy("mPreviouslyUsedIds")
@NonNull
private Set<Integer> getPreviouslyUsedIdsForPackageLocked(
@UserIdInt int userId, @NonNull String packageName) {
// "Deeply unmodifiable" map: the map itself and the Set<Integer> values it contains are all
// unmodifiable.
final Map<String, Set<Integer>> usedIdsForUser = getPreviouslyUsedIdsForUserLocked(userId);
final Set<Integer> usedIdsForPackage = usedIdsForUser.get(packageName);
if (usedIdsForPackage == null) {
return Collections.emptySet();
}
//The set is already unmodifiable.
return usedIdsForPackage;
}
private int getNewAssociationIdForPackage(@UserIdInt int userId, @NonNull String packageName) {
synchronized (mPreviouslyUsedIds) {
// First: collect all IDs currently in use for this user's Associations.
final SparseBooleanArray usedIds = new SparseBooleanArray();
// We should really only be checking associations for the given user (i.e.:
// mAssociationStore.getAssociationsForUser(userId)), BUT in the past we've got in a
// state where association IDs were not assigned correctly in regard to
// user-to-association-ids-range (e.g. associations with IDs from 1 to 100,000 should
// always belong to u0), so let's check all the associations.
for (AssociationInfo it : mAssociationStore.getAssociations()) {
usedIds.put(it.getId(), true);
}
// Second: collect all IDs that have been previously used for this package (and user).
final Set<Integer> previouslyUsedIds =
getPreviouslyUsedIdsForPackageLocked(userId, packageName);
int id = getFirstAssociationIdForUser(userId);
final int lastAvailableIdForUser = getLastAssociationIdForUser(userId);
// Find first ID that isn't used now AND has never been used for the given package.
while (usedIds.get(id) || previouslyUsedIds.contains(id)) {
// Increment and try again
id++;
// ... but first check if the ID is valid (within the range allocated to the user).
if (id > lastAvailableIdForUser) {
throw new RuntimeException("Cannot create a new Association ID for "
+ packageName + " for user " + userId);
}
}
return id;
}
}
// TODO: also revoke notification access
void disassociateInternal(int associationId) {
final AssociationInfo association = mAssociationStore.getAssociationById(associationId);
final int userId = association.getUserId();
final String packageName = association.getPackageName();
final String deviceProfile = association.getDeviceProfile();
final boolean wasPresent = mDevicePresenceMonitor.isDevicePresent(associationId);
// Removing the association.
mAssociationStore.removeAssociation(associationId);
logRemoveAssociation(deviceProfile);
final List<AssociationInfo> otherAssociations =
mAssociationStore.getAssociationsForPackage(userId, packageName);
// Check if the package is associated with other devices with the same profile.
// If not: take away the role.
if (deviceProfile != null) {
final boolean shouldKeepTheRole = any(otherAssociations,
it -> deviceProfile.equals(it.getDeviceProfile()));
if (!shouldKeepTheRole) {
Binder.withCleanCallingIdentity(() ->
removeRoleHolderForAssociation(getContext(), association));
}
}
if (!wasPresent || !association.isNotifyOnDeviceNearby()) return;
// The device was connected and the app was notified: check if we need to unbind the app
// now.
final boolean shouldStayBound = any(otherAssociations,
it -> it.isNotifyOnDeviceNearby()
&& mDevicePresenceMonitor.isDevicePresent(it.getId()));
if (shouldStayBound) return;
mCompanionAppController.unbindCompanionApplication(userId, packageName);
}
private void updateSpecialAccessPermissionForAssociatedPackage(AssociationInfo association) {
final PackageInfo packageInfo =
getPackageInfo(getContext(), association.getUserId(), association.getPackageName());
Binder.withCleanCallingIdentity(() -> updateSpecialAccessPermissionAsSystem(packageInfo));
}
private void updateSpecialAccessPermissionAsSystem(PackageInfo packageInfo) {
if (containsEither(packageInfo.requestedPermissions,
android.Manifest.permission.RUN_IN_BACKGROUND,
android.Manifest.permission.REQUEST_COMPANION_RUN_IN_BACKGROUND)) {
mPowerWhitelistManager.addToWhitelist(packageInfo.packageName);
} else {
try {
mPowerWhitelistManager.removeFromWhitelist(packageInfo.packageName);
} catch (UnsupportedOperationException e) {
Slog.w(TAG, packageInfo.packageName + " can't be removed from power save"
+ " whitelist. It might due to the package is whitelisted by the system.");
}
}
NetworkPolicyManager networkPolicyManager = NetworkPolicyManager.from(getContext());
if (containsEither(packageInfo.requestedPermissions,
android.Manifest.permission.USE_DATA_IN_BACKGROUND,
android.Manifest.permission.REQUEST_COMPANION_USE_DATA_IN_BACKGROUND)) {
networkPolicyManager.addUidPolicy(
packageInfo.applicationInfo.uid,
NetworkPolicyManager.POLICY_ALLOW_METERED_BACKGROUND);
} else {
networkPolicyManager.removeUidPolicy(
packageInfo.applicationInfo.uid,
NetworkPolicyManager.POLICY_ALLOW_METERED_BACKGROUND);
}
exemptFromAutoRevoke(packageInfo.packageName, packageInfo.applicationInfo.uid);
}
private void exemptFromAutoRevoke(String packageName, int uid) {
try {
mAppOpsManager.setMode(
AppOpsManager.OP_AUTO_REVOKE_PERMISSIONS_IF_UNUSED,
uid,
packageName,
AppOpsManager.MODE_IGNORED);
} catch (RemoteException e) {
Slog.w(TAG, "Error while granting auto revoke exemption for " + packageName, e);
}
}
private void updateAtm(int userId, List<AssociationInfo> associations) {
final Set<Integer> companionAppUids = new ArraySet<>();
for (AssociationInfo association : associations) {
final int uid = mPackageManagerInternal.getPackageUid(association.getPackageName(),
0, userId);
if (uid >= 0) {
companionAppUids.add(uid);
}
}
if (mAmInternal != null) {
// Make a copy of the set and send it to ActivityManager.
mAmInternal.setCompanionAppUids(userId, new ArraySet<>(companionAppUids));
}
}
private void maybeGrantAutoRevokeExemptions() {
Slog.d(TAG, "maybeGrantAutoRevokeExemptions()");
PackageManager pm = getContext().getPackageManager();
for (int userId : LocalServices.getService(UserManagerInternal.class).getUserIds()) {
SharedPreferences pref = getContext().getSharedPreferences(
new File(Environment.getUserSystemDirectory(userId), PREF_FILE_NAME),
Context.MODE_PRIVATE);
if (pref.getBoolean(PREF_KEY_AUTO_REVOKE_GRANTS_DONE, false)) {
continue;
}
try {
final List<AssociationInfo> associations =
mAssociationStore.getAssociationsForUser(userId);
for (AssociationInfo a : associations) {
try {
int uid = pm.getPackageUidAsUser(a.getPackageName(), userId);
exemptFromAutoRevoke(a.getPackageName(), uid);
} catch (PackageManager.NameNotFoundException e) {
Slog.w(TAG, "Unknown companion package: " + a.getPackageName(), e);
}
}
} finally {
pref.edit().putBoolean(PREF_KEY_AUTO_REVOKE_GRANTS_DONE, true).apply();
}
}
}
private final AssociationStore.OnChangeListener mAssociationStoreChangeListener =
new AssociationStore.OnChangeListener() {
@Override
public void onAssociationChanged(int changeType, AssociationInfo association) {
onAssociationChangedInternal(changeType, association);
}
};
private final CompanionDevicePresenceMonitor.Callback mDevicePresenceCallback =
new CompanionDevicePresenceMonitor.Callback() {
@Override
public void onDeviceAppeared(int associationId) {
onDeviceAppearedInternal(associationId);
}
@Override
public void onDeviceDisappeared(int associationId) {
onDeviceDisappearedInternal(associationId);
}
};
private final CompanionApplicationController.Callback mApplicationControllerCallback =
new CompanionApplicationController.Callback() {
@Override
public boolean onCompanionApplicationBindingDied(int userId, @NonNull String packageName) {
return onCompanionApplicationBindingDiedInternal(userId, packageName);
}
@Override
public void onRebindCompanionApplicationTimeout(int userId, @NonNull String packageName) {
onRebindCompanionApplicationTimeoutInternal(userId, packageName);
}
};
private final PackageMonitor mPackageMonitor = new PackageMonitor() {
@Override
public void onPackageRemoved(String packageName, int uid) {
onPackageRemoveOrDataClearedInternal(getChangingUserId(), packageName);
}
@Override
public void onPackageDataCleared(String packageName, int uid) {
onPackageRemoveOrDataClearedInternal(getChangingUserId(), packageName);
}
@Override
public void onPackageModified(String packageName) {
onPackageModifiedInternal(getChangingUserId(), packageName);
}
};
static int getFirstAssociationIdForUser(@UserIdInt int userId) {
// We want the IDs to start from 1, not 0.
return userId * ASSOCIATIONS_IDS_PER_USER_RANGE + 1;
}
static int getLastAssociationIdForUser(@UserIdInt int userId) {
return (userId + 1) * ASSOCIATIONS_IDS_PER_USER_RANGE;
}
private static Map<String, Set<Integer>> deepUnmodifiableCopy(Map<String, Set<Integer>> orig) {
final Map<String, Set<Integer>> copy = new HashMap<>();
for (Map.Entry<String, Set<Integer>> entry : orig.entrySet()) {
final Set<Integer> valueCopy = new HashSet<>(entry.getValue());
copy.put(entry.getKey(), Collections.unmodifiableSet(valueCopy));
}
return Collections.unmodifiableMap(copy);
}
private static <T> boolean containsEither(T[] array, T a, T b) {
return ArrayUtils.contains(array, a) || ArrayUtils.contains(array, b);
}
private class LocalService extends CompanionDeviceManagerServiceInternal {
@Override
public void associationCleanUp(String profile) {
CompanionDeviceManagerService.this.associationCleanUp(profile);
}
}
/**
* This method must only be called from {@link CompanionDeviceShellCommand} for testing
* purposes only!
*/
void persistState() {
mUserPersistenceHandler.clearMessages();
for (UserInfo user : mUserManager.getAliveUsers()) {
persistStateForUser(user.id);
}
}
/**
* This class is dedicated to handling requests to persist user state.
*/
@SuppressLint("HandlerLeak")
private class PersistUserStateHandler extends Handler {
PersistUserStateHandler() {
super(BackgroundThread.get().getLooper());
}
/**
* Persists user state unless there is already an outstanding request for the given user.
*/
synchronized void postPersistUserState(@UserIdInt int userId) {
if (!hasMessages(userId)) {
sendMessage(obtainMessage(userId));
}
}
/**
* Clears *ALL* outstanding persist requests for *ALL* users.
*/
synchronized void clearMessages() {
removeCallbacksAndMessages(null);
}
@Override
public void handleMessage(@NonNull Message msg) {
final int userId = msg.what;
persistStateForUser(userId);
}
}
}