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

Commit cd4c6a39 authored by Guojing Yuan's avatar Guojing Yuan
Browse files

[CDM] Add CompanionExemptionProcessor

1. Auto revoke now checks if there's any association for the package.
2. Power saver now checks if there's any connected device for the
   package.
3. OnPackageModified no longer invalidates all the packages for the
   user.

Bug: 373957005
Test: manually tested 3 exemptions
Flag: EXEMPT bugfix

Change-Id: Icf1cc447306b7695767a713df78a763649e24f9d
parent 3c0c6fed
Loading
Loading
Loading
Loading
+31 −175
Original line number Diff line number Diff line
@@ -31,16 +31,13 @@ import static android.os.UserHandle.getCallingUserId;

import static com.android.internal.util.CollectionUtils.any;
import static com.android.internal.util.Preconditions.checkState;
import static com.android.internal.util.function.pooled.PooledLambda.obtainMessage;
import static com.android.server.companion.utils.PackageUtils.enforceUsesCompanionDeviceFeature;
import static com.android.server.companion.utils.PackageUtils.getPackageInfo;
import static com.android.server.companion.utils.PackageUtils.isRestrictedSettingsAllowed;
import static com.android.server.companion.utils.PermissionsUtils.enforceCallerCanManageAssociationsForPackage;
import static com.android.server.companion.utils.PermissionsUtils.enforceCallerIsSystemOr;
import static com.android.server.companion.utils.PermissionsUtils.enforceCallerIsSystemOrCanInteractWithUserId;

import static java.util.Objects.requireNonNull;
import static java.util.concurrent.TimeUnit.MINUTES;

import android.annotation.EnforcePermission;
import android.annotation.NonNull;
@@ -69,31 +66,22 @@ import android.companion.datatransfer.PermissionSyncRequest;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManagerInternal;
import android.net.MacAddress;
import android.net.NetworkPolicyManager;
import android.os.Binder;
import android.os.Environment;
import android.os.Parcel;
import android.os.ParcelFileDescriptor;
import android.os.PowerExemptionManager;
import android.os.PowerManagerInternal;
import android.os.RemoteException;
import android.os.ServiceManager;
import android.os.UserHandle;
import android.os.UserManager;
import android.permission.flags.Flags;
import android.util.ArraySet;
import android.util.ExceptionUtils;
import android.util.Slog;

import com.android.internal.app.IAppOpsService;
import com.android.internal.content.PackageMonitor;
import com.android.internal.notification.NotificationAccessConfirmationActivityContract;
import com.android.internal.os.BackgroundThread;
import com.android.internal.util.ArrayUtils;
import com.android.internal.util.DumpUtils;
import com.android.server.FgThread;
@@ -114,35 +102,27 @@ import com.android.server.companion.devicepresence.DevicePresenceProcessor;
import com.android.server.companion.devicepresence.ObservableUuid;
import com.android.server.companion.devicepresence.ObservableUuidStore;
import com.android.server.companion.transport.CompanionTransportManager;
import com.android.server.pm.UserManagerInternal;
import com.android.server.wm.ActivityTaskManagerInternal;

import java.io.File;
import java.io.FileDescriptor;
import java.io.PrintWriter;
import java.util.Collection;
import java.util.List;
import java.util.Set;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

@SuppressLint("LongLogTag")
public class CompanionDeviceManagerService extends SystemService {
    private static final String TAG = "CDM_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 MAX_CN_LENGTH = 500;

    private final ActivityTaskManagerInternal mAtmInternal;
    private final ActivityManagerInternal mAmInternal;
    private final IAppOpsService mAppOpsManager;
    private final PowerExemptionManager mPowerExemptionManager;
    private final PackageManagerInternal mPackageManagerInternal;

    private final AssociationStore mAssociationStore;
    private final SystemDataTransferRequestStore mSystemDataTransferRequestStore;
    private final ObservableUuidStore mObservableUuidStore;

