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

Commit 5f07744a authored by Evan Chen's avatar Evan Chen
Browse files

Move association revoke logic to AssociationRevokeProcessor

Bug: 318413151
Test: CDM CTS tests
Change-Id: I45037b26602561516fc68480f90a66e2180d3a7d
parent 77c6fd2c
Loading
Loading
Loading
Loading
+369 −0
Original line number Original line 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.ActivityManager.RunningAppProcessInfo.IMPORTANCE_VISIBLE;
import static android.companion.AssociationRequest.DEVICE_PROFILE_AUTOMOTIVE_PROJECTION;

import static com.android.internal.util.CollectionUtils.any;
import static com.android.server.companion.MetricUtils.logRemoveAssociation;
import static com.android.server.companion.RolesUtils.removeRoleHolderForAssociation;
import static com.android.server.companion.CompanionDeviceManagerService.PerUserAssociationSet;

import android.annotation.NonNull;
import android.annotation.SuppressLint;
import android.annotation.UserIdInt;
import android.app.ActivityManager;
import android.companion.AssociationInfo;
import android.content.Context;
import android.content.pm.PackageManagerInternal;
import android.os.Binder;
import android.os.UserHandle;
import android.util.ArraySet;
import android.util.Log;
import android.util.Slog;

import com.android.internal.annotations.GuardedBy;
import com.android.server.companion.datatransfer.SystemDataTransferRequestStore;
import com.android.server.companion.presence.CompanionDevicePresenceMonitor;

import java.util.HashMap;
import java.util.Map;
import java.util.Set;

/**
 * A class response for Association removal.
 */
@SuppressLint("LongLogTag")
public class AssociationRevokeProcessor {

    private static final String TAG = "CDM_AssociationRevokeProcessor";
    private static final boolean DEBUG = false;
    private final @NonNull Context mContext;
    private final @NonNull CompanionDeviceManagerService mService;
    private final @NonNull AssociationStoreImpl mAssociationStore;
    private final @NonNull PackageManagerInternal mPackageManagerInternal;
    private final @NonNull CompanionDevicePresenceMonitor mDevicePresenceMonitor;
    private final @NonNull SystemDataTransferRequestStore mSystemDataTransferRequestStore;
    private final @NonNull CompanionApplicationController mCompanionAppController;
    private final OnPackageVisibilityChangeListener mOnPackageVisibilityChangeListener;
    private final ActivityManager mActivityManager;

    /**
     * A structure that consists of a set of revoked associations that pending for role holder
     * removal per each user.
     *
     * @see #maybeRemoveRoleHolderForAssociation(AssociationInfo)
     * @see #addToPendingRoleHolderRemoval(AssociationInfo)
     * @see #removeFromPendingRoleHolderRemoval(AssociationInfo)
     * @see #getPendingRoleHolderRemovalAssociationsForUser(int)
     */
    @GuardedBy("mRevokedAssociationsPendingRoleHolderRemoval")
    private final PerUserAssociationSet mRevokedAssociationsPendingRoleHolderRemoval =
            new PerUserAssociationSet();
    /**
     * Contains uid-s of packages pending to be removed from the role holder list (after
     * revocation of an association), which will happen one the package is no longer visible to the
     * user.
     * For quicker uid -> (userId, packageName) look-up this is not a {@code Set<Integer>} but
     * a {@code Map<Integer, String>} which maps uid-s to packageName-s (userId-s can be derived
     * from uid-s using {@link UserHandle#getUserId(int)}).
     *
     * @see #maybeRemoveRoleHolderForAssociation(AssociationInfo)
     * @see #addToPendingRoleHolderRemoval(AssociationInfo)
     * @see #removeFromPendingRoleHolderRemoval(AssociationInfo)
     */
    @GuardedBy("mRevokedAssociationsPendingRoleHolderRemoval")
    private final Map<Integer, String> mUidsPendingRoleHolderRemoval = new HashMap<>();

    AssociationRevokeProcessor(@NonNull CompanionDeviceManagerService service,
            @NonNull AssociationStoreImpl associationStore,
            @NonNull PackageManagerInternal packageManager,
            @NonNull CompanionDevicePresenceMonitor devicePresenceMonitor,
            @NonNull CompanionApplicationController applicationController,
            @NonNull SystemDataTransferRequestStore systemDataTransferRequestStore) {
        mService = service;
        mContext = service.getContext();
        mActivityManager = mContext.getSystemService(ActivityManager.class);
        mAssociationStore = associationStore;
        mPackageManagerInternal = packageManager;
        mOnPackageVisibilityChangeListener =
                new OnPackageVisibilityChangeListener(mActivityManager);
        mDevicePresenceMonitor = devicePresenceMonitor;
        mCompanionAppController = applicationController;
        mSystemDataTransferRequestStore = systemDataTransferRequestStore;
    }

