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

Commit 10c9fa77 authored by Sergey Nikolaienkov's avatar Sergey Nikolaienkov
Browse files

Introduce CDM AssociationStore

Add public (within system_server) AssociationStore interface that
provides APIs for retreiving and monitoring changes to the CDM
associations.
Add AssociationStoreImpl - implementation of the AssociationStore which
additionally adds adds methods for adding, removing and updating
associations to be used by CdmService.

Bug: 211398735
Test: atest CtsCompanionDeviceManagerCoreTestCases
Test: atest CtsCompanionDeviceManagerUiAutomationTestCases
Change-Id: I82bc948c9949c3034b539530ef9641baf220601c
parent ca48c387
Loading
Loading
Loading
Loading
+14 −0
Original line number Diff line number Diff line
@@ -197,6 +197,20 @@ public final class AssociationInfo implements Parcelable {
        return macAddress.equals(mDeviceMacAddress);
    }

    /** @hide */
    public @NonNull String toShortString() {
        final StringBuilder sb = new StringBuilder();
        sb.append("id=").append(mId);
        if (mDeviceMacAddress != null) {
            sb.append(", addr=").append(getDeviceMacAddressAsString());
        }
        if (mSelfManaged) {
            sb.append(", self-managed");
        }
        sb.append(", pkg=u").append(mUserId).append('/').append(mPackageName);
        return sb.toString();
    }

    @Override
    public String toString() {
        return "Association{"
+25 −16
Original line number Diff line number Diff line
@@ -22,7 +22,6 @@ import static android.app.PendingIntent.FLAG_ONE_SHOT;
import static android.companion.CompanionDeviceManager.COMPANION_DEVICE_DISCOVERY_PACKAGE_NAME;
import static android.content.ComponentName.createRelative;

import static com.android.internal.util.CollectionUtils.filter;
import static com.android.server.companion.CompanionDeviceManagerService.DEBUG;
import static com.android.server.companion.CompanionDeviceManagerService.LOG_TAG;
import static com.android.server.companion.PermissionsUtils.enforcePermissionsForAssociation;
@@ -57,6 +56,7 @@ import com.android.internal.util.ArrayUtils;

import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

/**
@@ -124,14 +124,17 @@ class AssociationRequestsProcessor {
    private static final int ASSOCIATE_WITHOUT_PROMPT_MAX_PER_TIME_WINDOW = 5;
    private static final long ASSOCIATE_WITHOUT_PROMPT_WINDOW_MS = 60 * 60 * 1000; // 60 min;

    private final Context mContext;
    private final CompanionDeviceManagerService mService;
    private final PackageManagerInternal mPackageManager;
    private final @NonNull Context mContext;
    private final @NonNull CompanionDeviceManagerService mService;
    private final @NonNull PackageManagerInternal mPackageManager;
    private final @NonNull AssociationStore mAssociationStore;

    AssociationRequestsProcessor(CompanionDeviceManagerService service) {
    AssociationRequestsProcessor(@NonNull CompanionDeviceManagerService service,
            @NonNull AssociationStore associationStore) {
        mContext = service.getContext();
        mService = service;
        mPackageManager = service.mPackageManagerInternal;
        mAssociationStore = associationStore;
    }

    /**
@@ -330,18 +333,24 @@ class AssociationRequestsProcessor {
        }

        // Throttle frequent associations
        long now = System.currentTimeMillis();
        Set<AssociationInfo> recentAssociations = filter(
                mService.getAssociations(userId, packageName),
                a -> now - a.getTimeApprovedMs() < ASSOCIATE_WITHOUT_PROMPT_WINDOW_MS);

        if (recentAssociations.size() >= ASSOCIATE_WITHOUT_PROMPT_MAX_PER_TIME_WINDOW) {
            Slog.w(TAG, "Too many associations. " + packageName
                    + " already associated " + recentAssociations.size()
                    + " devices within the last " + ASSOCIATE_WITHOUT_PROMPT_WINDOW_MS
                    + "ms: " + recentAssociations);
        final long now = System.currentTimeMillis();
        final List<AssociationInfo> associationForPackage =
                mAssociationStore.getAssociationsForPackage(userId, packageName);
        // Number of "recent" associations.
        int recent = 0;
        for (AssociationInfo association : associationForPackage) {
            final boolean isRecent =
                    now - association.getTimeApprovedMs() < ASSOCIATE_WITHOUT_PROMPT_WINDOW_MS;
            if (isRecent) {
                if (++recent >= ASSOCIATE_WITHOUT_PROMPT_MAX_PER_TIME_WINDOW) {
                    Slog.w(TAG, "Too many associations: " + packageName + " already "
                            + "associated " + recent + " devices within the last "
                            + ASSOCIATE_WITHOUT_PROMPT_WINDOW_MS + "ms");
                    return false;
                }
            }
        }

        String[] sameOemCerts = mContext.getResources()
                .getStringArray(com.android.internal.R.array.config_companionDeviceCerts);

+126 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2021 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 android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.UserIdInt;
import android.companion.AssociationInfo;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.Collection;
import java.util.List;

/**
 * Interface for a store of {@link AssociationInfo}-s.
 */
public interface AssociationStore {

    @IntDef(prefix = { "CHANGE_TYPE_" }, value = {
            CHANGE_TYPE_ADDED,
            CHANGE_TYPE_REMOVED,
            CHANGE_TYPE_UPDATED_ADDRESS_CHANGED,
            CHANGE_TYPE_UPDATED_ADDRESS_UNCHANGED,
    })
    @Retention(RetentionPolicy.SOURCE)
    @interface ChangeType {}

    int CHANGE_TYPE_ADDED = 0;
    int CHANGE_TYPE_REMOVED = 1;
    int CHANGE_TYPE_UPDATED_ADDRESS_CHANGED = 2;
    int CHANGE_TYPE_UPDATED_ADDRESS_UNCHANGED = 3;

    /**  Listener for any changes to {@link AssociationInfo}-s. */
    interface OnChangeListener {
        default void onAssociationChanged(
                @ChangeType int changeType, AssociationInfo association) {}

        default void onAssociationAdded(AssociationInfo association) {}

        default void onAssociationRemoved(AssociationInfo association) {}

        default void onAssociationUpdated(AssociationInfo association, boolean addressChanged) {}
    }

    /**
     * @return all CDM associations.
     */
    @NonNull
    Collection<AssociationInfo> getAssociations();

    /**
     * @return a {@link List} of associations that belong to the user.
     */
    @NonNull
    List<AssociationInfo> getAssociationsForUser(@UserIdInt int userId);

    /**
     * @return a {@link List} of association that belong to the package.
     */
    @NonNull
    List<AssociationInfo> getAssociationsForPackage(
            @UserIdInt int userId, @NonNull String packageName);

    /**
     * @return an association with the given address that belong to the given package if such an
     * association exists, otherwise {@code null}.
     */
    @Nullable
    AssociationInfo getAssociationsForPackageWithAddress(
            @UserIdInt int userId, @NonNull String packageName, @NonNull String macAddress);

    /**
     * @return an association with the given id if such an association exists, otherwise
     * {@code null}.
     */
    @Nullable
    AssociationInfo getAssociationById(int id);

    /**
     * @return all associations with the given MAc address.
     */
    @NonNull
    List<AssociationInfo> getAssociationsByAddress(@NonNull String macAddress);

    /** Register a {@link OnChangeListener} */
    void registerListener(@NonNull OnChangeListener listener);

    /** Un-register a previously registered {@link OnChangeListener} */
    void unregisterListener(@NonNull OnChangeListener listener);

    /** @hide */
    static String changeTypeToString(@ChangeType int changeType) {
        switch (changeType) {
            case CHANGE_TYPE_ADDED:
                return "ASSOCIATION_ADDED";

            case CHANGE_TYPE_REMOVED:
                return "ASSOCIATION_REMOVED";

            case CHANGE_TYPE_UPDATED_ADDRESS_CHANGED:
                return "ASSOCIATION_UPDATED";

            case CHANGE_TYPE_UPDATED_ADDRESS_UNCHANGED:
                return "ASSOCIATION_UPDATED_ADDRESS_UNCHANGED";

            default:
                return "Unknown (" + changeType + ")";
        }
    }
}
+302 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2021 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 android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.UserIdInt;
import android.companion.AssociationInfo;
import android.net.MacAddress;
import android.util.Log;
import android.util.SparseArray;

import com.android.internal.annotations.GuardedBy;
import com.android.internal.util.CollectionUtils;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;

/**
 * Implementation of the {@link AssociationStore}, with addition of the methods for modification.
 * <ul>
 * <li> {@link #addAssociation(AssociationInfo)}
 * <li> {@link #removeAssociation(int)}
 * <li> {@link #updateAssociation(AssociationInfo)}
 * </ul>
 *
 * The class has package-private access level, and instances of the class should only be created by
 * the {@link CompanionDeviceManagerService}.
 * Other system component (both inside and outside if the com.android.server.companion package)
 * should use public {@link AssociationStore} interface.
 */
class AssociationStoreImpl implements AssociationStore {
    private static final boolean DEBUG = false;
    private static final String TAG = "AssociationStore";

    private final Object mLock = new Object();

    @GuardedBy("mLock")
    private final Map<Integer, AssociationInfo> mIdMap;
    @GuardedBy("mLock")
    private final Map<MacAddress, Set<Integer>> mAddressMap;
    @GuardedBy("mLock")
    private final SparseArray<List<AssociationInfo>> mCachedPerUser = new SparseArray<>();

    @GuardedBy("mListeners")
    private final Set<OnChangeListener> mListeners = new LinkedHashSet<>();

    AssociationStoreImpl(Collection<AssociationInfo> associations) {
        synchronized (mLock) {
            final int size = associations.size();
            mIdMap = new HashMap<>(size);
            mAddressMap = new HashMap<>(size);

            for (AssociationInfo association : associations) {
                final int id = association.getId();
                mIdMap.put(id, association);

                final MacAddress address = association.getDeviceMacAddress();
                if (address != null) {
                    mAddressMap.computeIfAbsent(address, it -> new HashSet<>()).add(id);
                }
            }
        }
    }

    void addAssociation(@NonNull AssociationInfo association) {
        final int id = association.getId();

        if (DEBUG) {
            Log.i(TAG, "addAssociation() " + association.toShortString());
            Log.d(TAG, "  association=" + association);
        }

        synchronized (mLock) {
            if (mIdMap.containsKey(id)) {
                if (DEBUG) Log.w(TAG, "Association already stored.");
                return;
            }
            mIdMap.put(id, association);

            final MacAddress address = association.getDeviceMacAddress();
            if (address != null) {
                mAddressMap.computeIfAbsent(address, it -> new HashSet<>()).add(id);
            }

            invalidateCacheForUserLocked(association.getUserId());
        }

        broadcastChange(CHANGE_TYPE_ADDED, association);
    }

    void updateAssociation(@NonNull AssociationInfo updated) {
        final int id = updated.getId();

        if (DEBUG) {
            Log.i(TAG, "updateAssociation() " + updated.toShortString());
            Log.d(TAG, "  updated=" + updated);
        }

        final AssociationInfo current;
        final boolean macAddressChanged;
        synchronized (mLock) {
            current = mIdMap.get(id);
            if (current == null) {
                if (DEBUG) Log.w(TAG, "Association with id " + id + " does not exist.");
                return;
            }
            if (DEBUG) Log.d(TAG, "  current=" + current);

            if (current.equals(updated)) {
                if (DEBUG) Log.w(TAG, "  No changes.");
                return;
            }

            // Update the ID-to-Association map.
            mIdMap.put(id, updated);

            // Update the MacAddress-to-List<Association> map if needed.
            final MacAddress updatedAddress = updated.getDeviceMacAddress();
            final MacAddress currentAddress = current.getDeviceMacAddress();
            macAddressChanged = Objects.equals(
                    current.getDeviceMacAddress(), updated.getDeviceMacAddress());
            if (macAddressChanged) {
                if (currentAddress != null) {
                    mAddressMap.get(currentAddress).remove(id);
                }
                if (updatedAddress != null) {
                    mAddressMap.computeIfAbsent(updatedAddress, it -> new HashSet<>()).add(id);
                }
            }
        }

        final int changeType = macAddressChanged ? CHANGE_TYPE_UPDATED_ADDRESS_CHANGED
                : CHANGE_TYPE_UPDATED_ADDRESS_UNCHANGED;
        broadcastChange(changeType, updated);
    }

    void removeAssociation(int id) {
        if (DEBUG) Log.i(TAG, "removeAssociation() id=" + id);

        final AssociationInfo association;
        synchronized (mLock) {
            association = mIdMap.remove(id);

            if (association == null) {
                if (DEBUG) Log.w(TAG, "Association with id " + id + " is not stored.");
                return;
            } else {
                if (DEBUG) {
                    Log.i(TAG, "removed " + association.toShortString());
                    Log.d(TAG, "  association=" + association);
                }
            }

            final MacAddress macAddress = association.getDeviceMacAddress();
            if (macAddress != null) {
                mAddressMap.get(macAddress).remove(id);
            }

            invalidateCacheForUserLocked(association.getUserId());
        }

        broadcastChange(CHANGE_TYPE_REMOVED, association);
    }

    public @NonNull Collection<AssociationInfo> getAssociations() {
        final Collection<AssociationInfo> allAssociations;
        synchronized (mLock) {
            allAssociations = mIdMap.values();
        }
        return Collections.unmodifiableCollection(allAssociations);
    }

    public @NonNull List<AssociationInfo> getAssociationsForUser(@UserIdInt int userId) {
        synchronized (mLock) {
            return getAssociationsForUserLocked(userId);
        }
    }

    public @NonNull List<AssociationInfo> getAssociationsForPackage(
            @UserIdInt int userId, @NonNull String packageName) {
        final List<AssociationInfo> associationsForUser = getAssociationsForUser(userId);
        final List<AssociationInfo> associationsForPackage =
                CollectionUtils.filter(associationsForUser,
                        it -> it.getPackageName().equals(packageName));
        return Collections.unmodifiableList(associationsForPackage);
    }

    public @Nullable AssociationInfo getAssociationsForPackageWithAddress(
            @UserIdInt int userId, @NonNull String packageName, @NonNull String macAddress) {
        final List<AssociationInfo> associations = getAssociationsByAddress(macAddress);
        return CollectionUtils.find(associations,
                it -> it.belongsToPackage(userId, packageName));
    }

    public @Nullable AssociationInfo getAssociationById(int id) {
        synchronized (mLock) {
            return mIdMap.get(id);
        }
    }

    public @NonNull List<AssociationInfo> getAssociationsByAddress(@NonNull String macAddress) {
        final MacAddress address = MacAddress.fromString(macAddress);

        synchronized (mLock) {
            final Set<Integer> ids = mAddressMap.get(address);
            if (ids == null) return Collections.emptyList();

            final List<AssociationInfo> associations = new ArrayList<>();
            for (AssociationInfo association : mIdMap.values()) {
                if (address.equals(association.getDeviceMacAddress())) {
                    associations.add(association);
                }
            }

            return Collections.unmodifiableList(associations);
        }
    }

    @GuardedBy("mLock")
    private @NonNull List<AssociationInfo> getAssociationsForUserLocked(@UserIdInt int userId) {
        final List<AssociationInfo> cached = mCachedPerUser.get(userId);
        if (cached != null) {
            return cached;
        }

        final List<AssociationInfo> associationsForUser = new ArrayList<>();
        for (AssociationInfo association : mIdMap.values()) {
            if (association.getUserId() == userId) {
                associationsForUser.add(association);
            }
        }
        final List<AssociationInfo> set = Collections.unmodifiableList(associationsForUser);
        mCachedPerUser.set(userId, set);
        return set;
    }

    @GuardedBy("mLock")
    private void invalidateCacheForUserLocked(@UserIdInt int userId) {
        mCachedPerUser.delete(userId);
    }

    public void registerListener(@NonNull OnChangeListener listener) {
        synchronized (mListeners) {
            mListeners.add(listener);
        }
    }

    public void unregisterListener(@NonNull OnChangeListener listener) {
        synchronized (mListeners) {
            mListeners.remove(listener);
        }
    }

    private void broadcastChange(@ChangeType int changeType, AssociationInfo association) {
        synchronized (mListeners) {
            for (OnChangeListener listener : mListeners) {
                listener.onAssociationChanged(changeType, association);

                switch (changeType) {
                    case CHANGE_TYPE_ADDED:
                        listener.onAssociationAdded(association);
                        break;

                    case CHANGE_TYPE_REMOVED:
                        listener.onAssociationRemoved(association);
                        break;

                    case CHANGE_TYPE_UPDATED_ADDRESS_CHANGED:
                        listener.onAssociationUpdated(association, true);
                        break;

                    case CHANGE_TYPE_UPDATED_ADDRESS_UNCHANGED:
                        listener.onAssociationUpdated(association, false);
                        break;
                }
            }
        }
    }
}
+225 −284

File changed.

Preview size limit exceeded, changes collapsed.

Loading