blob: 5952182077137ebe62286492a7940293ef484ac9 [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.BIND_COMPANION_DEVICE_SERVICE;
import static android.bluetooth.le.ScanSettings.CALLBACK_TYPE_ALL_MATCHES;
import static android.bluetooth.le.ScanSettings.SCAN_MODE_BALANCED;
import static android.content.Context.BIND_IMPORTANT;
import static android.content.pm.PackageManager.CERT_INPUT_SHA256;
import static android.content.pm.PackageManager.MATCH_ALL;
import static android.content.pm.PackageManager.PERMISSION_GRANTED;
import static com.android.internal.util.CollectionUtils.any;
import static com.android.internal.util.CollectionUtils.emptyIfNull;
import static com.android.internal.util.CollectionUtils.filter;
import static com.android.internal.util.CollectionUtils.find;
import static com.android.internal.util.CollectionUtils.forEach;
import static com.android.internal.util.CollectionUtils.map;
import static com.android.internal.util.FunctionalUtils.uncheckExceptions;
import static com.android.internal.util.Preconditions.checkArgument;
import static com.android.internal.util.Preconditions.checkNotNull;
import static com.android.internal.util.Preconditions.checkState;
import static com.android.internal.util.function.pooled.PooledLambda.obtainMessage;
import static com.android.internal.util.function.pooled.PooledLambda.obtainRunnable;
import static java.util.Objects.requireNonNull;
import static java.util.concurrent.TimeUnit.MINUTES;
import android.Manifest;
import android.annotation.CheckResult;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.SuppressLint;
import android.app.ActivityManagerInternal;
import android.app.AppOpsManager;
import android.app.PendingIntent;
import android.app.role.RoleManager;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.le.BluetoothLeScanner;
import android.bluetooth.le.ScanCallback;
import android.bluetooth.le.ScanFilter;
import android.bluetooth.le.ScanResult;
import android.bluetooth.le.ScanSettings;
import android.companion.Association;
import android.companion.AssociationRequest;
import android.companion.CompanionDeviceManager;
import android.companion.CompanionDeviceService;
import android.companion.DeviceNotAssociatedException;
import android.companion.ICompanionDeviceDiscoveryService;
import android.companion.ICompanionDeviceManager;
import android.companion.ICompanionDeviceService;
import android.companion.IFindDeviceCallback;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.content.pm.FeatureInfo;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManagerInternal;
import android.content.pm.ResolveInfo;
import android.content.pm.Signature;
import android.content.pm.UserInfo;
import android.net.NetworkPolicyManager;
import android.os.Binder;
import android.os.Environment;
import android.os.Handler;
import android.os.IBinder;
import android.os.IInterface;
import android.os.Parcel;
import android.os.PowerWhitelistManager;
import android.os.Process;
import android.os.RemoteException;
import android.os.ResultReceiver;
import android.os.ServiceManager;
import android.os.ShellCallback;
import android.os.ShellCommand;
import android.os.UserHandle;
import android.os.UserManager;
import android.permission.PermissionControllerManager;
import android.provider.Settings;
import android.provider.SettingsStringUtil.ComponentNameSet;
import android.text.BidiFormatter;
import android.util.ArrayMap;
import android.util.ArraySet;
import android.util.AtomicFile;
import android.util.ExceptionUtils;
import android.util.Log;
import android.util.PackageUtils;
import android.util.Slog;
import android.util.SparseArray;
import android.util.Xml;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.app.IAppOpsService;
import com.android.internal.content.PackageMonitor;
import com.android.internal.infra.AndroidFuture;
import com.android.internal.infra.PerUser;
import com.android.internal.infra.ServiceConnector;
import com.android.internal.notification.NotificationAccessConfirmationActivityContract;
import com.android.internal.os.BackgroundThread;
import com.android.internal.util.ArrayUtils;
import com.android.internal.util.CollectionUtils;
import com.android.internal.util.DumpUtils;
import com.android.internal.util.function.pooled.PooledLambda;
import com.android.server.FgThread;
import com.android.server.LocalServices;
import com.android.server.SystemService;
import com.android.server.pm.UserManagerInternal;
import com.android.server.wm.ActivityTaskManagerInternal;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import org.xmlpull.v1.XmlSerializer;
import java.io.File;
import java.io.FileDescriptor;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.PrintWriter;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.function.Function;
/** @hide */
@SuppressLint("LongLogTag")
public class CompanionDeviceManagerService extends SystemService implements Binder.DeathRecipient {
private static final ComponentName SERVICE_TO_BIND_TO = ComponentName.createRelative(
CompanionDeviceManager.COMPANION_DEVICE_DISCOVERY_PACKAGE_NAME,
".CompanionDeviceDiscoveryService");
private static final long DEVICE_DISAPPEARED_TIMEOUT_MS = 10 * 1000;
private static final long DEVICE_DISAPPEARED_UNBIND_TIMEOUT_MS = 10 * 60 * 1000;
private static final long DEVICE_LISTENER_DIED_REBIND_TIMEOUT_MS = 10 * 1000;
private static final boolean DEBUG = false;
private static final String LOG_TAG = "CompanionDeviceManagerService";
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 int ASSOCIATE_WITHOUT_PROMPT_MAX_PER_TIME_WINDOW = 5;
private static final long ASSOCIATE_WITHOUT_PROMPT_WINDOW_MS = 60 * 60 * 1000; // 60 min;
private static final String XML_TAG_ASSOCIATIONS = "associations";
private static final String XML_TAG_ASSOCIATION = "association";
private static final String XML_ATTR_PACKAGE = "package";
private static final String XML_ATTR_DEVICE = "device";
private static final String XML_ATTR_PROFILE = "profile";
private static final String XML_ATTR_NOTIFY_DEVICE_NEARBY = "notify_device_nearby";
private static final String XML_ATTR_TIME_APPROVED = "time_approved";
private static final String XML_FILE_NAME = "companion_device_manager_associations.xml";
private final CompanionDeviceManagerImpl mImpl;
private final ConcurrentMap<Integer, AtomicFile> mUidToStorage = new ConcurrentHashMap<>();
private PowerWhitelistManager mPowerWhitelistManager;
private PerUser<ServiceConnector<ICompanionDeviceDiscoveryService>> mServiceConnectors;
/** userId -> packageName -> serviceConnector */
private PerUser<ArrayMap<String, ServiceConnector<ICompanionDeviceService>>>
mDeviceListenerServiceConnectors;
private IAppOpsService mAppOpsManager;
private RoleManager mRoleManager;
private BluetoothAdapter mBluetoothAdapter;
private UserManager mUserManager;
private IFindDeviceCallback mFindDeviceCallback;
private ScanCallback mBleScanCallback = new BleScanCallback();
private AssociationRequest mRequest;
private String mCallingPackage;
private AndroidFuture<Association> mOngoingDeviceDiscovery;
private PermissionControllerManager mPermissionControllerManager;
private BluetoothDeviceConnectedListener mBluetoothDeviceConnectedListener =
new BluetoothDeviceConnectedListener();
private BleStateBroadcastReceiver mBleStateBroadcastReceiver = new BleStateBroadcastReceiver();
private List<String> mCurrentlyConnectedDevices = new ArrayList<>();
private ArrayMap<String, Date> mDevicesLastNearby = new ArrayMap<>();
private UnbindDeviceListenersRunnable
mUnbindDeviceListenersRunnable = new UnbindDeviceListenersRunnable();
private ArrayMap<String, TriggerDeviceDisappearedRunnable> mTriggerDeviceDisappearedRunnables =
new ArrayMap<>();
private final Object mLock = new Object();
private final Handler mMainHandler = Handler.getMain();
/** userId -> [association] */
@GuardedBy("mLock")
private @Nullable SparseArray<Set<Association>> mCachedAssociations = new SparseArray<>();
ActivityTaskManagerInternal mAtmInternal;
ActivityManagerInternal mAmInternal;
PackageManagerInternal mPackageManagerInternal;
public CompanionDeviceManagerService(Context context) {
super(context);
mImpl = new CompanionDeviceManagerImpl();
mPowerWhitelistManager = context.getSystemService(PowerWhitelistManager.class);
mRoleManager = context.getSystemService(RoleManager.class);
mAppOpsManager = IAppOpsService.Stub.asInterface(
ServiceManager.getService(Context.APP_OPS_SERVICE));
mAtmInternal = LocalServices.getService(ActivityTaskManagerInternal.class);
mAmInternal = LocalServices.getService(ActivityManagerInternal.class);
mPackageManagerInternal = LocalServices.getService(PackageManagerInternal.class);
mPermissionControllerManager = requireNonNull(
context.getSystemService(PermissionControllerManager.class));
mUserManager = context.getSystemService(UserManager.class);
Intent serviceIntent = new Intent().setComponent(SERVICE_TO_BIND_TO);
mServiceConnectors = new PerUser<ServiceConnector<ICompanionDeviceDiscoveryService>>() {
@Override
protected ServiceConnector<ICompanionDeviceDiscoveryService> create(int userId) {
return new ServiceConnector.Impl<>(
getContext(),
serviceIntent, 0/* bindingFlags */, userId,
ICompanionDeviceDiscoveryService.Stub::asInterface);
}
};
mDeviceListenerServiceConnectors = new PerUser<ArrayMap<String,
ServiceConnector<ICompanionDeviceService>>>() {
@NonNull
@Override
protected ArrayMap<String, ServiceConnector<ICompanionDeviceService>> create(
int userId) {
return new ArrayMap<>();
}
};
registerPackageMonitor();
}
private void registerPackageMonitor() {
new PackageMonitor() {
@Override
public void onPackageRemoved(String packageName, int uid) {
Slog.d(LOG_TAG, "onPackageRemoved(packageName = " + packageName
+ ", uid = " + uid + ")");
int userId = getChangingUserId();
updateAssociations(
as -> CollectionUtils.filter(as,
a -> !Objects.equals(a.getPackageName(), packageName)),
userId);
unbindDevicePresenceListener(packageName, userId);
}
@Override
public void onPackageModified(String packageName) {
Slog.d(LOG_TAG, "onPackageModified(packageName = " + packageName + ")");
int userId = getChangingUserId();
forEach(getAllAssociations(userId, packageName), association -> {
updateSpecialAccessPermissionForAssociatedPackage(association);
});
}
}.register(getContext(), FgThread.get().getLooper(), UserHandle.ALL, true);
}
private void unbindDevicePresenceListener(String packageName, int userId) {
ServiceConnector<ICompanionDeviceService> deviceListener =
mDeviceListenerServiceConnectors.forUser(userId)
.remove(packageName);
if (deviceListener != null) {
deviceListener.unbind();
}
}
@Override
public void onStart() {
publishBinderService(Context.COMPANION_DEVICE_SERVICE, mImpl);
}
@Override
public void onBootPhase(int phase) {
if (phase == SystemService.PHASE_SYSTEM_SERVICES_READY) {
// Init Bluetooth
mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
if (mBluetoothAdapter != null) {
mBluetoothAdapter.registerBluetoothConnectionCallback(
getContext().getMainExecutor(),
mBluetoothDeviceConnectedListener);
getContext().registerReceiver(
mBleStateBroadcastReceiver, mBleStateBroadcastReceiver.mIntentFilter);
initBleScanning();
} else {
Slog.w(LOG_TAG, "No BluetoothAdapter available");
}
}
}
@Override
public void onUserUnlocking(@NonNull TargetUser user) {
int userHandle = user.getUserIdentifier();
Set<Association> associations = getAllAssociations(userHandle);
if (associations == null || associations.isEmpty()) {
return;
}
updateAtm(userHandle, associations);
BackgroundThread.getHandler().sendMessageDelayed(
obtainMessage(CompanionDeviceManagerService::maybeGrantAutoRevokeExemptions, this),
MINUTES.toMillis(10));
}
void maybeGrantAutoRevokeExemptions() {
Slog.d(LOG_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 {
Set<Association> associations = getAllAssociations(userId);
if (associations == null) {
continue;
}
for (Association a : associations) {
try {
int uid = pm.getPackageUidAsUser(a.getPackageName(), userId);
exemptFromAutoRevoke(a.getPackageName(), uid);
} catch (PackageManager.NameNotFoundException e) {
Slog.w(LOG_TAG, "Unknown companion package: " + a.getPackageName(), e);
}
}
} finally {
pref.edit().putBoolean(PREF_KEY_AUTO_REVOKE_GRANTS_DONE, true).apply();
}
}
}
@Override
public void binderDied() {
Slog.w(LOG_TAG, "binderDied()");
mMainHandler.post(this::cleanup);
}
private void cleanup() {
Slog.d(LOG_TAG, "cleanup(); discovery = "
+ mOngoingDeviceDiscovery + ", request = " + mRequest);
synchronized (mLock) {
AndroidFuture<Association> ongoingDeviceDiscovery = mOngoingDeviceDiscovery;
if (ongoingDeviceDiscovery != null && !ongoingDeviceDiscovery.isDone()) {
ongoingDeviceDiscovery.cancel(true);
}
mFindDeviceCallback = unlinkToDeath(mFindDeviceCallback, this, 0);
mRequest = null;
mCallingPackage = null;
}
}
/**
* Usage: {@code a = unlinkToDeath(a, deathRecipient, flags); }
*/
@Nullable
@CheckResult
private static <T extends IInterface> T unlinkToDeath(T iinterface,
IBinder.DeathRecipient deathRecipient, int flags) {
if (iinterface != null) {
iinterface.asBinder().unlinkToDeath(deathRecipient, flags);
}
return null;
}
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(LOG_TAG, "Error during IPC", e);
throw ExceptionUtils.propagate(e, RemoteException.class);
}
}
@Override
public void associate(
AssociationRequest request,
IFindDeviceCallback callback,
String callingPackage) throws RemoteException {
Slog.i(LOG_TAG, "associate(request = " + request + ", callback = " + callback
+ ", callingPackage = " + callingPackage + ")");
checkNotNull(request, "Request cannot be null");
checkNotNull(callback, "Callback cannot be null");
checkCallerIsSystemOr(callingPackage);
int userId = getCallingUserId();
checkUsesFeature(callingPackage, userId);
checkProfilePermissions(request);
mFindDeviceCallback = callback;
mRequest = request;
mCallingPackage = callingPackage;
request.setCallingPackage(callingPackage);
if (mayAssociateWithoutPrompt(callingPackage, userId)) {
Slog.i(LOG_TAG, "setSkipPrompt(true)");
request.setSkipPrompt(true);
}
callback.asBinder().linkToDeath(CompanionDeviceManagerService.this /* recipient */, 0);
AndroidFuture<String> fetchProfileDescription =
request.getDeviceProfile() == null
? AndroidFuture.completedFuture(null)
: getDeviceProfilePermissionDescription(
request.getDeviceProfile());
mOngoingDeviceDiscovery = fetchProfileDescription.thenComposeAsync(description -> {
Slog.d(LOG_TAG, "fetchProfileDescription done: " + description);
request.setDeviceProfilePrivilegesDescription(description);
return mServiceConnectors.forUser(userId).postAsync(service -> {
Slog.d(LOG_TAG, "Connected to CDM service; starting discovery for " + request);
AndroidFuture<Association> future = new AndroidFuture<>();
service.startDiscovery(request, callingPackage, callback, future);
return future;
}).cancelTimeout();
}, FgThread.getExecutor()).whenComplete(uncheckExceptions((association, err) -> {
if (err == null) {
addAssociation(association, userId);
} else {
Slog.e(LOG_TAG, "Failed to discover device(s)", err);
callback.onFailure("No devices found: " + err.getMessage());
}
cleanup();
}));
}
@Override
public void stopScan(AssociationRequest request,
IFindDeviceCallback callback,
String callingPackage) {
Slog.d(LOG_TAG, "stopScan(request = " + request + ")");
if (Objects.equals(request, mRequest)
&& Objects.equals(callback, mFindDeviceCallback)
&& Objects.equals(callingPackage, mCallingPackage)) {
cleanup();
}
}
@Override
public List<String> getAssociations(String callingPackage, int userId)
throws RemoteException {
if (!callerCanManageCompanionDevices()) {
checkCallerIsSystemOr(callingPackage, userId);
checkUsesFeature(callingPackage, getCallingUserId());
}
return new ArrayList<>(map(
getAllAssociations(userId, callingPackage),
a -> a.getDeviceMacAddress()));
}
@Override
public List<Association> getAssociationsForUser(int userId) {
if (!callerCanManageCompanionDevices()) {
throw new SecurityException("Caller must hold "
+ android.Manifest.permission.MANAGE_COMPANION_DEVICES);
}
return new ArrayList<>(getAllAssociations(userId, null /* packageFilter */));
}
//TODO also revoke notification access
@Override
public void disassociate(String deviceMacAddress, String callingPackage)
throws RemoteException {
checkNotNull(deviceMacAddress);
checkCallerIsSystemOr(callingPackage);
checkUsesFeature(callingPackage, getCallingUserId());
removeAssociation(getCallingUserId(), callingPackage, deviceMacAddress);
}
private boolean callerCanManageCompanionDevices() {
return getContext().checkCallingOrSelfPermission(
android.Manifest.permission.MANAGE_COMPANION_DEVICES)
== PERMISSION_GRANTED;
}
private void checkCallerIsSystemOr(String pkg) throws RemoteException {
checkCallerIsSystemOr(pkg, getCallingUserId());
}
private void checkCallerIsSystemOr(String pkg, int userId) throws RemoteException {
if (isCallerSystem()) {
return;
}
checkArgument(getCallingUserId() == userId,
"Must be called by either same user or system");
int callingUid = Binder.getCallingUid();
if (mAppOpsManager.checkPackage(callingUid, pkg) != AppOpsManager.MODE_ALLOWED) {
throw new SecurityException(pkg + " doesn't belong to uid " + callingUid);
}
}
private void checkProfilePermissions(AssociationRequest request) {
checkProfilePermission(request,
AssociationRequest.DEVICE_PROFILE_WATCH,
Manifest.permission.REQUEST_COMPANION_PROFILE_WATCH);
}
private void checkProfilePermission(
AssociationRequest request, String profile, String permission) {
if (profile.equals(request.getDeviceProfile())
&& getContext().checkCallingOrSelfPermission(permission)
!= PackageManager.PERMISSION_GRANTED) {
throw new SecurityException("Using " + profile + " requires " + permission);
}
}
@Override
public PendingIntent requestNotificationAccess(ComponentName component)
throws RemoteException {
String callingPackage = component.getPackageName();
checkCanCallNotificationApi(callingPackage);
int userId = getCallingUserId();
final long identity = Binder.clearCallingIdentity();
try {
return PendingIntent.getActivityAsUser(getContext(),
0 /* request code */,
NotificationAccessConfirmationActivityContract.launcherIntent(
getContext(), userId, component),
PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_ONE_SHOT
| PendingIntent.FLAG_CANCEL_CURRENT,
null /* options */,
new UserHandle(userId));
} finally {
Binder.restoreCallingIdentity(identity);
}
}
@Override
public boolean hasNotificationAccess(ComponentName component) throws RemoteException {
checkCanCallNotificationApi(component.getPackageName());
String setting = Settings.Secure.getString(getContext().getContentResolver(),
Settings.Secure.ENABLED_NOTIFICATION_LISTENERS);
return new ComponentNameSet(setting).contains(component);
}
@Override
public boolean isDeviceAssociatedForWifiConnection(String packageName, String macAddress,
int userId) {
getContext().enforceCallingOrSelfPermission(
android.Manifest.permission.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(
getAllAssociations(userId, packageName),
a -> Objects.equals(a.getDeviceMacAddress(), macAddress));
}
@Override
public void registerDevicePresenceListenerService(
String packageName, String deviceAddress)
throws RemoteException {
registerDevicePresenceListenerActive(packageName, deviceAddress, true);
}
@Override
public void unregisterDevicePresenceListenerService(
String packageName, String deviceAddress)
throws RemoteException {
registerDevicePresenceListenerActive(packageName, deviceAddress, false);
}
private void registerDevicePresenceListenerActive(String packageName, String deviceAddress,
boolean active) throws RemoteException {
getContext().enforceCallingOrSelfPermission(
android.Manifest.permission.REQUEST_OBSERVE_COMPANION_DEVICE_PRESENCE,
"[un]registerDevicePresenceListenerService");
checkCallerIsSystemOr(packageName);
int userId = getCallingUserId();
Set<Association> deviceAssociations = CollectionUtils.filter(
getAllAssociations(userId, packageName),
association -> deviceAddress.equals(association.getDeviceMacAddress()));
if (deviceAssociations.isEmpty()) {
throw new RemoteException(new DeviceNotAssociatedException("App " + packageName
+ " is not associated with device " + deviceAddress
+ " for user " + userId));
}
updateAssociations(associations -> map(associations, association -> {
if (Objects.equals(association.getPackageName(), packageName)
&& Objects.equals(association.getDeviceMacAddress(), deviceAddress)) {
return new Association(
association.getUserId(),
association.getDeviceMacAddress(),
association.getPackageName(),
association.getDeviceProfile(),
active, /* notifyOnDeviceNearby */
association.getTimeApprovedMs());
} else {
return association;
}
}), userId);
restartBleScan();
}
@Override
public void createAssociation(String packageName, String macAddress, int userId,
byte[] certificate) {
if (!getContext().getPackageManager().hasSigningCertificate(
packageName, certificate, CERT_INPUT_SHA256)) {
Slog.e(LOG_TAG, "Given certificate doesn't match the package certificate.");
return;
}
getContext().enforceCallingOrSelfPermission(
android.Manifest.permission.ASSOCIATE_COMPANION_DEVICES, "createAssociation");
addAssociation(new Association(
userId, macAddress, packageName, null, false,
System.currentTimeMillis()), userId);
}
private void checkCanCallNotificationApi(String callingPackage) throws RemoteException {
checkCallerIsSystemOr(callingPackage);
int userId = getCallingUserId();
checkState(!ArrayUtils.isEmpty(getAllAssociations(userId, callingPackage)),
"App must have an association before calling this API");
checkUsesFeature(callingPackage, userId);
}
private void checkUsesFeature(String pkg, int userId) {
if (isCallerSystem()) {
// Drop the requirement for calls from system process
return;
}
FeatureInfo[] reqFeatures = getPackageInfo(pkg, userId).reqFeatures;
String requiredFeature = PackageManager.FEATURE_COMPANION_DEVICE_SETUP;
int numFeatures = ArrayUtils.size(reqFeatures);
for (int i = 0; i < numFeatures; i++) {
if (requiredFeature.equals(reqFeatures[i].name)) return;
}
throw new IllegalStateException("Must declare uses-feature "
+ requiredFeature
+ " in manifest to use this API");
}
@Override
public boolean canPairWithoutPrompt(
String packageName, String deviceMacAddress, int userId) {
return CollectionUtils.any(
getAllAssociations(userId, packageName, deviceMacAddress),
a -> System.currentTimeMillis() - a.getTimeApprovedMs()
< PAIR_WITHOUT_PROMPT_WINDOW_MS);
}
@Override
public void onShellCommand(FileDescriptor in, FileDescriptor out, FileDescriptor err,
String[] args, ShellCallback callback, ResultReceiver resultReceiver)
throws RemoteException {
new ShellCmd().exec(this, in, out, err, args, callback, resultReceiver);
}
@Override
public void dump(@NonNull FileDescriptor fd,
@NonNull PrintWriter fout,
@Nullable String[] args) {
if (!DumpUtils.checkDumpAndUsageStatsPermission(getContext(), LOG_TAG, fout)) {
return;
}
fout.append("Companion Device Associations:").append('\n');
synchronized (mLock) {
for (UserInfo user : getAllUsers()) {
forEach(mCachedAssociations.get(user.id), a -> {
fout.append(" ")
.append("u").append("" + a.getUserId()).append(": ")
.append(a.getPackageName()).append(" - ")
.append(a.getDeviceMacAddress()).append('\n');
});
}
}
}
}
private static int getCallingUserId() {
return UserHandle.getUserId(Binder.getCallingUid());
}
private static boolean isCallerSystem() {
return Binder.getCallingUid() == Process.SYSTEM_UID;
}
void addAssociation(Association association, int userId) {
updateSpecialAccessPermissionForAssociatedPackage(association);
recordAssociation(association, userId);
}
void removeAssociation(int userId, String pkg, String deviceMacAddress) {
updateAssociations(associations -> CollectionUtils.filter(associations, association -> {
boolean notMatch = association.getUserId() != userId
|| !Objects.equals(association.getDeviceMacAddress(), deviceMacAddress)
|| !Objects.equals(association.getPackageName(), pkg);
if (!notMatch) {
onAssociationPreRemove(association);
}
return notMatch;
}), userId);
restartBleScan();
}
void onAssociationPreRemove(Association association) {
if (association.isNotifyOnDeviceNearby()) {
ServiceConnector<ICompanionDeviceService> serviceConnector =
mDeviceListenerServiceConnectors.forUser(association.getUserId())
.get(association.getPackageName());
if (serviceConnector != null) {
serviceConnector.unbind();
}
}
String deviceProfile = association.getDeviceProfile();
if (deviceProfile != null) {
Association otherAssociationWithDeviceProfile = find(
getAllAssociations(association.getUserId()),
a -> !a.equals(association) && deviceProfile.equals(a.getDeviceProfile()));
if (otherAssociationWithDeviceProfile != null) {
Slog.i(LOG_TAG, "Not revoking " + deviceProfile
+ " for " + association
+ " - profile still present in " + otherAssociationWithDeviceProfile);
} else {
long identity = Binder.clearCallingIdentity();
try {
mRoleManager.removeRoleHolderAsUser(
association.getDeviceProfile(),
association.getPackageName(),
RoleManager.MANAGE_HOLDERS_FLAG_DONT_KILL_APP,
UserHandle.of(association.getUserId()),
getContext().getMainExecutor(),
success -> {
if (!success) {
Slog.e(LOG_TAG, "Failed to revoke device profile role "
+ association.getDeviceProfile()
+ " to " + association.getPackageName()
+ " for user " + association.getUserId());
}
});
} finally {
Binder.restoreCallingIdentity(identity);
}
}
}
}
private void updateSpecialAccessPermissionForAssociatedPackage(Association association) {
PackageInfo packageInfo = getPackageInfo(
association.getPackageName(),
association.getUserId());
if (packageInfo == null) {
return;
}
Binder.withCleanCallingIdentity(obtainRunnable(CompanionDeviceManagerService::
updateSpecialAccessPermissionAsSystem, this, association, packageInfo)
.recycleOnUse());
}
private void updateSpecialAccessPermissionAsSystem(
Association association, 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 {
mPowerWhitelistManager.removeFromWhitelist(packageInfo.packageName);
}
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);
if (mCurrentlyConnectedDevices.contains(association.getDeviceMacAddress())) {
grantDeviceProfile(association);
}
if (association.isNotifyOnDeviceNearby()) {
restartBleScan();
}
}
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(LOG_TAG,
"Error while granting auto revoke exemption for " + packageName, e);
}
}
private Set<String> getSameOemPackageCerts(
String packageName, String[] oemPackages, String[] sameOemCerts) {
Set<String> sameOemPackageCerts = new HashSet<>();
// Assume OEM may enter same package name in the parallel string array with
// multiple APK certs corresponding to it
for (int i = 0; i < oemPackages.length; i++) {
if (oemPackages[i].equals(packageName)) {
sameOemPackageCerts.add(sameOemCerts[i].replaceAll(":", ""));
}
}
return sameOemPackageCerts;
}
boolean mayAssociateWithoutPrompt(String packageName, int userId) {
String[] sameOemPackages = getContext()
.getResources()
.getStringArray(com.android.internal.R.array.config_companionDevicePackages);
if (!ArrayUtils.contains(sameOemPackages, packageName)) {
Slog.w(LOG_TAG, packageName
+ " can not silently create associations due to no package found."
+ " Packages from OEM: " + Arrays.toString(sameOemPackages)
);
return false;
}
// Throttle frequent associations
long now = System.currentTimeMillis();
Set<Association> recentAssociations = filter(
getAllAssociations(userId, packageName),
a -> now - a.getTimeApprovedMs() < ASSOCIATE_WITHOUT_PROMPT_WINDOW_MS);
if (recentAssociations.size() >= ASSOCIATE_WITHOUT_PROMPT_MAX_PER_TIME_WINDOW) {
Slog.w(LOG_TAG, "Too many associations. " + packageName
+ " already associated " + recentAssociations.size()
+ " devices within the last " + ASSOCIATE_WITHOUT_PROMPT_WINDOW_MS
+ "ms: " + recentAssociations);
return false;
}
String[] sameOemCerts = getContext()
.getResources()
.getStringArray(com.android.internal.R.array.config_companionDeviceCerts);
Signature[] signatures = mPackageManagerInternal
.getPackage(packageName).getSigningDetails().signatures;
String[] apkCerts = PackageUtils.computeSignaturesSha256Digests(signatures);
Set<String> sameOemPackageCerts =
getSameOemPackageCerts(packageName, sameOemPackages, sameOemCerts);
for (String cert : apkCerts) {
if (sameOemPackageCerts.contains(cert)) {
return true;
}
}
Slog.w(LOG_TAG, packageName
+ " can not silently create associations. " + packageName
+ " has SHA256 certs from APK: " + Arrays.toString(apkCerts)
+ " and from OEM: " + Arrays.toString(sameOemCerts)
);
return false;
}
private static <T> boolean containsEither(T[] array, T a, T b) {
return ArrayUtils.contains(array, a) || ArrayUtils.contains(array, b);
}
@Nullable
private PackageInfo getPackageInfo(String packageName, int userId) {
return Binder.withCleanCallingIdentity(PooledLambda.obtainSupplier((context, pkg, id) -> {
try {
return context.getPackageManager().getPackageInfoAsUser(
pkg,
PackageManager.GET_PERMISSIONS | PackageManager.GET_CONFIGURATIONS,
id);
} catch (PackageManager.NameNotFoundException e) {
Slog.e(LOG_TAG, "Failed to get PackageInfo for package " + pkg, e);
return null;
}
}, getContext(), packageName, userId).recycleOnUse());
}
private void recordAssociation(Association association, int userId) {
Slog.i(LOG_TAG, "recordAssociation(" + association + ")");
updateAssociations(associations -> CollectionUtils.add(associations, association), userId);
}
private void updateAssociations(Function<Set<Association>, Set<Association>> update,
int userId) {
synchronized (mLock) {
final Set<Association> old = getAllAssociations(userId);
Set<Association> associations = new ArraySet<>(old);
associations = update.apply(associations);
Slog.i(LOG_TAG, "Updating associations: " + old + " --> " + associations);
mCachedAssociations.put(userId, Collections.unmodifiableSet(associations));
BackgroundThread.getHandler().sendMessage(PooledLambda.obtainMessage(
CompanionDeviceManagerService::persistAssociations,
this, associations, userId));
updateAtm(userId, associations);
}
}
private void updateAtm(int userId, Set<Association> associations) {
final Set<Integer> companionAppUids = new ArraySet<>();
for (Association association : associations) {
final int uid = mPackageManagerInternal.getPackageUid(association.getPackageName(),
0, userId);
if (uid >= 0) {
companionAppUids.add(uid);
}
}
if (mAtmInternal != null) {
mAtmInternal.setCompanionAppUids(userId, companionAppUids);
}
if (mAmInternal != null) {
// Make a copy of companionAppUids and send it to ActivityManager.
mAmInternal.setCompanionAppUids(userId, new ArraySet<>(companionAppUids));
}
}
private void persistAssociations(Set<Association> associations, int userId) {
Slog.i(LOG_TAG, "Writing associations to disk: " + associations);
final AtomicFile file = getStorageFileForUser(userId);
synchronized (file) {
file.write(out -> {
XmlSerializer xml = Xml.newSerializer();
try {
xml.setOutput(out, StandardCharsets.UTF_8.name());
xml.setFeature("http://xmlpull.org/v1/doc/features.html#indent-output", true);
xml.startDocument(null, true);
xml.startTag(null, XML_TAG_ASSOCIATIONS);
forEach(associations, association -> {
XmlSerializer tag = xml.startTag(null, XML_TAG_ASSOCIATION)
.attribute(null, XML_ATTR_PACKAGE, association.getPackageName())
.attribute(null, XML_ATTR_DEVICE,
association.getDeviceMacAddress());
if (association.getDeviceProfile() != null) {
tag.attribute(null, XML_ATTR_PROFILE, association.getDeviceProfile());
tag.attribute(null, XML_ATTR_NOTIFY_DEVICE_NEARBY,
Boolean.toString(
association.isNotifyOnDeviceNearby()));
}
tag.attribute(null, XML_ATTR_TIME_APPROVED,
Long.toString(association.getTimeApprovedMs()));
tag.endTag(null, XML_TAG_ASSOCIATION);
});
xml.endTag(null, XML_TAG_ASSOCIATIONS);
xml.endDocument();
} catch (Exception e) {
Slog.e(LOG_TAG, "Error while writing associations file", e);
throw ExceptionUtils.propagate(e);
}
});
}
}
private AtomicFile getStorageFileForUser(int userId) {
return mUidToStorage.computeIfAbsent(userId, (u) ->
new AtomicFile(new File(
//TODO deprecated method - what's the right replacement?
Environment.getUserSystemDirectory(u),
XML_FILE_NAME)));
}
@Nullable
private Set<Association> getAllAssociations(int userId) {
synchronized (mLock) {
if (mCachedAssociations.get(userId) == null) {
mCachedAssociations.put(userId, Collections.unmodifiableSet(
emptyIfNull(readAllAssociations(userId))));
Slog.i(LOG_TAG, "Read associations from disk: " + mCachedAssociations);
}
return mCachedAssociations.get(userId);
}
}
private List<UserInfo> getAllUsers() {
long identity = Binder.clearCallingIdentity();
try {
return mUserManager.getUsers();
} finally {
Binder.restoreCallingIdentity(identity);
}
}
private Set<Association> getAllAssociations(int userId, @Nullable String packageFilter) {
return CollectionUtils.filter(
getAllAssociations(userId),
// Null filter == get all associations
a -> packageFilter == null || Objects.equals(packageFilter, a.getPackageName()));
}
private Set<Association> getAllAssociations() {
long identity = Binder.clearCallingIdentity();
try {
ArraySet<Association> result = new ArraySet<>();
for (UserInfo user : mUserManager.getAliveUsers()) {
result.addAll(getAllAssociations(user.id));
}
return result;
} finally {
Binder.restoreCallingIdentity(identity);
}
}
private Set<Association> getAllAssociations(
int userId, @Nullable String packageFilter, @Nullable String addressFilter) {
return CollectionUtils.filter(
getAllAssociations(userId),
// Null filter == get all associations
a -> (packageFilter == null || Objects.equals(packageFilter, a.getPackageName()))
&& (addressFilter == null
|| Objects.equals(addressFilter, a.getDeviceMacAddress())));
}
private Set<Association> readAllAssociations(int userId) {
final AtomicFile file = getStorageFileForUser(userId);
if (!file.getBaseFile().exists()) return null;
ArraySet<Association> result = null;
final XmlPullParser parser = Xml.newPullParser();
synchronized (file) {
try (FileInputStream in = file.openRead()) {
parser.setInput(in, StandardCharsets.UTF_8.name());
int type;
while ((type = parser.next()) != XmlPullParser.END_DOCUMENT) {
if (type != XmlPullParser.START_TAG
&& !XML_TAG_ASSOCIATIONS.equals(parser.getName())) continue;
final String appPackage = parser.getAttributeValue(null, XML_ATTR_PACKAGE);
final String deviceAddress = parser.getAttributeValue(null, XML_ATTR_DEVICE);
final String profile = parser.getAttributeValue(null, XML_ATTR_PROFILE);
final boolean persistentGrants = Boolean.valueOf(
parser.getAttributeValue(null, XML_ATTR_NOTIFY_DEVICE_NEARBY));
final long timeApproved = parseLongOrDefault(
parser.getAttributeValue(null, XML_ATTR_TIME_APPROVED), 0L);
if (appPackage == null || deviceAddress == null) continue;
result = ArrayUtils.add(result,
new Association(userId, deviceAddress, appPackage,
profile, persistentGrants, timeApproved));
}
return result;
} catch (XmlPullParserException | IOException e) {
Slog.e(LOG_TAG, "Error while reading associations file", e);
return null;
}
}
}
void onDeviceConnected(String address) {
Slog.d(LOG_TAG, "onDeviceConnected(address = " + address + ")");
mCurrentlyConnectedDevices.add(address);
for (UserInfo user : getAllUsers()) {
for (Association association : getAllAssociations(user.id)) {
if (Objects.equals(address, association.getDeviceMacAddress())) {
if (association.getDeviceProfile() != null) {
Slog.i(LOG_TAG, "Granting role " + association.getDeviceProfile()
+ " to " + association.getPackageName()
+ " due to device connected: " + association.getDeviceMacAddress());
grantDeviceProfile(association);
}
}
}
}
onDeviceNearby(address);
}
private void grantDeviceProfile(Association association) {
Slog.i(LOG_TAG, "grantDeviceProfile(association = " + association + ")");
if (association.getDeviceProfile() != null) {
mRoleManager.addRoleHolderAsUser(
association.getDeviceProfile(),
association.getPackageName(),
RoleManager.MANAGE_HOLDERS_FLAG_DONT_KILL_APP,
UserHandle.of(association.getUserId()),
getContext().getMainExecutor(),
success -> {
if (!success) {
Slog.e(LOG_TAG, "Failed to grant device profile role "
+ association.getDeviceProfile()
+ " to " + association.getPackageName()
+ " for user " + association.getUserId());
}
});
}
}
void onDeviceDisconnected(String address) {
Slog.d(LOG_TAG, "onDeviceDisconnected(address = " + address + ")");
mCurrentlyConnectedDevices.remove(address);
Date lastSeen = mDevicesLastNearby.get(address);
if (isDeviceDisappeared(lastSeen)) {
onDeviceDisappeared(address);
unscheduleTriggerDeviceDisappearedRunnable(address);
}
}
private boolean isDeviceDisappeared(Date lastSeen) {
return lastSeen == null || System.currentTimeMillis() - lastSeen.getTime()
>= DEVICE_DISAPPEARED_UNBIND_TIMEOUT_MS;
}
private ServiceConnector<ICompanionDeviceService> getDeviceListenerServiceConnector(
Association a) {
return mDeviceListenerServiceConnectors.forUser(a.getUserId()).computeIfAbsent(
a.getPackageName(),
pkg -> createDeviceListenerServiceConnector(a));
}
private ServiceConnector<ICompanionDeviceService> createDeviceListenerServiceConnector(
Association a) {
List<ResolveInfo> resolveInfos = getContext().getPackageManager().queryIntentServicesAsUser(
new Intent(CompanionDeviceService.SERVICE_INTERFACE), MATCH_ALL, a.getUserId());
List<ResolveInfo> packageResolveInfos = filter(resolveInfos,
info -> Objects.equals(info.serviceInfo.packageName, a.getPackageName()));
if (packageResolveInfos.size() != 1) {
Slog.w(LOG_TAG, "Device presence listener package must have exactly one "
+ "CompanionDeviceService, but " + a.getPackageName()
+ " has " + packageResolveInfos.size());
return new ServiceConnector.NoOp<>();
}
String servicePermission = packageResolveInfos.get(0).serviceInfo.permission;
if (!BIND_COMPANION_DEVICE_SERVICE.equals(servicePermission)) {
Slog.w(LOG_TAG, "Binding CompanionDeviceService must have "
+ BIND_COMPANION_DEVICE_SERVICE + " permission.");
return new ServiceConnector.NoOp<>();
}
ComponentName componentName = packageResolveInfos.get(0).serviceInfo.getComponentName();
Slog.i(LOG_TAG, "Initializing CompanionDeviceService binding for " + componentName);
return new ServiceConnector.Impl<ICompanionDeviceService>(getContext(),
new Intent(CompanionDeviceService.SERVICE_INTERFACE).setComponent(componentName),
BIND_IMPORTANT,
a.getUserId(),
ICompanionDeviceService.Stub::asInterface) {
@Override
protected long getAutoDisconnectTimeoutMs() {
// Service binding is managed manually based on corresponding device being nearby
return Long.MAX_VALUE;
}
@Override
public void binderDied() {
super.binderDied();
// Re-connect to the service if process gets killed
mMainHandler.postDelayed(this::connect, DEVICE_LISTENER_DIED_REBIND_TIMEOUT_MS);
}
};
}
private class BleScanCallback extends ScanCallback {
@Override
public void onScanResult(int callbackType, ScanResult result) {
if (DEBUG) {
Slog.i(LOG_TAG, "onScanResult(callbackType = "
+ callbackType + ", result = " + result + ")");
}
onDeviceNearby(result.getDevice().getAddress());
}
@Override
public void onBatchScanResults(List<ScanResult> results) {
for (int i = 0, size = results.size(); i < size; i++) {
onScanResult(CALLBACK_TYPE_ALL_MATCHES, results.get(i));
}
}
@Override
public void onScanFailed(int errorCode) {
if (errorCode == SCAN_FAILED_ALREADY_STARTED) {
// ignore - this might happen if BT tries to auto-restore scans for us in the
// future
Slog.i(LOG_TAG, "Ignoring BLE scan error: SCAN_FAILED_ALREADY_STARTED");
} else {
Slog.w(LOG_TAG, "Failed to start BLE scan: error " + errorCode);
}
}
}
private class BleStateBroadcastReceiver extends BroadcastReceiver {
final IntentFilter mIntentFilter =
new IntentFilter(BluetoothAdapter.ACTION_BLE_STATE_CHANGED);
@Override
public void onReceive(Context context, Intent intent) {
int previousState = intent.getIntExtra(BluetoothAdapter.EXTRA_PREVIOUS_STATE, -1);
int newState = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, -1);
Slog.d(LOG_TAG, "Received BT state transition broadcast: "
+ BluetoothAdapter.nameForState(previousState)
+ " -> " + BluetoothAdapter.nameForState(newState));
boolean bleOn = newState == BluetoothAdapter.STATE_ON
|| newState == BluetoothAdapter.STATE_BLE_ON;
if (bleOn) {
if (mBluetoothAdapter.getBluetoothLeScanner() != null) {
startBleScan();
} else {
Slog.wtf(LOG_TAG, "BLE on, but BluetoothLeScanner == null");
}
}
}
}
private class UnbindDeviceListenersRunnable implements Runnable {
public String getJobId(String address) {
return "CDM_deviceGone_unbind_" + address;
}
@Override
public void run() {
Slog.i(LOG_TAG, "UnbindDeviceListenersRunnable.run(); devicesNearby = "
+ mDevicesLastNearby);
int size = mDevicesLastNearby.size();
for (int i = 0; i < size; i++) {
String address = mDevicesLastNearby.keyAt(i);
Date lastNearby = mDevicesLastNearby.valueAt(i);
if (isDeviceDisappeared(lastNearby)) {
for (Association association : getAllAssociations(address)) {
if (association.isNotifyOnDeviceNearby()) {
getDeviceListenerServiceConnector(association).unbind();
}
}
}
}
}
}
private class TriggerDeviceDisappearedRunnable implements Runnable {
private final String mAddress;
TriggerDeviceDisappearedRunnable(String address) {
mAddress = address;
}
public void schedule() {
mMainHandler.removeCallbacks(this);
mMainHandler.postDelayed(this, this, DEVICE_DISAPPEARED_TIMEOUT_MS);
}
@Override
public void run() {
Slog.d(LOG_TAG, "TriggerDeviceDisappearedRunnable.run(address = " + mAddress + ")");
if (!mCurrentlyConnectedDevices.contains(mAddress)) {
onDeviceDisappeared(mAddress);
}
}
}
private void unscheduleTriggerDeviceDisappearedRunnable(String address) {
Runnable r = mTriggerDeviceDisappearedRunnables.get(address);
if (r != null) {
Slog.d(LOG_TAG,
"unscheduling TriggerDeviceDisappearedRunnable(address = " + address + ")");
mMainHandler.removeCallbacks(r);
}
}
private Set<Association> getAllAssociations(String deviceAddress) {
List<UserInfo> aliveUsers = mUserManager.getAliveUsers();
Set<Association> result = new ArraySet<>();
for (int i = 0, size = aliveUsers.size(); i < size; i++) {
UserInfo user = aliveUsers.get(i);
for (Association association : getAllAssociations(user.id)) {
if (Objects.equals(association.getDeviceMacAddress(), deviceAddress)) {
result.add(association);
}
}
}
return result;
}
private void onDeviceNearby(String address) {
Date timestamp = new Date();
Date oldTimestamp = mDevicesLastNearby.put(address, timestamp);
cancelUnbindDeviceListener(address);
mTriggerDeviceDisappearedRunnables
.computeIfAbsent(address, addr -> new TriggerDeviceDisappearedRunnable(address))
.schedule();
// Avoid spamming the app if device is already known to be nearby
boolean justAppeared = oldTimestamp == null
|| timestamp.getTime() - oldTimestamp.getTime() >= DEVICE_DISAPPEARED_TIMEOUT_MS;
if (justAppeared) {
Slog.i(LOG_TAG, "onDeviceNearby(justAppeared, address = " + address + ")");
for (Association association : getAllAssociations(address)) {
if (association.isNotifyOnDeviceNearby()) {
Slog.i(LOG_TAG,
"Sending onDeviceAppeared to " + association.getPackageName() + ")");
getDeviceListenerServiceConnector(association).run(
service -> service.onDeviceAppeared(association.getDeviceMacAddress()));
}
}
}
}
private void onDeviceDisappeared(String address) {
Slog.i(LOG_TAG, "onDeviceDisappeared(address = " + address + ")");
boolean hasDeviceListeners = false;
for (Association association : getAllAssociations(address)) {
if (association.isNotifyOnDeviceNearby()) {
Slog.i(LOG_TAG,
"Sending onDeviceDisappeared to " + association.getPackageName() + ")");
getDeviceListenerServiceConnector(association).run(
service -> service.onDeviceDisappeared(address));
hasDeviceListeners = true;
}
}
cancelUnbindDeviceListener(address);
if (hasDeviceListeners) {
mMainHandler.postDelayed(
mUnbindDeviceListenersRunnable,
mUnbindDeviceListenersRunnable.getJobId(address),
DEVICE_DISAPPEARED_UNBIND_TIMEOUT_MS);
}
}
private void cancelUnbindDeviceListener(String address) {
mMainHandler.removeCallbacks(
mUnbindDeviceListenersRunnable, mUnbindDeviceListenersRunnable.getJobId(address));
}
private void initBleScanning() {
Slog.i(LOG_TAG, "initBleScanning()");
boolean bluetoothReady = mBluetoothAdapter.registerServiceLifecycleCallback(
new BluetoothAdapter.ServiceLifecycleCallback() {
@Override
public void onBluetoothServiceUp() {
Slog.i(LOG_TAG, "Bluetooth stack is up");
startBleScan();
}
@Override
public void onBluetoothServiceDown() {
Slog.w(LOG_TAG, "Bluetooth stack is down");
}
});
if (bluetoothReady) {
startBleScan();
}
}
void startBleScan() {
Slog.i(LOG_TAG, "startBleScan()");
List<ScanFilter> filters = getBleScanFilters();
if (filters.isEmpty()) {
return;
}
BluetoothLeScanner scanner = mBluetoothAdapter.getBluetoothLeScanner();
if (scanner == null) {
Slog.w(LOG_TAG, "scanner == null (likely BLE isn't ON yet)");
} else {
scanner.startScan(
filters,
new ScanSettings.Builder().setScanMode(SCAN_MODE_BALANCED).build(),
mBleScanCallback);
}
}
void restartBleScan() {
mBluetoothAdapter.getBluetoothLeScanner().stopScan(mBleScanCallback);
startBleScan();
}
private List<ScanFilter> getBleScanFilters() {
ArrayList<ScanFilter> result = new ArrayList<>();
ArraySet<String> addressesSeen = new ArraySet<>();
for (Association association : getAllAssociations()) {
String address = association.getDeviceMacAddress();
if (addressesSeen.contains(address)) {
continue;
}
if (association.isNotifyOnDeviceNearby()) {
result.add(new ScanFilter.Builder().setDeviceAddress(address).build());
addressesSeen.add(address);
}
}
return result;
}
private AndroidFuture<String> getDeviceProfilePermissionDescription(String deviceProfile) {
AndroidFuture<String> result = new AndroidFuture<>();
mPermissionControllerManager.getPrivilegesDescriptionStringForProfile(
deviceProfile, FgThread.getExecutor(), desc -> {
try {
result.complete(String.valueOf(desc));
} catch (Exception e) {
result.completeExceptionally(e);
}
});
return result;
}
private static long parseLongOrDefault(String str, long def) {
try {
return Long.parseLong(str);
} catch (NumberFormatException e) {
Slog.w(LOG_TAG, "Failed to parse", e);
return def;
}
}
private class ShellCmd extends ShellCommand {
public static final String USAGE = "help\n"
+ "list USER_ID\n"
+ "associate USER_ID PACKAGE MAC_ADDRESS\n"
+ "disassociate USER_ID PACKAGE MAC_ADDRESS";
ShellCmd() {
getContext().enforceCallingOrSelfPermission(
android.Manifest.permission.MANAGE_COMPANION_DEVICES, "ShellCmd");
}
@Override
public int onCommand(String cmd) {
try {
switch (cmd) {
case "list": {
forEach(
getAllAssociations(getNextArgInt()),
a -> getOutPrintWriter()
.println(a.getPackageName() + " "
+ a.getDeviceMacAddress()));
}
break;
case "associate": {
int userId = getNextArgInt();
String pkg = getNextArgRequired();
String address = getNextArgRequired();
addAssociation(new Association(userId, address, pkg, null, false,
System.currentTimeMillis()), userId);
}
break;
case "disassociate": {
removeAssociation(getNextArgInt(), getNextArgRequired(),
getNextArgRequired());
}
break;
case "simulate_connect": {
onDeviceConnected(getNextArgRequired());
}
break;
case "simulate_disconnect": {
onDeviceDisconnected(getNextArgRequired());
}
break;
default:
return handleDefaultCommands(cmd);
}
return 0;
} catch (Throwable t) {
Slog.e(LOG_TAG, "Error running a command: $ " + cmd, t);
getErrPrintWriter().println(Log.getStackTraceString(t));
return 1;
}
}
private int getNextArgInt() {
return Integer.parseInt(getNextArgRequired());
}
@Override
public void onHelp() {
getOutPrintWriter().println(USAGE);
}
}
private class BluetoothDeviceConnectedListener
extends BluetoothAdapter.BluetoothConnectionCallback {
@Override
public void onDeviceConnected(BluetoothDevice device) {
CompanionDeviceManagerService.this.onDeviceConnected(device.getAddress());
}
@Override
public void onDeviceDisconnected(BluetoothDevice device, @DisconnectReason int reason) {
Slog.d(LOG_TAG, device.getAddress() + " disconnected w/ reason: (" + reason + ") "
+ BluetoothAdapter.BluetoothConnectionCallback.disconnectReasonText(reason));
CompanionDeviceManagerService.this.onDeviceDisconnected(device.getAddress());
}
}
}