Store CDM device profile and apply role when device is connected
Test: manual - ensure role privileges are granted/revoked when device is connected/disconnected
Bug: 165951651
Change-Id: Id24a4b3a3510781d9105763b1722f44583a7fd7c
diff --git a/core/java/android/bluetooth/BluetoothAdapter.java b/core/java/android/bluetooth/BluetoothAdapter.java
index c07cd52..1713a0c 100644
--- a/core/java/android/bluetooth/BluetoothAdapter.java
+++ b/core/java/android/bluetooth/BluetoothAdapter.java
@@ -3689,7 +3689,7 @@
*
* @hide
*/
- public abstract class BluetoothConnectionCallback {
+ public abstract static class BluetoothConnectionCallback {
/**
* Callback triggered when a bluetooth device (classic or BLE) is connected
* @param device is the connected bluetooth device
diff --git a/core/java/android/companion/Association.java b/core/java/android/companion/Association.java
index 06a3f2f..17bf11b 100644
--- a/core/java/android/companion/Association.java
+++ b/core/java/android/companion/Association.java
@@ -37,6 +37,8 @@
private final @UserIdInt int mUserId;
private final @NonNull String mDeviceMacAddress;
private final @NonNull String mPackageName;
+ private final @Nullable String mDeviceProfile;
+ private final boolean mKeepProfilePrivilegesWhenDeviceAway;
/** @hide */
public int getUserId() {
@@ -45,7 +47,7 @@
- // Code below generated by codegen v1.0.15.
+ // Code below generated by codegen v1.0.21.
//
// DO NOT MODIFY!
// CHECKSTYLE:OFF Generated code
@@ -67,7 +69,9 @@
public Association(
@UserIdInt int userId,
@NonNull String deviceMacAddress,
- @NonNull String packageName) {
+ @NonNull String packageName,
+ @Nullable String deviceProfile,
+ boolean keepProfilePrivilegesWhenDeviceAway) {
this.mUserId = userId;
com.android.internal.util.AnnotationValidations.validate(
UserIdInt.class, null, mUserId);
@@ -77,6 +81,8 @@
this.mPackageName = packageName;
com.android.internal.util.AnnotationValidations.validate(
NonNull.class, null, mPackageName);
+ this.mDeviceProfile = deviceProfile;
+ this.mKeepProfilePrivilegesWhenDeviceAway = keepProfilePrivilegesWhenDeviceAway;
// onConstructed(); // You can define this method to get a callback
}
@@ -91,6 +97,16 @@
return mPackageName;
}
+ @DataClass.Generated.Member
+ public @Nullable String getDeviceProfile() {
+ return mDeviceProfile;
+ }
+
+ @DataClass.Generated.Member
+ public boolean isKeepProfilePrivilegesWhenDeviceAway() {
+ return mKeepProfilePrivilegesWhenDeviceAway;
+ }
+
@Override
@DataClass.Generated.Member
public String toString() {
@@ -100,7 +116,9 @@
return "Association { " +
"userId = " + mUserId + ", " +
"deviceMacAddress = " + mDeviceMacAddress + ", " +
- "packageName = " + mPackageName +
+ "packageName = " + mPackageName + ", " +
+ "deviceProfile = " + mDeviceProfile + ", " +
+ "keepProfilePrivilegesWhenDeviceAway = " + mKeepProfilePrivilegesWhenDeviceAway +
" }";
}
@@ -119,7 +137,9 @@
return true
&& mUserId == that.mUserId
&& Objects.equals(mDeviceMacAddress, that.mDeviceMacAddress)
- && Objects.equals(mPackageName, that.mPackageName);
+ && Objects.equals(mPackageName, that.mPackageName)
+ && Objects.equals(mDeviceProfile, that.mDeviceProfile)
+ && mKeepProfilePrivilegesWhenDeviceAway == that.mKeepProfilePrivilegesWhenDeviceAway;
}
@Override
@@ -132,6 +152,8 @@
_hash = 31 * _hash + mUserId;
_hash = 31 * _hash + Objects.hashCode(mDeviceMacAddress);
_hash = 31 * _hash + Objects.hashCode(mPackageName);
+ _hash = 31 * _hash + Objects.hashCode(mDeviceProfile);
+ _hash = 31 * _hash + Boolean.hashCode(mKeepProfilePrivilegesWhenDeviceAway);
return _hash;
}
@@ -141,9 +163,14 @@
// You can override field parcelling by defining methods like:
// void parcelFieldName(Parcel dest, int flags) { ... }
+ byte flg = 0;
+ if (mKeepProfilePrivilegesWhenDeviceAway) flg |= 0x10;
+ if (mDeviceProfile != null) flg |= 0x8;
+ dest.writeByte(flg);
dest.writeInt(mUserId);
dest.writeString(mDeviceMacAddress);
dest.writeString(mPackageName);
+ if (mDeviceProfile != null) dest.writeString(mDeviceProfile);
}
@Override
@@ -157,9 +184,12 @@
// You can override field unparcelling by defining methods like:
// static FieldType unparcelFieldName(Parcel in) { ... }
+ byte flg = in.readByte();
+ boolean keepProfilePrivilegesWhenDeviceAway = (flg & 0x10) != 0;
int userId = in.readInt();
String deviceMacAddress = in.readString();
String packageName = in.readString();
+ String deviceProfile = (flg & 0x8) == 0 ? null : in.readString();
this.mUserId = userId;
com.android.internal.util.AnnotationValidations.validate(
@@ -170,6 +200,8 @@
this.mPackageName = packageName;
com.android.internal.util.AnnotationValidations.validate(
NonNull.class, null, mPackageName);
+ this.mDeviceProfile = deviceProfile;
+ this.mKeepProfilePrivilegesWhenDeviceAway = keepProfilePrivilegesWhenDeviceAway;
// onConstructed(); // You can define this method to get a callback
}
@@ -189,10 +221,10 @@
};
@DataClass.Generated(
- time = 1599083149942L,
- codegenVersion = "1.0.15",
+ time = 1606940835778L,
+ codegenVersion = "1.0.21",
sourceFile = "frameworks/base/core/java/android/companion/Association.java",
- inputSignatures = "private final @android.annotation.UserIdInt int mUserId\nprivate final @android.annotation.NonNull java.lang.String mDeviceMacAddress\nprivate final @android.annotation.NonNull java.lang.String mPackageName\npublic int getUserId()\nclass Association extends java.lang.Object implements [android.os.Parcelable]\n@com.android.internal.util.DataClass(genEqualsHashCode=true, genToString=true, genHiddenConstructor=true)")
+ inputSignatures = "private final @android.annotation.UserIdInt int mUserId\nprivate final @android.annotation.NonNull java.lang.String mDeviceMacAddress\nprivate final @android.annotation.NonNull java.lang.String mPackageName\nprivate final @android.annotation.Nullable java.lang.String mDeviceProfile\nprivate final boolean mKeepProfilePrivilegesWhenDeviceAway\npublic int getUserId()\nclass Association extends java.lang.Object implements [android.os.Parcelable]\n@com.android.internal.util.DataClass(genEqualsHashCode=true, genToString=true, genHiddenConstructor=true)")
@Deprecated
private void __metadata() {}
diff --git a/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/DeviceDiscoveryService.java b/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/DeviceDiscoveryService.java
index 2fe351e..fd71670 100644
--- a/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/DeviceDiscoveryService.java
+++ b/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/DeviceDiscoveryService.java
@@ -321,7 +321,8 @@
}
void onDeviceSelected(String callingPackage, String deviceAddress) {
- mServiceCallback.complete(new Association(getUserId(), deviceAddress, callingPackage));
+ mServiceCallback.complete(new Association(
+ getUserId(), deviceAddress, callingPackage, mRequest.getDeviceProfile(), false));
}
void onCancel() {
diff --git a/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java b/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java
index 5fedf9f..0a80b02 100644
--- a/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java
+++ b/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java
@@ -31,8 +31,12 @@
import android.annotation.CheckResult;
import android.annotation.NonNull;
import android.annotation.Nullable;
+import android.annotation.SuppressLint;
import android.app.AppOpsManager;
import android.app.PendingIntent;
+import android.app.role.RoleManager;
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothDevice;
import android.companion.Association;
import android.companion.AssociationRequest;
import android.companion.CompanionDeviceManager;
@@ -47,6 +51,7 @@
import android.content.pm.PackageInfo;
import android.content.pm.PackageItemInfo;
import android.content.pm.PackageManager;
+import android.content.pm.UserInfo;
import android.net.NetworkPolicyManager;
import android.os.Binder;
import android.os.Environment;
@@ -62,6 +67,7 @@
import android.os.ShellCallback;
import android.os.ShellCommand;
import android.os.UserHandle;
+import android.os.UserManager;
import android.provider.Settings;
import android.provider.SettingsStringUtil.ComponentNameSet;
import android.text.BidiFormatter;
@@ -70,6 +76,7 @@
import android.util.ExceptionUtils;
import android.util.Log;
import android.util.Slog;
+import android.util.SparseArray;
import android.util.Xml;
import com.android.internal.annotations.GuardedBy;
@@ -116,12 +123,16 @@
//TODO on associate called again after configuration change -> replace old callback with new
//TODO avoid leaking calling activity in IFindDeviceCallback (see PrintManager#print for example)
/** @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,
".DeviceDiscoveryService");
+ // 10 min
+ public static final int DEVICE_DISCONNECT_PROFILE_REVOKE_DELAY_MS = 10 * 60 * 1000;
+
private static final boolean DEBUG = false;
private static final String LOG_TAG = "CompanionDeviceManagerService";
@@ -132,6 +143,8 @@
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_PERSISTENT_PROFILE_GRANTS = "persistent_profile_grants";
private static final String XML_FILE_NAME = "companion_device_manager_associations.xml";
private final CompanionDeviceManagerImpl mImpl;
@@ -139,21 +152,29 @@
private PowerWhitelistManager mPowerWhitelistManager;
private PerUser<ServiceConnector<ICompanionDeviceDiscoveryService>> mServiceConnectors;
private IAppOpsService mAppOpsManager;
+ private RoleManager mRoleManager;
+ private BluetoothAdapter mBluetoothAdapter;
private IFindDeviceCallback mFindDeviceCallback;
private AssociationRequest mRequest;
private String mCallingPackage;
private AndroidFuture<Association> mOngoingDeviceDiscovery;
+ private BluetoothDeviceConnectedListener mBluetoothDeviceConnectedListener =
+ new BluetoothDeviceConnectedListener();
+ private List<String> mCurrentlyConnectedDevices = new ArrayList<>();
+
private final Object mLock = new Object();
+ /** userId -> [association] */
@GuardedBy("mLock")
- private @Nullable Set<Association> mCachedAssociations = null;
+ private @Nullable SparseArray<Set<Association>> mCachedAssociations = new SparseArray<>();
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));
@@ -184,9 +205,9 @@
@Override
public void onPackageModified(String packageName) {
int userId = getChangingUserId();
- if (!ArrayUtils.isEmpty(getAllAssociations(userId, packageName))) {
- updateSpecialAccessPermissionForAssociatedPackage(packageName, userId);
- }
+ forEach(getAllAssociations(userId, packageName), association -> {
+ updateSpecialAccessPermissionForAssociatedPackage(association);
+ });
}
}.register(getContext(), FgThread.get().getLooper(), UserHandle.ALL, true);
@@ -198,6 +219,18 @@
}
@Override
+ public void onBootPhase(int phase) {
+ if (phase == SystemService.PHASE_SYSTEM_SERVICES_READY) {
+ mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
+ if (mBluetoothAdapter != null) {
+ mBluetoothAdapter.registerBluetoothConnectionCallback(
+ getContext().getMainExecutor(),
+ mBluetoothDeviceConnectedListener);
+ }
+ }
+ }
+
+ @Override
public void onUserUnlocking(@NonNull TargetUser user) {
int userHandle = user.getUserIdentifier();
Set<Association> associations = getAllAssociations(userHandle);
@@ -495,12 +528,14 @@
fout.append("Companion Device Associations:").append('\n');
synchronized (mLock) {
- forEach(mCachedAssociations, a -> {
- fout.append(" ")
- .append("u").append("" + a.getUserId()).append(": ")
- .append(a.getPackageName()).append(" - ")
- .append(a.getDeviceMacAddress()).append('\n');
- });
+ 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');
+ });
+ }
}
}
}
@@ -513,32 +548,34 @@
return Binder.getCallingUid() == Process.SYSTEM_UID;
}
- void addAssociation(int userId, String packageName, String deviceAddress) {
- addAssociation(new Association(userId, deviceAddress, packageName));
- }
-
void addAssociation(Association association) {
- updateSpecialAccessPermissionForAssociatedPackage(
- association.getPackageName(), association.getUserId());
+ updateSpecialAccessPermissionForAssociatedPackage(association);
recordAssociation(association);
}
void removeAssociation(int userId, String pkg, String deviceMacAddress) {
- updateAssociations(associations -> CollectionUtils.remove(associations,
- new Association(userId, deviceMacAddress, pkg)));
+ updateAssociations(associations -> CollectionUtils.filter(associations, association -> {
+ return association.getUserId() != userId
+ || !Objects.equals(association.getDeviceMacAddress(), deviceMacAddress)
+ || !Objects.equals(association.getPackageName(), pkg);
+ }));
}
- private void updateSpecialAccessPermissionForAssociatedPackage(String packageName, int userId) {
- PackageInfo packageInfo = getPackageInfo(packageName, userId);
+ private void updateSpecialAccessPermissionForAssociatedPackage(Association association) {
+ PackageInfo packageInfo = getPackageInfo(
+ association.getPackageName(),
+ association.getUserId());
if (packageInfo == null) {
return;
}
Binder.withCleanCallingIdentity(obtainRunnable(CompanionDeviceManagerService::
- updateSpecialAccessPermissionAsSystem, this, packageInfo).recycleOnUse());
+ updateSpecialAccessPermissionAsSystem, this, association, packageInfo)
+ .recycleOnUse());
}
- private void updateSpecialAccessPermissionAsSystem(PackageInfo packageInfo) {
+ 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)) {
@@ -602,10 +639,6 @@
updateAssociations(associations -> CollectionUtils.add(associations, association));
}
- private void recordAssociation(String privilegedPackage, String deviceAddress) {
- recordAssociation(new Association(getCallingUserId(), deviceAddress, privilegedPackage));
- }
-
private void updateAssociations(Function<Set<Association>, Set<Association>> update) {
updateAssociations(update, getCallingUserId());
}
@@ -625,7 +658,7 @@
if (DEBUG) {
Slog.i(LOG_TAG, "Updating associations: " + old + " --> " + associations);
}
- mCachedAssociations = Collections.unmodifiableSet(associations);
+ mCachedAssociations.put(userId, Collections.unmodifiableSet(associations));
BackgroundThread.getHandler().sendMessage(PooledLambda.obtainMessage(
CompanionDeviceManagerService::persistAssociations,
this, associations, userId));
@@ -651,10 +684,17 @@
xml.startTag(null, XML_TAG_ASSOCIATIONS);
forEach(associations, association -> {
- xml.startTag(null, XML_TAG_ASSOCIATION)
+ XmlSerializer tag = xml.startTag(null, XML_TAG_ASSOCIATION)
.attribute(null, XML_ATTR_PACKAGE, association.getPackageName())
- .attribute(null, XML_ATTR_DEVICE, association.getDeviceMacAddress())
- .endTag(null, XML_TAG_ASSOCIATION);
+ .attribute(null, XML_ATTR_DEVICE,
+ association.getDeviceMacAddress());
+ if (association.getDeviceProfile() != null) {
+ tag.attribute(null, XML_ATTR_PROFILE, association.getDeviceProfile());
+ tag.attribute(null, XML_ATTR_PERSISTENT_PROFILE_GRANTS,
+ Boolean.toString(
+ association.isKeepProfilePrivilegesWhenDeviceAway()));
+ }
+ tag.endTag(null, XML_TAG_ASSOCIATION);
});
xml.endTag(null, XML_TAG_ASSOCIATIONS);
@@ -678,17 +718,21 @@
@Nullable
private Set<Association> getAllAssociations(int userId) {
synchronized (mLock) {
- if (mCachedAssociations == null) {
- mCachedAssociations = Collections.unmodifiableSet(
- emptyIfNull(readAllAssociations(userId)));
+ if (mCachedAssociations.get(userId) == null) {
+ mCachedAssociations.put(userId, Collections.unmodifiableSet(
+ emptyIfNull(readAllAssociations(userId))));
if (DEBUG) {
Slog.i(LOG_TAG, "Read associations from disk: " + mCachedAssociations);
}
}
- return mCachedAssociations;
+ return mCachedAssociations.get(userId);
}
}
+ private List<UserInfo> getAllUsers() {
+ return getContext().getSystemService(UserManager.class).getUsers();
+ }
+
@Nullable
private Set<Association> getAllAssociations(int userId, @Nullable String packageFilter) {
return CollectionUtils.filter(
@@ -714,10 +758,15 @@
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_PERSISTENT_PROFILE_GRANTS));
+
if (appPackage == null || deviceAddress == null) continue;
result = ArrayUtils.add(result,
- new Association(userId, deviceAddress, appPackage));
+ new Association(userId, deviceAddress, appPackage,
+ profile, persistentGrants));
}
return result;
} catch (XmlPullParserException | IOException e) {
@@ -727,6 +776,77 @@
}
}
+ void onDeviceConnected(String address) {
+ mCurrentlyConnectedDevices.add(address);
+
+ Handler.getMain().removeCallbacksAndMessages(getDisconnectJobHandlerId(address));
+
+ for (UserInfo user : getAllUsers()) {
+ for (Association association : getAllAssociations(user.id)) {
+ if (Objects.equals(address, association.getDeviceMacAddress())) {
+ if (association.getDeviceProfile() != null) {
+ Log.i(LOG_TAG, "Granting role " + association.getDeviceProfile()
+ + " to " + association.getPackageName()
+ + " due to device connected: " + address);
+ mRoleManager.addRoleHolderAsUser(
+ association.getDeviceProfile(),
+ association.getPackageName(),
+ RoleManager.MANAGE_HOLDERS_FLAG_DONT_KILL_APP,
+ UserHandle.of(association.getUserId()),
+ getContext().getMainExecutor(),
+ success -> {
+ if (!success) {
+ Log.e(LOG_TAG, "Failed to grant device profile role "
+ + association.getDeviceProfile()
+ + " to " + association.getPackageName()
+ + " for user " + association.getUserId());
+ }
+ });
+ }
+ }
+ }
+ }
+ }
+
+ void onDeviceDisconnected(String address) {
+ mCurrentlyConnectedDevices.remove(address);
+
+ Handler.getMain().postDelayed(() -> {
+ if (!mCurrentlyConnectedDevices.contains(address)) {
+ for (UserInfo user : getAllUsers()) {
+ for (Association association : getAllAssociations(user.id)) {
+ if (association.getDeviceProfile() != null
+ && Objects.equals(address, association.getDeviceMacAddress())
+ && !association.isKeepProfilePrivilegesWhenDeviceAway()) {
+ Log.i(LOG_TAG, "Revoking role " + association.getDeviceProfile()
+ + " to " + association.getPackageName()
+ + " due to device disconnected: " + address);
+ mRoleManager.removeRoleHolderAsUser(
+ association.getDeviceProfile(),
+ association.getPackageName(),
+ RoleManager.MANAGE_HOLDERS_FLAG_DONT_KILL_APP,
+ UserHandle.of(association.getUserId()),
+ getContext().getMainExecutor(),
+ success -> {
+ if (!success) {
+ Log.e(LOG_TAG, "Failed to revoke device profile role "
+ + association.getDeviceProfile()
+ + " to " + association.getPackageName()
+ + " for user " + association.getUserId());
+ }
+ });
+ }
+ }
+ }
+ }
+ }, getDisconnectJobHandlerId(address), DEVICE_DISCONNECT_PROFILE_REVOKE_DELAY_MS);
+ }
+
+ @NonNull
+ private String getDisconnectJobHandlerId(String address) {
+ return "CDM_onDisconnected_" + address;
+ }
+
private class ShellCmd extends ShellCommand {
public static final String USAGE = "help\n"
+ "list USER_ID\n"
@@ -749,13 +869,22 @@
} break;
case "associate": {
- addAssociation(getNextArgInt(), getNextArgRequired(), getNextArgRequired());
+ addAssociation(new Association(getNextArgInt(), getNextArgRequired(),
+ getNextArgRequired(), null, false));
} 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;
@@ -771,4 +900,17 @@
}
}
+
+ private class BluetoothDeviceConnectedListener
+ extends BluetoothAdapter.BluetoothConnectionCallback {
+ @Override
+ public void onDeviceConnected(BluetoothDevice device) {
+ CompanionDeviceManagerService.this.onDeviceConnected(device.getAddress());
+ }
+
+ @Override
+ public void onDeviceDisconnected(BluetoothDevice device) {
+ CompanionDeviceManagerService.this.onDeviceDisconnected(device.getAddress());
+ }
+ }
}