    private final CompanionExemptionProcessor mCompanionExemptionProcessor;
    private final AssociationRequestsProcessor mAssociationRequestsProcessor;
    private final SystemDataTransferProcessor mSystemDataTransferProcessor;
    private final BackupRestoreProcessor mBackupRestoreProcessor;
@@ -156,12 +136,15 @@ public class CompanionDeviceManagerService extends SystemService {
        super(context);

        final ActivityManager activityManager = context.getSystemService(ActivityManager.class);
        mPowerExemptionManager = context.getSystemService(PowerExemptionManager.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);
        final PowerExemptionManager powerExemptionManager = context.getSystemService(
                PowerExemptionManager.class);
        final AppOpsManager appOpsManager = context.getSystemService(AppOpsManager.class);
        final ActivityTaskManagerInternal atmInternal = LocalServices.getService(
                ActivityTaskManagerInternal.class);
        final ActivityManagerInternal amInternal = LocalServices.getService(
                ActivityManagerInternal.class);
        final PackageManagerInternal packageManagerInternal = LocalServices.getService(
                PackageManagerInternal.class);
        final UserManager userManager = context.getSystemService(UserManager.class);
        final PowerManagerInternal powerManagerInternal = LocalServices.getService(
                PowerManagerInternal.class);
@@ -173,25 +156,29 @@ public class CompanionDeviceManagerService extends SystemService {

        // Init processors
        mAssociationRequestsProcessor = new AssociationRequestsProcessor(context,
                mPackageManagerInternal, mAssociationStore);
        mBackupRestoreProcessor = new BackupRestoreProcessor(context, mPackageManagerInternal,
                packageManagerInternal, mAssociationStore);
        mBackupRestoreProcessor = new BackupRestoreProcessor(context, packageManagerInternal,
                mAssociationStore, associationDiskStore, mSystemDataTransferRequestStore,
                mAssociationRequestsProcessor);

        mCompanionAppBinder = new CompanionAppBinder(context);

        mCompanionExemptionProcessor = new CompanionExemptionProcessor(context,
                powerExemptionManager, appOpsManager, packageManagerInternal, atmInternal,
                amInternal, mAssociationStore);

        mDevicePresenceProcessor = new DevicePresenceProcessor(context,
                mCompanionAppBinder, userManager, mAssociationStore, mObservableUuidStore,
                powerManagerInternal);
                powerManagerInternal, mCompanionExemptionProcessor);

        mTransportManager = new CompanionTransportManager(context, mAssociationStore);

        mDisassociationProcessor = new DisassociationProcessor(context, activityManager,
                mAssociationStore, mPackageManagerInternal, mDevicePresenceProcessor,
                mAssociationStore, packageManagerInternal, mDevicePresenceProcessor,
                mCompanionAppBinder, mSystemDataTransferRequestStore, mTransportManager);

        mSystemDataTransferProcessor = new SystemDataTransferProcessor(this,
                mPackageManagerInternal, mAssociationStore,
                packageManagerInternal, mAssociationStore,
                mSystemDataTransferRequestStore, mTransportManager);

        // TODO(b/279663946): move context sync to a dedicated system service
@@ -202,7 +189,6 @@ public class CompanionDeviceManagerService extends SystemService {
    public void onStart() {
        // Init association stores
        mAssociationStore.refreshCache();
        mAssociationStore.registerLocalListener(mAssociationStoreChangeListener);

        // Init UUID store
        mObservableUuidStore.getObservableUuidsForUser(getContext().getUserId());
@@ -240,11 +226,11 @@ public class CompanionDeviceManagerService extends SystemService {

        if (associations.isEmpty()) return;

        updateAtm(userId, associations);
        mCompanionExemptionProcessor.updateAtm(userId, associations);

        BackgroundThread.getHandler().sendMessageDelayed(
                obtainMessage(CompanionDeviceManagerService::maybeGrantAutoRevokeExemptions, this),
                MINUTES.toMillis(10));
        try (ExecutorService executor = Executors.newSingleThreadExecutor()) {
            executor.execute(mCompanionExemptionProcessor::updateAutoRevokeExemptions);
        }
    }

    @Override
@@ -274,18 +260,16 @@ public class CompanionDeviceManagerService extends SystemService {
            mObservableUuidStore.removeObservableUuid(userId, uuid.getUuid(), packageName);
        }

        mCompanionAppBinder.onPackagesChanged(userId);
        mCompanionAppBinder.onPackagesChanged(userId, packageName);
    }

    private void onPackageModifiedInternal(@UserIdInt int userId, @NonNull String packageName) {
        final List<AssociationInfo> associationsForPackage =
        final List<AssociationInfo> associations =
                mAssociationStore.getAssociationsByPackage(userId, packageName);
        for (AssociationInfo association : associationsForPackage) {
            updateSpecialAccessPermissionForAssociatedPackage(association.getUserId(),
                    association.getPackageName());
        if (!associations.isEmpty()) {
            mCompanionExemptionProcessor.exemptPackage(userId, packageName, false);
            mCompanionAppBinder.onPackagesChanged(userId, packageName);
        }

        mCompanionAppBinder.onPackagesChanged(userId);
    }

    private void onPackageAddedInternal(@UserIdInt int userId, @NonNull String packageName) {
@@ -750,130 +734,6 @@ public class CompanionDeviceManagerService extends SystemService {
        }
    }

    /**
     * Update special access for the association's package
     */
    public void updateSpecialAccessPermissionForAssociatedPackage(int userId, String packageName) {
        final PackageInfo packageInfo =
                getPackageInfo(getContext(), userId, packageName);

        Binder.withCleanCallingIdentity(() -> updateSpecialAccessPermissionAsSystem(packageInfo));
    }

    private void updateSpecialAccessPermissionAsSystem(PackageInfo packageInfo) {
        if (packageInfo == null) {
            return;
        }

        if (containsEither(packageInfo.requestedPermissions,
                android.Manifest.permission.RUN_IN_BACKGROUND,
                android.Manifest.permission.REQUEST_COMPANION_RUN_IN_BACKGROUND)) {
            mPowerExemptionManager.addToPermanentAllowList(packageInfo.packageName);
        } else {
            try {
                mPowerExemptionManager.removeFromPermanentAllowList(packageInfo.packageName);
            } catch (UnsupportedOperationException e) {
                Slog.w(TAG, packageInfo.packageName + " can't be removed from power save"
                        + " whitelist. It might due to the package is whitelisted by the system.");
            }
        }

        NetworkPolicyManager networkPolicyManager = NetworkPolicyManager.from(getContext());
        try {
            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);
            }
        } catch (IllegalArgumentException e) {
            Slog.e(TAG, e.getMessage());
        }

        exemptFromAutoRevoke(packageInfo.packageName, packageInfo.applicationInfo.uid);
    }

    private void exemptFromAutoRevoke(String packageName, int uid) {
        try {
            mAppOpsManager.setMode(
                    AppOpsManager.OP_AUTO_REVOKE_PERMISSIONS_IF_UNUSED,
                    uid,
                    packageName,
                    AppOpsManager.MODE_IGNORED);
        } catch (RemoteException e) {
            Slog.w(TAG, "Error while granting auto revoke exemption for " + packageName, e);
        }
    }

    private void updateAtm(int userId, List<AssociationInfo> associations) {
        final Set<Integer> companionAppUids = new ArraySet<>();
        for (AssociationInfo association : associations) {
            final int uid = mPackageManagerInternal.getPackageUid(association.getPackageName(),
                    0, userId);
            if (uid >= 0) {
                companionAppUids.add(uid);
            }
        }
        if (mAtmInternal != null) {
            mAtmInternal.setCompanionAppUids(userId, companionAppUids);
        }
        if (mAmInternal != null) {
            // Make a copy of the set and send it to ActivityManager.
            mAmInternal.setCompanionAppUids(userId, new ArraySet<>(companionAppUids));
        }
    }

    private void maybeGrantAutoRevokeExemptions() {
        Slog.d(TAG, "maybeGrantAutoRevokeExemptions()");

        PackageManager pm = getContext().getPackageManager();
        for (int userId : LocalServices.getService(UserManagerInternal.class).getUserIds()) {
            SharedPreferences pref = getContext().getSharedPreferences(
                    new File(Environment.getUserSystemDirectory(userId), PREF_FILE_NAME),
                    Context.MODE_PRIVATE);
            if (pref.getBoolean(PREF_KEY_AUTO_REVOKE_GRANTS_DONE, false)) {
                continue;
            }

            try {
                final List<AssociationInfo> associations =
                        mAssociationStore.getActiveAssociationsByUser(userId);
                for (AssociationInfo a : associations) {
                    try {
                        int uid = pm.getPackageUidAsUser(a.getPackageName(), userId);
                        exemptFromAutoRevoke(a.getPackageName(), uid);
                    } catch (PackageManager.NameNotFoundException e) {
                        Slog.w(TAG, "Unknown companion package: " + a.getPackageName(), e);
                    }
                }
            } finally {
                pref.edit().putBoolean(PREF_KEY_AUTO_REVOKE_GRANTS_DONE, true).apply();
            }
        }
    }

    private final AssociationStore.OnChangeListener mAssociationStoreChangeListener =
            new AssociationStore.OnChangeListener() {
                @Override
                public void onAssociationChanged(int changeType, AssociationInfo association) {
                    Slog.d(TAG, "onAssociationChanged changeType=[" + changeType
                            + "], association=[" + association);

                    final int userId = association.getUserId();
                    final List<AssociationInfo> updatedAssociations =
                            mAssociationStore.getActiveAssociationsByUser(userId);

                    updateAtm(userId, updatedAssociations);
                    updateSpecialAccessPermissionForAssociatedPackage(association.getUserId(),
                            association.getPackageName());
                }
            };

    private final PackageMonitor mPackageMonitor = new PackageMonitor() {
        @Override
        public void onPackageRemoved(String packageName, int uid) {
@@ -896,10 +756,6 @@ public class CompanionDeviceManagerService extends SystemService {
        }
    };

    private static <T> boolean containsEither(T[] array, T a, T b) {
        return ArrayUtils.contains(array, a) || ArrayUtils.contains(array, b);
    }

    private class LocalService implements CompanionDeviceManagerServiceInternal {

        @Override
+217 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2024 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.app.AppOpsManager.MODE_ALLOWED;
import static android.app.AppOpsManager.MODE_IGNORED;

import static com.android.server.companion.utils.PackageUtils.getPackageInfo;

import android.annotation.SuppressLint;
import android.app.ActivityManagerInternal;
import android.app.AppOpsManager;
import android.companion.AssociationInfo;
import android.content.Context;
import android.content.SharedPreferences;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManagerInternal;
import android.net.NetworkPolicyManager;
import android.os.Binder;
import android.os.Environment;
import android.os.PowerExemptionManager;
import android.util.ArraySet;
import android.util.Slog;

import com.android.internal.util.ArrayUtils;
import com.android.server.LocalServices;
import com.android.server.companion.association.AssociationStore;
import com.android.server.pm.UserManagerInternal;
import com.android.server.wm.ActivityTaskManagerInternal;

import java.io.File;
import java.util.List;
import java.util.Set;

@SuppressLint("LongLogTag")
public class CompanionExemptionProcessor {

    private static final String TAG = "CDM_CompanionExemptionProcessor";

    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 final Context mContext;
    private final PowerExemptionManager mPowerExemptionManager;
    private final AppOpsManager mAppOpsManager;
    private final PackageManagerInternal mPackageManager;
    private final ActivityTaskManagerInternal mAtmInternal;
    private final ActivityManagerInternal mAmInternal;
    private final AssociationStore mAssociationStore;

    public CompanionExemptionProcessor(Context context, PowerExemptionManager powerExemptionManager,
            AppOpsManager appOpsManager, PackageManagerInternal packageManager,
            ActivityTaskManagerInternal atmInternal, ActivityManagerInternal amInternal,
            AssociationStore associationStore) {
        mContext = context;
        mPowerExemptionManager = powerExemptionManager;
        mAppOpsManager = appOpsManager;
        mPackageManager = packageManager;
        mAtmInternal = atmInternal;
        mAmInternal = amInternal;
        mAssociationStore = associationStore;

        mAssociationStore.registerLocalListener(new AssociationStore.OnChangeListener() {
            @Override
            public void onAssociationChanged(int changeType, AssociationInfo association) {
                final int userId = association.getUserId();
                final List<AssociationInfo> updatedAssociations =
                        mAssociationStore.getActiveAssociationsByUser(userId);

                updateAtm(userId, updatedAssociations);
            }
        });
    }

    /**
     * Update ActivityManager and ActivityTaskManager exemptions
     */
    public void updateAtm(int userId, List<AssociationInfo> associations) {
        final Set<Integer> companionAppUids = new ArraySet<>();
        for (AssociationInfo association : associations) {
            int uid = mPackageManager.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 the set and send it to ActivityManager.
            mAmInternal.setCompanionAppUids(userId, new ArraySet<>(companionAppUids));
        }
    }

    /**
     * Update special access for the association's package
     */
    public void exemptPackage(int userId, String packageName, boolean hasPresentDevices) {
        final PackageInfo packageInfo = getPackageInfo(mContext, userId, packageName);

        Binder.withCleanCallingIdentity(
                () -> exemptPackageAsSystem(userId, packageInfo, hasPresentDevices));
    }

    @SuppressLint("MissingPermission")
    private void exemptPackageAsSystem(int userId, PackageInfo packageInfo,
            boolean hasPresentDevices) {
        if (packageInfo == null) {
            return;
        }

        // If the app has run-in-bg permission and present devices, add it to power saver allowlist.
        if (containsEither(packageInfo.requestedPermissions,
                android.Manifest.permission.RUN_IN_BACKGROUND,
                android.Manifest.permission.REQUEST_COMPANION_RUN_IN_BACKGROUND)
                && hasPresentDevices) {
            mPowerExemptionManager.addToPermanentAllowList(packageInfo.packageName);
        } else {
            try {
                mPowerExemptionManager.removeFromPermanentAllowList(packageInfo.packageName);
            } catch (UnsupportedOperationException e) {
                Slog.w(TAG, packageInfo.packageName + " can't be removed from power save"
                        + " allowlist. It might be due to the package being allowlisted by the"
                        + " system.");
            }
        }

        // If the app has run-in-bg permission and present device, allow metered network use.
        NetworkPolicyManager networkPolicyManager = NetworkPolicyManager.from(mContext);
        try {
            if (containsEither(packageInfo.requestedPermissions,
                    android.Manifest.permission.USE_DATA_IN_BACKGROUND,
                    android.Manifest.permission.REQUEST_COMPANION_USE_DATA_IN_BACKGROUND)
                    && hasPresentDevices) {
                networkPolicyManager.addUidPolicy(
                        packageInfo.applicationInfo.uid,
                        NetworkPolicyManager.POLICY_ALLOW_METERED_BACKGROUND);
            } else {
                networkPolicyManager.removeUidPolicy(
                        packageInfo.applicationInfo.uid,
                        NetworkPolicyManager.POLICY_ALLOW_METERED_BACKGROUND);
            }
        } catch (IllegalArgumentException e) {
            Slog.e(TAG, e.getMessage());
        }

        updateAutoRevokeExemption(packageInfo.packageName, packageInfo.applicationInfo.uid,
                !mAssociationStore.getActiveAssociationsByPackage(userId,
                        packageInfo.packageName).isEmpty());
    }

    /**
     * Update auto revoke exemptions.
     * If the app has any association, exempt it from permission auto revoke.
     */
    public void updateAutoRevokeExemptions() {
        Slog.d(TAG, "maybeGrantAutoRevokeExemptions()");

        PackageManager pm = mContext.getPackageManager();
        for (int userId : LocalServices.getService(UserManagerInternal.class).getUserIds()) {
            SharedPreferences pref = mContext.getSharedPreferences(
                    new File(Environment.getUserSystemDirectory(userId), PREF_FILE_NAME),
                    Context.MODE_PRIVATE);
            if (pref.getBoolean(PREF_KEY_AUTO_REVOKE_GRANTS_DONE, false)) {
                continue;
            }

            try {
                final List<AssociationInfo> associations =
                        mAssociationStore.getActiveAssociationsByUser(userId);
                for (AssociationInfo a : associations) {
                    try {
                        int uid = pm.getPackageUidAsUser(a.getPackageName(), userId);
                        updateAutoRevokeExemption(a.getPackageName(), uid, true);
                    } catch (PackageManager.NameNotFoundException e) {
                        Slog.w(TAG, "Unknown companion package: " + a.getPackageName(), e);
                    }
                }
            } finally {
                pref.edit().putBoolean(PREF_KEY_AUTO_REVOKE_GRANTS_DONE, true).apply();
            }
        }
    }

    @SuppressLint("MissingPermission")
    private void updateAutoRevokeExemption(String packageName, int uid, boolean hasAssociations) {
        try {
            mAppOpsManager.setMode(
                    AppOpsManager.OP_AUTO_REVOKE_PERMISSIONS_IF_UNUSED,
                    uid,
                    packageName,
                    hasAssociations ? MODE_IGNORED : MODE_ALLOWED);
        } catch (Exception e) {
            Slog.e(TAG, "Error while granting auto revoke exemption for " + packageName, e);
        }
    }

    private <T> boolean containsEither(T[] array, T a, T b) {
        return ArrayUtils.contains(array, a) || ArrayUtils.contains(array, b);
    }

}
+2 −6

File changed.

Preview size limit exceeded, changes collapsed.

+17 −10

File changed.

Preview size limit exceeded, changes collapsed.