Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 151da5b2 authored by Eugene Susla's avatar Eugene Susla
Browse files

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
parent 9c3ddc9c
Loading
Loading
Loading
Loading
+1 −1
Original line number Diff line number Diff line
@@ -3689,7 +3689,7 @@ public final class BluetoothAdapter {
     *
     * @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
+39 −7
Original line number Diff line number Diff line
@@ -37,6 +37,8 @@ public final class Association implements Parcelable {
    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 @@ public final class Association implements Parcelable {



    // 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 final class Association implements Parcelable {
    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 @@ public final class Association implements Parcelable {
        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 @@ public final class Association implements Parcelable {
        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 @@ public final class Association implements Parcelable {
        return "Association { " +
                "userId = " + mUserId + ", " +
                "deviceMacAddress = " + mDeviceMacAddress + ", " +
                "packageName = " + mPackageName +
                "packageName = " + mPackageName + ", " +
                "deviceProfile = " + mDeviceProfile + ", " +
                "keepProfilePrivilegesWhenDeviceAway = " + mKeepProfilePrivilegesWhenDeviceAway +
        " }";
    }

@@ -119,7 +137,9 @@ public final class Association implements Parcelable {
        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 @@ public final class Association implements Parcelable {
        _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 @@ public final class Association implements Parcelable {
        // 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 @@ public final class Association implements Parcelable {
        // 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 @@ public final class Association implements Parcelable {
        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 @@ public final class Association implements Parcelable {
    };

    @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() {}

+2 −1
Original line number Diff line number Diff line
@@ -321,7 +321,8 @@ public class DeviceDiscoveryService extends Service {
    }

    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() {
+178 −36
Original line number Diff line number Diff line
@@ -31,8 +31,12 @@ import static java.util.concurrent.TimeUnit.MINUTES;
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.FeatureInfo;
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.ServiceManager;
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.AtomicFile;
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 @@ import java.util.function.Function;
//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 @@ public class CompanionDeviceManagerService extends SystemService implements Bind
    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 @@ public class CompanionDeviceManagerService extends SystemService implements Bind
    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 @@ public class CompanionDeviceManagerService extends SystemService implements Bind
            @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);
@@ -197,6 +218,18 @@ public class CompanionDeviceManagerService extends SystemService implements Bind
        publishBinderService(Context.COMPANION_DEVICE_SERVICE, mImpl);
    }

    @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();
@@ -495,7 +528,8 @@ public class CompanionDeviceManagerService extends SystemService implements Bind

            fout.append("Companion Device Associations:").append('\n');
            synchronized (mLock) {
                forEach(mCachedAssociations, a -> {
                for (UserInfo user : getAllUsers()) {
                    forEach(mCachedAssociations.get(user.id), a -> {
                        fout.append("  ")
                                .append("u").append("" + a.getUserId()).append(": ")
                                .append(a.getPackageName()).append(" - ")
@@ -504,6 +538,7 @@ public class CompanionDeviceManagerService extends SystemService implements Bind
                }
            }
        }
    }

    private static int getCallingUserId() {
        return UserHandle.getUserId(Binder.getCallingUid());
@@ -513,32 +548,34 @@ public class CompanionDeviceManagerService extends SystemService implements Bind
        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 @@ public class CompanionDeviceManagerService extends SystemService implements Bind
        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 @@ public class CompanionDeviceManagerService extends SystemService implements Bind
            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 @@ public class CompanionDeviceManagerService extends SystemService implements Bind
                    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,15 +718,19 @@ public class CompanionDeviceManagerService extends SystemService implements Bind
    @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
@@ -714,10 +758,15 @@ public class CompanionDeviceManagerService extends SystemService implements Bind
                    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 @@ public class CompanionDeviceManagerService extends SystemService implements Bind
        }
    }

    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 @@ public class CompanionDeviceManagerService extends SystemService implements Bind
                } 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 @@ public class CompanionDeviceManagerService extends SystemService implements Bind
        }
    }


    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());
        }
    }
}