    // TODO: also revoke notification access
    void disassociateInternal(int associationId) {
        final AssociationInfo association = mAssociationStore.getAssociationById(associationId);
        final int userId = association.getUserId();
        final String packageName = association.getPackageName();
        final String deviceProfile = association.getDeviceProfile();

        if (!maybeRemoveRoleHolderForAssociation(association)) {
            // Need to remove the app from list of the role holders, but will have to do it later
            // (the app is in foreground at the moment).
            addToPendingRoleHolderRemoval(association);
        }

        // Need to check if device still present now because CompanionDevicePresenceMonitor will
        // remove current connected device after mAssociationStore.removeAssociation
        final boolean wasPresent = mDevicePresenceMonitor.isDevicePresent(associationId);

        // Removing the association.
        mAssociationStore.removeAssociation(associationId);
        // Do not need to persistUserState since CompanionDeviceManagerService will get callback
        // from #onAssociationChanged, and it will handle the persistUserState which including
        // active and revoked association.
        logRemoveAssociation(deviceProfile);

        // Remove all the system data transfer requests for the association.
        mSystemDataTransferRequestStore.removeRequestsByAssociationId(userId, associationId);

        if (!wasPresent || !association.isNotifyOnDeviceNearby()) return;
        // The device was connected and the app was notified: check if we need to unbind the app
        // now.
        final boolean shouldStayBound = any(
                mAssociationStore.getAssociationsForPackage(userId, packageName),
                it -> it.isNotifyOnDeviceNearby()
                        && mDevicePresenceMonitor.isDevicePresent(it.getId()));
        if (shouldStayBound) return;
        mCompanionAppController.unbindCompanionApplication(userId, packageName);
    }

    /**
     * First, checks if the companion application should be removed from the list role holders when
     * upon association's removal, i.e.: association's profile (matches the role) is not null,
     * the application does not have other associations with the same profile, etc.
     *
     * <p>
     * Then, if establishes that the application indeed has to be removed from the list of the role
     * holders, checks if it could be done right now -
     * {@link android.app.role.RoleManager#removeRoleHolderAsUser(String, String, int, UserHandle, java.util.concurrent.Executor, java.util.function.Consumer) RoleManager#removeRoleHolderAsUser()}
     * will kill the application's process, which leads poor user experience if the application was
     * in foreground when this happened, to avoid this CDMS delays invoking
     * {@code RoleManager.removeRoleHolderAsUser()} until the app is no longer in foreground.
     *
     * @return {@code true} if the application does NOT need be removed from the list of the role
     *         holders OR if the application was successfully removed from the list of role holders.
     *         I.e.: from the role-management perspective the association is done with.
     *         {@code false} if the application needs to be removed from the list of role the role
     *         holders, BUT it CDMS would prefer to do it later.
     *         I.e.: application is in the foreground at the moment, but invoking
     *         {@code RoleManager.removeRoleHolderAsUser()} will kill the application's process,
     *         which would lead to the poor UX, hence need to try later.
     */
    boolean maybeRemoveRoleHolderForAssociation(@NonNull AssociationInfo association) {
        if (DEBUG) Log.d(TAG, "maybeRemoveRoleHolderForAssociation() association=" + association);
        final String deviceProfile = association.getDeviceProfile();

        if (deviceProfile == null) {
            // No role was granted to for this association, there is nothing else we need to here.
            return true;
        }
        // Do not need to remove the system role since it was pre-granted by the system.
        if (deviceProfile.equals(DEVICE_PROFILE_AUTOMOTIVE_PROJECTION)) {
            return true;
        }

        // Check if the applications is associated with another devices with the profile. If so,
        // it should remain the role holder.
        final int id = association.getId();
        final int userId = association.getUserId();
        final String packageName = association.getPackageName();
        final boolean roleStillInUse = any(
                mAssociationStore.getAssociationsForPackage(userId, packageName),
                it -> deviceProfile.equals(it.getDeviceProfile()) && id != it.getId());
        if (roleStillInUse) {
            // Application should remain a role holder, there is nothing else we need to here.
            return true;
        }

        final int packageProcessImportance = getPackageProcessImportance(userId, packageName);
        if (packageProcessImportance <= IMPORTANCE_VISIBLE) {
            // Need to remove the app from the list of role holders, but the process is visible to
            // the user at the moment, so we'll need to it later: log and return false.
            Slog.i(TAG, "Cannot remove role holder for the removed association id=" + id
                    + " now - process is visible.");
            return false;
        }

        removeRoleHolderForAssociation(mContext, association);
        return true;
    }

    @SuppressLint("MissingPermission")
    private int  getPackageProcessImportance(@UserIdInt int userId, @NonNull String packageName) {
        return Binder.withCleanCallingIdentity(() -> {
            final int uid =
                    mPackageManagerInternal.getPackageUid(packageName, /* flags */0, userId);
            return mActivityManager.getUidImportance(uid);
        });
    }

    /**
     * Set revoked flag for active association and add the revoked association and the uid into
     * the caches.
     *
     * @see #mRevokedAssociationsPendingRoleHolderRemoval
     * @see #mUidsPendingRoleHolderRemoval
     * @see OnPackageVisibilityChangeListener
     */
    void addToPendingRoleHolderRemoval(@NonNull AssociationInfo association) {
        // First: set revoked flag
        association = (new AssociationInfo.Builder(association)).setRevoked(true).build();
        final String packageName = association.getPackageName();
        final int userId = association.getUserId();
        final int uid = mPackageManagerInternal.getPackageUid(packageName, /* flags */0, userId);
        // Second: add to the set.
        synchronized (mRevokedAssociationsPendingRoleHolderRemoval) {
            mRevokedAssociationsPendingRoleHolderRemoval.forUser(association.getUserId())
                    .add(association);
            if (!mUidsPendingRoleHolderRemoval.containsKey(uid)) {
                mUidsPendingRoleHolderRemoval.put(uid, packageName);

                if (mUidsPendingRoleHolderRemoval.size() == 1) {
                    // Just added first uid: start the listener
                    mOnPackageVisibilityChangeListener.startListening();
                }
            }
        }
    }

    /**
     * Remove the revoked association from the cache and also remove the uid from the map if
     * there are other associations with the same package still pending for role holder removal.
     *
     * @see #mRevokedAssociationsPendingRoleHolderRemoval
     * @see #mUidsPendingRoleHolderRemoval
     * @see OnPackageVisibilityChangeListener
     */
    private void removeFromPendingRoleHolderRemoval(@NonNull AssociationInfo association) {
        final String packageName = association.getPackageName();
        final int userId = association.getUserId();
        final int uid = mPackageManagerInternal.getPackageUid(packageName, /* flags */  0, userId);

        synchronized (mRevokedAssociationsPendingRoleHolderRemoval) {
            mRevokedAssociationsPendingRoleHolderRemoval.forUser(userId)
                    .remove(association);

            final boolean shouldKeepUidForRemoval = any(
                    getPendingRoleHolderRemovalAssociationsForUser(userId),
                    ai -> packageName.equals(ai.getPackageName()));
            // Do not remove the uid from the map since other associations with
            // the same packageName still pending for role holder removal.
            if (!shouldKeepUidForRemoval) {
                mUidsPendingRoleHolderRemoval.remove(uid);
            }

            if (mUidsPendingRoleHolderRemoval.isEmpty()) {
                // The set is empty now - can "turn off" the listener.
                mOnPackageVisibilityChangeListener.stopListening();
            }
        }
    }

    /**
     * @return a copy of the revoked associations set (safeguarding against
     *         {@code ConcurrentModificationException}-s).
     */
    @NonNull Set<AssociationInfo> getPendingRoleHolderRemovalAssociationsForUser(
            @UserIdInt int userId) {
        synchronized (mRevokedAssociationsPendingRoleHolderRemoval) {
            // Return a copy.
            return new ArraySet<>(mRevokedAssociationsPendingRoleHolderRemoval.forUser(userId));
        }
    }

    private String getPackageNameByUid(int uid) {
        synchronized (mRevokedAssociationsPendingRoleHolderRemoval) {
            return mUidsPendingRoleHolderRemoval.get(uid);
        }
    }

    /**
     * An OnUidImportanceListener class which watches the importance of the packages.
     * In this class, we ONLY interested in the importance of the running process is greater than
     * {@link ActivityManager.RunningAppProcessInfo#IMPORTANCE_VISIBLE} for the uids have been added
     * into the {@link #mUidsPendingRoleHolderRemoval}. Lastly remove the role holder for the
     * revoked associations for the same packages.
     *
     * @see #maybeRemoveRoleHolderForAssociation(AssociationInfo)
     * @see #removeFromPendingRoleHolderRemoval(AssociationInfo)
     * @see #getPendingRoleHolderRemovalAssociationsForUser(int)
     */
    private class OnPackageVisibilityChangeListener implements
            ActivityManager.OnUidImportanceListener {
        final @NonNull ActivityManager mAm;

        OnPackageVisibilityChangeListener(@NonNull ActivityManager am) {
            this.mAm = am;
        }

        @SuppressLint("MissingPermission")
        void startListening() {
            Binder.withCleanCallingIdentity(
                    () -> mAm.addOnUidImportanceListener(
                            /* listener */ OnPackageVisibilityChangeListener.this,
                            ActivityManager.RunningAppProcessInfo.IMPORTANCE_VISIBLE));
        }

        @SuppressLint("MissingPermission")
        void stopListening() {
            Binder.withCleanCallingIdentity(
                    () -> mAm.removeOnUidImportanceListener(
                            /* listener */ OnPackageVisibilityChangeListener.this));
        }

        @Override
        public void onUidImportance(int uid, int importance) {
            if (importance <= ActivityManager.RunningAppProcessInfo.IMPORTANCE_VISIBLE) {
                // The lower the importance value the more "important" the process is.
                // We are only interested when the process ceases to be visible.
                return;
            }

            final String packageName = getPackageNameByUid(uid);
            if (packageName == null) {
                // Not interested in this uid.
                return;
            }

            final int userId = UserHandle.getUserId(uid);

            boolean needToPersistStateForUser = false;

            for (AssociationInfo association :
                    getPendingRoleHolderRemovalAssociationsForUser(userId)) {
                if (!packageName.equals(association.getPackageName())) continue;

                if (!maybeRemoveRoleHolderForAssociation(association)) {
                    // Did not remove the role holder, will have to try again later.
                    continue;
                }

                removeFromPendingRoleHolderRemoval(association);
                needToPersistStateForUser = true;
            }

            if (needToPersistStateForUser) {
                mService.postPersistUserState(userId);
            }
        }
    }
}
+31 −317

File changed.

Preview size limit exceeded, changes collapsed.

+6 −3
Original line number Original line Diff line number Diff line
@@ -45,6 +45,7 @@ class CompanionDeviceShellCommand extends ShellCommand {
    private static final String TAG = "CDM_CompanionDeviceShellCommand";
    private static final String TAG = "CDM_CompanionDeviceShellCommand";


    private final CompanionDeviceManagerService mService;
    private final CompanionDeviceManagerService mService;
    private final AssociationRevokeProcessor mRevokeProcessor;
    private final AssociationStoreImpl mAssociationStore;
    private final AssociationStoreImpl mAssociationStore;
    private final CompanionDevicePresenceMonitor mDevicePresenceMonitor;
    private final CompanionDevicePresenceMonitor mDevicePresenceMonitor;
    private final CompanionTransportManager mTransportManager;
    private final CompanionTransportManager mTransportManager;
@@ -59,7 +60,8 @@ class CompanionDeviceShellCommand extends ShellCommand {
            CompanionTransportManager transportManager,
            CompanionTransportManager transportManager,
            SystemDataTransferProcessor systemDataTransferProcessor,
            SystemDataTransferProcessor systemDataTransferProcessor,
            AssociationRequestsProcessor associationRequestsProcessor,
            AssociationRequestsProcessor associationRequestsProcessor,
            BackupRestoreProcessor backupRestoreProcessor) {
            BackupRestoreProcessor backupRestoreProcessor,
            AssociationRevokeProcessor revokeProcessor) {
        mService = service;
        mService = service;
        mAssociationStore = associationStore;
        mAssociationStore = associationStore;
        mDevicePresenceMonitor = devicePresenceMonitor;
        mDevicePresenceMonitor = devicePresenceMonitor;
@@ -67,6 +69,7 @@ class CompanionDeviceShellCommand extends ShellCommand {
        mSystemDataTransferProcessor = systemDataTransferProcessor;
        mSystemDataTransferProcessor = systemDataTransferProcessor;
        mAssociationRequestsProcessor = associationRequestsProcessor;
        mAssociationRequestsProcessor = associationRequestsProcessor;
        mBackupRestoreProcessor = backupRestoreProcessor;
        mBackupRestoreProcessor = backupRestoreProcessor;
        mRevokeProcessor = revokeProcessor;
    }
    }


    @Override
    @Override
@@ -126,7 +129,7 @@ class CompanionDeviceShellCommand extends ShellCommand {
                    final AssociationInfo association =
                    final AssociationInfo association =
                            mService.getAssociationWithCallerChecks(userId, packageName, address);
                            mService.getAssociationWithCallerChecks(userId, packageName, address);
                    if (association != null) {
                    if (association != null) {
                        mService.disassociateInternal(association.getId());
                        mRevokeProcessor.disassociateInternal(association.getId());
                    }
                    }
                }
                }
                break;
                break;
@@ -138,7 +141,7 @@ class CompanionDeviceShellCommand extends ShellCommand {
                            mAssociationStore.getAssociationsForPackage(userId, packageName);
                            mAssociationStore.getAssociationsForPackage(userId, packageName);
                    for (AssociationInfo association : userAssociations) {
                    for (AssociationInfo association : userAssociations) {
                        if (sanitizeWithCallerChecks(mService.getContext(), association) != null) {
                        if (sanitizeWithCallerChecks(mService.getContext(), association) != null) {
                            mService.disassociateInternal(association.getId());
                            mRevokeProcessor.disassociateInternal(association.getId());
                        }
                        }
                    }
                    }
                }
                }