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
This commit is contained in:
Sergey Nikolaienkov
2021-12-28 08:59:02 +01:00
parent ca48c3874f
commit 10c9fa7771
7 changed files with 750 additions and 324 deletions

View File

@@ -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{"

View File

@@ -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);
return false;
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);

View File

@@ -0,0 +1,126 @@
/*
* 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 + ")";
}
}
}

View File

@@ -0,0 +1,302 @@
/*
* 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;
}
}
}
}
}

View File

@@ -27,15 +27,12 @@ import static android.os.Binder.getCallingUid;
import static android.os.Process.SYSTEM_UID;
import static android.os.UserHandle.getCallingUserId;
import static com.android.internal.util.CollectionUtils.add;
import static com.android.internal.util.CollectionUtils.any;
import static com.android.internal.util.CollectionUtils.filter;
import static com.android.internal.util.CollectionUtils.find;
import static com.android.internal.util.CollectionUtils.forEach;
import static com.android.internal.util.CollectionUtils.map;
import static com.android.internal.util.Preconditions.checkState;
import static com.android.internal.util.function.pooled.PooledLambda.obtainMessage;
import static com.android.internal.util.function.pooled.PooledLambda.obtainRunnable;
import static com.android.server.companion.AssociationStore.CHANGE_TYPE_UPDATED_ADDRESS_UNCHANGED;
import static com.android.server.companion.PermissionsUtils.checkCallerCanManageAssociationsForPackage;
import static com.android.server.companion.PermissionsUtils.checkCallerCanManageCompanionDevice;
import static com.android.server.companion.PermissionsUtils.enforceCallerCanInteractWithUserId;
@@ -45,8 +42,6 @@ import static com.android.server.companion.PermissionsUtils.enforceCallerIsSyste
import static com.android.server.companion.RolesUtils.addRoleHolderForAssociation;
import static com.android.server.companion.RolesUtils.removeRoleHolderForAssociation;
import static java.util.Collections.emptySet;
import static java.util.Collections.unmodifiableSet;
import static java.util.Objects.requireNonNull;
import static java.util.concurrent.TimeUnit.MINUTES;
@@ -82,7 +77,6 @@ import android.content.pm.PackageInfo;
import android.content.pm.PackageItemInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManagerInternal;
import android.content.pm.UserInfo;
import android.net.MacAddress;
import android.net.NetworkPolicyManager;
import android.os.Binder;
@@ -112,9 +106,7 @@ 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.CollectionUtils;
import com.android.internal.util.DumpUtils;
import com.android.internal.util.function.pooled.PooledLambda;
import com.android.server.FgThread;
import com.android.server.LocalServices;
import com.android.server.SystemService;
@@ -127,6 +119,7 @@ import java.io.PrintWriter;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
@@ -135,12 +128,11 @@ import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.TimeZone;
import java.util.function.Function;
import java.util.function.Predicate;
/** @hide */
@SuppressLint("LongLogTag")
public class CompanionDeviceManagerService extends SystemService {
public class CompanionDeviceManagerService extends SystemService
implements AssociationStore.OnChangeListener {
static final String LOG_TAG = "CompanionDeviceManagerService";
static final boolean DEBUG = false;
@@ -162,10 +154,11 @@ public class CompanionDeviceManagerService extends SystemService {
sDateFormat.setTimeZone(TimeZone.getDefault());
}
private final CompanionDeviceManagerImpl mImpl;
// Persistent data store for all Associations.
private final PersistentDataStore mPersistentDataStore;
private final AssociationRequestsProcessor mAssociationRequestsProcessor;
private PersistentDataStore mPersistentStore;
private AssociationStoreImpl mAssociationStore;
private AssociationRequestsProcessor mAssociationRequestsProcessor;
private PowerWhitelistManager mPowerWhitelistManager;
private IAppOpsService mAppOpsManager;
private BluetoothAdapter mBluetoothAdapter;
@@ -187,20 +180,16 @@ public class CompanionDeviceManagerService extends SystemService {
private final RemoteCallbackList<IOnAssociationsChangedListener> mListeners =
new RemoteCallbackList<>();
final Object mLock = new Object();
final Handler mMainHandler = Handler.getMain();
private CompanionDevicePresenceController mCompanionDevicePresenceController;
/** Maps a {@link UserIdInt} to a set of associations for the user. */
@GuardedBy("mLock")
private final SparseArray<Set<AssociationInfo>> mCachedAssociations = new SparseArray<>();
/**
* A structure that consist of two nested maps, and effectively maps (userId + packageName) to
* a list of IDs that have been previously assigned to associations for that package.
* We maintain this structure so that we never re-use association IDs for the same package
* (until it's uninstalled).
*/
@GuardedBy("mLock")
@GuardedBy("mPreviouslyUsedIds")
private final SparseArray<Map<String, Set<Integer>>> mPreviouslyUsedIds = new SparseArray<>();
ActivityTaskManagerInternal mAtmInternal;
@@ -209,8 +198,6 @@ public class CompanionDeviceManagerService extends SystemService {
public CompanionDeviceManagerService(Context context) {
super(context);
mImpl = new CompanionDeviceManagerImpl();
mPersistentDataStore = new PersistentDataStore();
mPowerWhitelistManager = context.getSystemService(PowerWhitelistManager.class);
mAppOpsManager = IAppOpsService.Stub.asInterface(
@@ -221,49 +208,36 @@ public class CompanionDeviceManagerService extends SystemService {
mPermissionControllerManager = requireNonNull(
context.getSystemService(PermissionControllerManager.class));
mUserManager = context.getSystemService(UserManager.class);
mCompanionDevicePresenceController = new CompanionDevicePresenceController(this);
mAssociationRequestsProcessor = new AssociationRequestsProcessor(this);
registerPackageMonitor();
}
private void registerPackageMonitor() {
new PackageMonitor() {
@Override
public void onPackageRemoved(String packageName, int uid) {
final int userId = getChangingUserId();
Slog.i(LOG_TAG, "onPackageRemoved() u" + userId + "/" + packageName);
clearAssociationForPackage(userId, packageName);
}
@Override
public void onPackageDataCleared(String packageName, int uid) {
final int userId = getChangingUserId();
Slog.i(LOG_TAG, "onPackageDataCleared() u" + userId + "/" + packageName);
clearAssociationForPackage(userId, packageName);
}
@Override
public void onPackageModified(String packageName) {
final int userId = getChangingUserId();
Slog.i(LOG_TAG, "onPackageModified() u" + userId + "/" + packageName);
forEach(getAssociations(userId, packageName), association ->
updateSpecialAccessPermissionForAssociatedPackage(association));
}
}.register(getContext(), FgThread.get().getLooper(), UserHandle.ALL, true);
}
@Override
public void onStart() {
publishBinderService(Context.COMPANION_DEVICE_SERVICE, mImpl);
mPersistentStore = new PersistentDataStore();
final Set<AssociationInfo> allAssociations = new ArraySet<>();
synchronized (mPreviouslyUsedIds) {
// The data is stored in DE directories, so we can read the data for all users now
// (which would not be possible if the data was stored to CE directories).
mPersistentStore.readStateForUsers(
mUserManager.getAliveUsers(), allAssociations, mPreviouslyUsedIds);
}
mAssociationStore = new AssociationStoreImpl(allAssociations);
mAssociationStore.registerListener(this);
mCompanionDevicePresenceController = new CompanionDevicePresenceController(this);
mAssociationRequestsProcessor = new AssociationRequestsProcessor(this, mAssociationStore);
// Publish "binder service"
final CompanionDeviceManagerImpl impl = new CompanionDeviceManagerImpl();
publishBinderService(Context.COMPANION_DEVICE_SERVICE, impl);
}
@Override
public void onBootPhase(int phase) {
if (phase == SystemService.PHASE_SYSTEM_SERVICES_READY) {
registerPackageMonitor();
// Init Bluetooth
mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
if (mBluetoothAdapter != null) {
@@ -282,7 +256,7 @@ public class CompanionDeviceManagerService extends SystemService {
@Override
public void onUserUnlocking(@NonNull TargetUser user) {
final int userId = user.getUserIdentifier();
final Set<AssociationInfo> associations = getAllAssociationsForUser(userId);
final List<AssociationInfo> associations = mAssociationStore.getAssociationsForUser(userId);
if (associations.isEmpty()) return;
@@ -293,42 +267,18 @@ public class CompanionDeviceManagerService extends SystemService {
MINUTES.toMillis(10));
}
@NonNull
Set<AssociationInfo> getAllAssociationsForUser(@UserIdInt int userId) {
synchronized (mLock) {
readPersistedStateForUserIfNeededLocked(userId);
// This returns non-null, because the readAssociationsInfoForUserIfNeededLocked() method
// we just called adds an empty set, if there was no previously saved data.
return mCachedAssociations.get(userId);
}
}
@NonNull
Set<AssociationInfo> getAssociations(@UserIdInt int userId, @NonNull String packageName) {
return filter(getAllAssociationsForUser(userId),
a -> a.belongsToPackage(userId, packageName));
}
@Nullable
private AssociationInfo getAssociation(int associationId) {
return find(getAllAssociations(), association -> association.getId() == associationId);
}
@Nullable
AssociationInfo getAssociation(
@UserIdInt int userId, @NonNull String packageName, @NonNull String macAddress) {
return find(getAssociations(userId, packageName), a -> a.isLinkedTo(macAddress));
}
@Nullable
AssociationInfo getAssociationWithCallerChecks(
@UserIdInt int userId, @NonNull String packageName, @NonNull String macAddress) {
return sanitizeWithCallerChecks(getAssociation(userId, packageName, macAddress));
final AssociationInfo association = mAssociationStore.getAssociationsForPackageWithAddress(
userId, packageName, macAddress);
return sanitizeWithCallerChecks(association);
}
@Nullable
AssociationInfo getAssociationWithCallerChecks(int associationId) {
return sanitizeWithCallerChecks(getAssociation(associationId));
final AssociationInfo association = mAssociationStore.getAssociationById(associationId);
return sanitizeWithCallerChecks(association);
}
@Nullable
@@ -344,19 +294,6 @@ public class CompanionDeviceManagerService extends SystemService {
return association;
}
private Set<AssociationInfo> getAllAssociations() {
final long identity = Binder.clearCallingIdentity();
try {
final Set<AssociationInfo> result = new ArraySet<>();
for (UserInfo user : mUserManager.getAliveUsers()) {
result.addAll(getAllAssociationsForUser(user.id));
}
return result;
} finally {
Binder.restoreCallingIdentity(identity);
}
}
void maybeGrantAutoRevokeExemptions() {
Slog.d(LOG_TAG, "maybeGrantAutoRevokeExemptions()");
PackageManager pm = getContext().getPackageManager();
@@ -369,10 +306,8 @@ public class CompanionDeviceManagerService extends SystemService {
}
try {
Set<AssociationInfo> associations = getAllAssociationsForUser(userId);
if (associations == null) {
continue;
}
final List<AssociationInfo> associations =
mAssociationStore.getAssociationsForUser(userId);
for (AssociationInfo a : associations) {
try {
int uid = pm.getPackageUidAsUser(a.getPackageName(), userId);
@@ -387,6 +322,61 @@ public class CompanionDeviceManagerService extends SystemService {
}
}
@Override
public void onAssociationChanged(
@AssociationStore.ChangeType int changeType, AssociationInfo association) {
final int id = association.getId();
final int userId = association.getUserId();
final String packageName = association.getPackageName();
if (changeType == AssociationStore.CHANGE_TYPE_REMOVED) {
markIdAsPreviouslyUsedForPackage(id, userId, packageName);
}
final List<AssociationInfo> updatedAssociations =
mAssociationStore.getAssociationsForUser(userId);
final Map<String, Set<Integer>> usedIdsForUser = getPreviouslyUsedIdsForUser(userId);
BackgroundThread.getHandler().post(() ->
mPersistentStore.persistStateForUser(userId, updatedAssociations, usedIdsForUser));
// Notify listeners if ADDED, REMOVED or UPDATED_ADDRESS_CHANGED.
// Do NOT notify when UPDATED_ADDRESS_UNCHANGED, which means a minor tweak in association's
// configs, which "listeners" won't (and shouldn't) be able to see.
if (changeType != CHANGE_TYPE_UPDATED_ADDRESS_UNCHANGED) {
notifyListeners(userId, updatedAssociations);
}
updateAtm(userId, updatedAssociations);
restartBleScan();
}
private void notifyListeners(
@UserIdInt int userId, @NonNull List<AssociationInfo> associations) {
mListeners.broadcast((listener, callbackUserId) -> {
if ((int) callbackUserId == userId) {
try {
listener.onAssociationsChanged(associations);
} catch (RemoteException ignored) {
}
}
});
}
private void markIdAsPreviouslyUsedForPackage(
int associationId, @UserIdInt int userId, @NonNull String packageName) {
synchronized (mPreviouslyUsedIds) {
Map<String, Set<Integer>> usedIdsForUser = mPreviouslyUsedIds.get(userId);
if (usedIdsForUser == null) {
usedIdsForUser = new HashMap<>();
mPreviouslyUsedIds.put(userId, usedIdsForUser);
}
final Set<Integer> usedIdsForPackage =
usedIdsForUser.computeIfAbsent(packageName, it -> new HashSet<>());
usedIdsForPackage.add(associationId);
}
}
class CompanionDeviceManagerImpl extends ICompanionDeviceManager.Stub {
@Override
@@ -424,8 +414,7 @@ public class CompanionDeviceManagerService extends SystemService {
checkUsesFeature(packageName, getCallingUserId());
}
return new ArrayList<>(
CompanionDeviceManagerService.this.getAssociations(userId, packageName));
return mAssociationStore.getAssociationsForPackage(userId, packageName);
}
@Override
@@ -433,8 +422,7 @@ public class CompanionDeviceManagerService extends SystemService {
enforceCallerCanInteractWithUserId(getContext(), userId);
enforceCallerCanManageCompanionDevice(getContext(), "getAllAssociationsForUser");
return new ArrayList<>(
CompanionDeviceManagerService.this.getAllAssociationsForUser(userId));
return mAssociationStore.getAssociationsForUser(userId);
}
@Override
@@ -470,7 +458,7 @@ public class CompanionDeviceManagerService extends SystemService {
+ "(ie. it belongs to a different package or a different user).");
}
disassociateInternal(userId, association.getId());
disassociateInternal(association.getId());
}
@Override
@@ -483,7 +471,7 @@ public class CompanionDeviceManagerService extends SystemService {
+ "or belongs to a different user");
}
disassociateInternal(association.getUserId(), associationId);
disassociateInternal(associationId);
}
@Override
@@ -540,7 +528,7 @@ public class CompanionDeviceManagerService extends SystemService {
return true;
}
return any(CompanionDeviceManagerService.this.getAssociations(userId, packageName),
return any(mAssociationStore.getAssociationsForPackage(userId, packageName),
a -> a.isLinkedTo(macAddress));
}
@@ -623,25 +611,18 @@ public class CompanionDeviceManagerService extends SystemService {
final int userId = getCallingUserId();
enforceCallerIsSystemOr(userId, packageName);
Set<AssociationInfo> deviceAssociations = filter(
CompanionDeviceManagerService.this.getAssociations(userId, packageName),
a -> a.isLinkedTo(deviceAddress));
final AssociationInfo association =
mAssociationStore.getAssociationsForPackageWithAddress(
userId, packageName, deviceAddress);
if (deviceAssociations.isEmpty()) {
if (association == null) {
throw new RemoteException(new DeviceNotAssociatedException("App " + packageName
+ " is not associated with device " + deviceAddress
+ " for user " + userId));
}
updateAssociations(associations -> map(associations, association -> {
if (association.belongsToPackage(userId, packageName)
&& association.isLinkedTo(deviceAddress)) {
association.setNotifyOnDeviceNearby(active);
}
return association;
}), userId);
restartBleScan();
association.setNotifyOnDeviceNearby(active);
mAssociationStore.updateAssociation(association);
}
@Override
@@ -664,14 +645,16 @@ public class CompanionDeviceManagerService extends SystemService {
enforceCallerIsSystemOr(userId, callingPackage);
checkState(!ArrayUtils.isEmpty(
CompanionDeviceManagerService.this.getAssociations(userId, callingPackage)),
mAssociationStore.getAssociationsForPackage(userId, callingPackage)),
"App must have an association before calling this API");
checkUsesFeature(callingPackage, userId);
}
@Override
public boolean canPairWithoutPrompt(String packageName, String macAddress, int userId) {
final AssociationInfo association = getAssociation(userId, packageName, macAddress);
final AssociationInfo association =
mAssociationStore.getAssociationsForPackageWithAddress(
userId, packageName, macAddress);
if (association == null) {
return false;
}
@@ -684,7 +667,8 @@ public class CompanionDeviceManagerService extends SystemService {
String[] args, ShellCallback callback, ResultReceiver resultReceiver)
throws RemoteException {
enforceCallerCanManageCompanionDevice(getContext(), "onShellCommand");
new CompanionDeviceShellCommand(CompanionDeviceManagerService.this)
new CompanionDeviceShellCommand(
CompanionDeviceManagerService.this, mAssociationStore)
.exec(this, in, out, err, args, callback, resultReceiver);
}
@@ -697,13 +681,8 @@ public class CompanionDeviceManagerService extends SystemService {
}
fout.append("Companion Device Associations:").append('\n');
synchronized (mLock) {
for (UserInfo user : getAllUsers()) {
forEach(mCachedAssociations.get(user.id), a -> {
fout.append(" ").append(a.toString()).append('\n');
});
}
for (AssociationInfo a : mAssociationStore.getAssociations()) {
fout.append(" ").append(a.toString()).append('\n');
}
fout.append("Currently Connected Devices:").append('\n');
@@ -754,31 +733,56 @@ public class CompanionDeviceManagerService extends SystemService {
@Nullable String deviceProfile, boolean selfManaged) {
final int id = getNewAssociationIdForPackage(userId, packageName);
final long timestamp = System.currentTimeMillis();
final AssociationInfo association = new AssociationInfo(id, userId, packageName,
macAddress, displayName, deviceProfile, selfManaged, false, timestamp);
Slog.i(LOG_TAG, "New CDM association created=" + association);
mAssociationStore.addAssociation(association);
updateSpecialAccessPermissionForAssociatedPackage(association);
recordAssociation(association, userId);
return association;
}
@GuardedBy("mLock")
@NonNull
private Map<String, Set<Integer>> getPreviouslyUsedIdsForUser(@UserIdInt int userId) {
synchronized (mPreviouslyUsedIds) {
return getPreviouslyUsedIdsForUserLocked(userId);
}
}
@GuardedBy("mPreviouslyUsedIds")
@NonNull
private Map<String, Set<Integer>> getPreviouslyUsedIdsForUserLocked(@UserIdInt int userId) {
final Map<String, Set<Integer>> usedIdsForUser = mPreviouslyUsedIds.get(userId);
if (usedIdsForUser == null) {
return Collections.emptyMap();
}
return deepUnmodifiableCopy(usedIdsForUser);
}
@GuardedBy("mPreviouslyUsedIds")
@NonNull
private Set<Integer> getPreviouslyUsedIdsForPackageLocked(
@UserIdInt int userId, @NonNull String packageName) {
final Set<Integer> previouslyUsedIds = mPreviouslyUsedIds.get(userId).get(packageName);
if (previouslyUsedIds != null) return previouslyUsedIds;
return emptySet();
// "Deeply unmodifiable" map: the map itself and the Set<Integer> values it contains are all
// unmodifiable.
final Map<String, Set<Integer>> usedIdsForUser = getPreviouslyUsedIdsForUserLocked(userId);
final Set<Integer> usedIdsForPackage = usedIdsForUser.get(packageName);
if (usedIdsForPackage == null) {
return Collections.emptySet();
}
//The set is already unmodifiable.
return usedIdsForPackage;
}
private int getNewAssociationIdForPackage(@UserIdInt int userId, @NonNull String packageName) {
synchronized (mLock) {
readPersistedStateForUserIfNeededLocked(userId);
synchronized (mPreviouslyUsedIds) {
// First: collect all IDs currently in use for this user's Associations.
final SparseBooleanArray usedIds = new SparseBooleanArray();
for (AssociationInfo it : getAllAssociationsForUser(userId)) {
for (AssociationInfo it : mAssociationStore.getAssociationsForUser(userId)) {
usedIds.put(it.getId(), true);
}
@@ -804,41 +808,14 @@ public class CompanionDeviceManagerService extends SystemService {
}
}
//TODO also revoke notification access
void disassociateInternal(@UserIdInt int userId, int associationId) {
updateAssociations(associations ->
filterOut(associations, it -> {
if (it.getId() != associationId) return false;
onAssociationPreRemove(it);
markIdAsPreviouslyUsedForPackage(
it.getId(), it.getUserId(), it.getPackageName());
return true;
}), userId);
restartBleScan();
//TODO: also revoke notification access
void disassociateInternal(int associationId) {
onAssociationPreRemove(associationId);
mAssociationStore.removeAssociation(associationId);
}
void clearAssociationForPackage(@UserIdInt int userId, @NonNull String packageName) {
if (DEBUG) Slog.d(LOG_TAG, "clearAssociationForPackage() u" + userId + "/" + packageName);
mCompanionDevicePresenceController.unbindDevicePresenceListener(packageName, userId);
updateAssociations(set -> filterOut(set, it -> it.belongsToPackage(userId, packageName)),
userId);
}
private void markIdAsPreviouslyUsedForPackage(
int associationId, @UserIdInt int userId, @NonNull String packageName) {
synchronized (mLock) {
// Mark as previously used.
readPersistedStateForUserIfNeededLocked(userId);
mPreviouslyUsedIds.get(userId)
.computeIfAbsent(packageName, it -> new HashSet<>())
.add(associationId);
}
}
void onAssociationPreRemove(AssociationInfo association) {
void onAssociationPreRemove(int associationId) {
final AssociationInfo association = mAssociationStore.getAssociationById(associationId);
if (association.isNotifyOnDeviceNearby()
|| (association.isSelfManaged()
&& mPresentSelfManagedDevices.contains(association.getId()))) {
@@ -849,7 +826,7 @@ public class CompanionDeviceManagerService extends SystemService {
String deviceProfile = association.getDeviceProfile();
if (deviceProfile != null) {
AssociationInfo otherAssociationWithDeviceProfile = find(
getAllAssociationsForUser(association.getUserId()),
mAssociationStore.getAssociationsForUser(association.getUserId()),
a -> !a.equals(association) && deviceProfile.equals(a.getDeviceProfile()));
if (otherAssociationWithDeviceProfile != null) {
Slog.i(LOG_TAG, "Not revoking " + deviceProfile
@@ -941,49 +918,7 @@ public class CompanionDeviceManagerService extends SystemService {
.getPackageInfoAsUser(packageName, flags , userId));
}
private void recordAssociation(AssociationInfo association, int userId) {
Slog.i(LOG_TAG, "recordAssociation(" + association + ")");
updateAssociations(associations -> add(associations, association), userId);
}
private void updateAssociations(Function<Set<AssociationInfo>, Set<AssociationInfo>> update,
int userId) {
final List<AssociationInfo> associationList;
synchronized (mLock) {
if (DEBUG) Slog.d(LOG_TAG, "Updating Associations set...");
final Set<AssociationInfo> prevAssociations = getAllAssociationsForUser(userId);
if (DEBUG) Slog.d(LOG_TAG, " > Before : " + prevAssociations + "...");
final Set<AssociationInfo> updatedAssociations = update.apply(
new ArraySet<>(prevAssociations));
if (DEBUG) Slog.d(LOG_TAG, " > After: " + updatedAssociations);
associationList = new ArrayList<>(updatedAssociations);
mCachedAssociations.put(userId, unmodifiableSet(updatedAssociations));
BackgroundThread.getHandler().sendMessage(
PooledLambda.obtainMessage(
(associations, usedIds) ->
mPersistentDataStore
.persistStateForUser(userId, associations, usedIds),
updatedAssociations, deepCopy(mPreviouslyUsedIds.get(userId))));
updateAtm(userId, updatedAssociations);
}
mListeners.broadcast((listener, callbackUserId) -> {
if ((int) callbackUserId == userId) {
try {
listener.onAssociationsChanged(associationList);
} catch (RemoteException ignored) {
}
}
});
}
private void updateAtm(int userId, Set<AssociationInfo> associations) {
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(),
@@ -998,50 +933,18 @@ public class CompanionDeviceManagerService extends SystemService {
}
}
@GuardedBy("mLock")
private void readPersistedStateForUserIfNeededLocked(@UserIdInt int userId) {
if (mCachedAssociations.get(userId) != null) return;
Slog.i(LOG_TAG, "Reading state for user " + userId + " from the disk");
final Set<AssociationInfo> associations = new ArraySet<>();
final Map<String, Set<Integer>> previouslyUsedIds = new ArrayMap<>();
mPersistentDataStore.readStateForUser(userId, associations, previouslyUsedIds);
if (DEBUG) {
Slog.d(LOG_TAG, " > associations=" + associations + "\n"
+ " > previouslyUsedIds=" + previouslyUsedIds);
}
mCachedAssociations.put(userId, unmodifiableSet(associations));
mPreviouslyUsedIds.append(userId, previouslyUsedIds);
}
private List<UserInfo> getAllUsers() {
final long identity = Binder.clearCallingIdentity();
try {
return mUserManager.getUsers();
} finally {
Binder.restoreCallingIdentity(identity);
}
}
void onDeviceConnected(String address) {
Slog.d(LOG_TAG, "onDeviceConnected(address = " + address + ")");
mCurrentlyConnectedDevices.add(address);
for (UserInfo user : getAllUsers()) {
for (AssociationInfo association : getAllAssociationsForUser(user.id)) {
if (association.isLinkedTo(address)) {
if (association.getDeviceProfile() != null) {
Slog.i(LOG_TAG, "Granting role " + association.getDeviceProfile()
+ " to " + association.getPackageName()
+ " due to device connected: " + association.getDeviceMacAddress());
for (AssociationInfo association : mAssociationStore.getAssociationsByAddress(address)) {
if (association.getDeviceProfile() != null) {
Slog.i(LOG_TAG, "Granting role " + association.getDeviceProfile()
+ " to " + association.getPackageName()
+ " due to device connected: " + association.getDeviceMacAddress());
addRoleHolderForAssociation(getContext(), association);
}
}
addRoleHolderForAssociation(getContext(), association);
}
}
@@ -1134,7 +1037,9 @@ public class CompanionDeviceManagerService extends SystemService {
Date lastNearby = mDevicesLastNearby.valueAt(i);
if (isDeviceDisappeared(lastNearby)) {
for (AssociationInfo association : getAllAssociations(address)) {
final List<AssociationInfo> associations =
mAssociationStore.getAssociationsByAddress(address);
for (AssociationInfo association : associations) {
if (association.isNotifyOnDeviceNearby()) {
mCompanionDevicePresenceController.unbindDevicePresenceListener(
association.getPackageName(), association.getUserId());
@@ -1176,20 +1081,6 @@ public class CompanionDeviceManagerService extends SystemService {
}
}
private Set<AssociationInfo> getAllAssociations(String deviceAddress) {
List<UserInfo> aliveUsers = mUserManager.getAliveUsers();
Set<AssociationInfo> result = new ArraySet<>();
for (int i = 0, size = aliveUsers.size(); i < size; i++) {
UserInfo user = aliveUsers.get(i);
for (AssociationInfo association : getAllAssociationsForUser(user.id)) {
if (association.isLinkedTo(deviceAddress)) {
result.add(association);
}
}
}
return result;
}
private void onDeviceNearby(String address) {
Date timestamp = new Date();
Date oldTimestamp = mDevicesLastNearby.put(address, timestamp);
@@ -1205,7 +1096,9 @@ public class CompanionDeviceManagerService extends SystemService {
|| timestamp.getTime() - oldTimestamp.getTime() >= DEVICE_DISAPPEARED_TIMEOUT_MS;
if (justAppeared) {
Slog.i(LOG_TAG, "onDeviceNearby(justAppeared, address = " + address + ")");
for (AssociationInfo association : getAllAssociations(address)) {
final List<AssociationInfo> associations =
mAssociationStore.getAssociationsByAddress(address);
for (AssociationInfo association : associations) {
if (association.isNotifyOnDeviceNearby()) {
mCompanionDevicePresenceController.onDeviceNotifyAppeared(association,
getContext(), mMainHandler);
@@ -1218,7 +1111,9 @@ public class CompanionDeviceManagerService extends SystemService {
Slog.i(LOG_TAG, "onDeviceDisappeared(address = " + address + ")");
boolean hasDeviceListeners = false;
for (AssociationInfo association : getAllAssociations(address)) {
final List<AssociationInfo> associations =
mAssociationStore.getAssociationsByAddress(address);
for (AssociationInfo association : associations) {
if (association.isNotifyOnDeviceNearby()) {
mCompanionDevicePresenceController.onDeviceNotifyDisappeared(
association, getContext(), mMainHandler);
@@ -1291,7 +1186,7 @@ public class CompanionDeviceManagerService extends SystemService {
private List<ScanFilter> getBleScanFilters() {
ArrayList<ScanFilter> result = new ArrayList<>();
ArraySet<String> addressesSeen = new ArraySet<>();
for (AssociationInfo association : getAllAssociations()) {
for (AssociationInfo association : mAssociationStore.getAssociations()) {
if (association.isSelfManaged()) {
continue;
}
@@ -1331,17 +1226,6 @@ public class CompanionDeviceManagerService extends SystemService {
}
}
private static @NonNull <T> Set<T> filterOut(
@NonNull Set<T> set, @NonNull Predicate<? super T> predicate) {
return CollectionUtils.filter(set, predicate.negate());
}
private Map<String, Set<Integer>> deepCopy(Map<String, Set<Integer>> orig) {
final Map<String, Set<Integer>> copy = new HashMap<>(orig.size(), 1f);
forEach(orig, (key, value) -> copy.put(key, new ArraySet<>(value)));
return copy;
}
void checkUsesFeature(@NonNull String pkg, @UserIdInt int userId) {
if (getCallingUid() == SYSTEM_UID) return;
@@ -1356,4 +1240,61 @@ public class CompanionDeviceManagerService extends SystemService {
+ FEATURE_COMPANION_DEVICE_SETUP
+ " in manifest to use this API");
}
private void registerPackageMonitor() {
new PackageMonitor() {
@Override
public void onPackageRemoved(String packageName, int uid) {
final int userId = getChangingUserId();
Slog.i(LOG_TAG, "onPackageRemoved() u" + userId + "/" + packageName);
clearAssociationForPackage(userId, packageName);
}
@Override
public void onPackageDataCleared(String packageName, int uid) {
final int userId = getChangingUserId();
Slog.i(LOG_TAG, "onPackageDataCleared() u" + userId + "/" + packageName);
clearAssociationForPackage(userId, packageName);
}
@Override
public void onPackageModified(String packageName) {
final int userId = getChangingUserId();
Slog.i(LOG_TAG, "onPackageModified() u" + userId + "/" + packageName);
final List<AssociationInfo> associationsForPackage =
mAssociationStore.getAssociationsForPackage(userId, packageName);
for (AssociationInfo association : associationsForPackage) {
updateSpecialAccessPermissionForAssociatedPackage(association);
}
}
}.register(getContext(), FgThread.get().getLooper(), UserHandle.ALL, true);
}
private void clearAssociationForPackage(@UserIdInt int userId, @NonNull String packageName) {
if (DEBUG) Slog.d(LOG_TAG, "clearAssociationForPackage() u" + userId + "/" + packageName);
// First, unbind CompanionService if needed.
mCompanionDevicePresenceController.unbindDevicePresenceListener(packageName, userId);
// Clear associations.
final List<AssociationInfo> associationsForPackage =
mAssociationStore.getAssociationsForPackage(userId, packageName);
for (AssociationInfo association : associationsForPackage) {
mAssociationStore.removeAssociation(association.getId());
}
}
private static Map<String, Set<Integer>> deepUnmodifiableCopy(Map<String, Set<Integer>> orig) {
final Map<String, Set<Integer>> copy = new HashMap<>();
for (Map.Entry<String, Set<Integer>> entry : orig.entrySet()) {
final Set<Integer> valueCopy = new HashSet<>(entry.getValue());
copy.put(entry.getKey(), Collections.unmodifiableSet(valueCopy));
}
return Collections.unmodifiableMap(copy);
}
}

View File

@@ -16,7 +16,6 @@
package com.android.server.companion;
import static com.android.internal.util.CollectionUtils.forEach;
import static com.android.server.companion.CompanionDeviceManagerService.LOG_TAG;
import android.companion.AssociationInfo;
@@ -24,24 +23,33 @@ import android.util.Log;
import android.util.Slog;
import java.io.PrintWriter;
import java.util.List;
class CompanionDeviceShellCommand extends android.os.ShellCommand {
private final CompanionDeviceManagerService mService;
private final AssociationStore mAssociationStore;
CompanionDeviceShellCommand(CompanionDeviceManagerService service) {
CompanionDeviceShellCommand(CompanionDeviceManagerService service,
AssociationStore associationStore) {
mService = service;
mAssociationStore = associationStore;
}
@Override
public int onCommand(String cmd) {
final PrintWriter out = getOutPrintWriter();
try {
switch (cmd) {
case "list": {
forEach(
mService.getAllAssociationsForUser(getNextArgInt()),
a -> getOutPrintWriter()
.println(a.getPackageName() + " "
+ a.getDeviceMacAddress()));
final int userId = getNextArgInt();
final List<AssociationInfo> associationsForUser =
mAssociationStore.getAssociationsForUser(userId);
for (AssociationInfo association : associationsForUser) {
// TODO(b/212535524): use AssociationInfo.toShortString(), once it's not
// longer referenced in tests.
out.println(association.getPackageName() + " "
+ association.getDeviceMacAddress());
}
}
break;
@@ -60,7 +68,7 @@ class CompanionDeviceShellCommand extends android.os.ShellCommand {
final AssociationInfo association =
mService.getAssociationWithCallerChecks(userId, packageName, address);
if (association != null) {
mService.disassociateInternal(userId, association.getId());
mService.disassociateInternal(association.getId());
}
}
break;

View File

@@ -33,11 +33,14 @@ import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.UserIdInt;
import android.companion.AssociationInfo;
import android.content.pm.UserInfo;
import android.net.MacAddress;
import android.os.Environment;
import android.util.ArrayMap;
import android.util.AtomicFile;
import android.util.ExceptionUtils;
import android.util.Slog;
import android.util.SparseArray;
import android.util.TypedXmlPullParser;
import android.util.TypedXmlSerializer;
import android.util.Xml;
@@ -51,7 +54,9 @@ import org.xmlpull.v1.XmlSerializer;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
@@ -68,8 +73,8 @@ import java.util.concurrent.ConcurrentMap;
*
* Before Android T the data was stored using the v0 schema.
*
* @see #readAssociationsV0(TypedXmlPullParser, int, Set)
* @see #readAssociationV0(TypedXmlPullParser, int, int, Set)
* @see #readAssociationsV0(TypedXmlPullParser, int, Collection)
* @see #readAssociationV0(TypedXmlPullParser, int, int, Collection)
*
* The following snippet is a sample of a the file that is using v0 schema.
* <pre>{@code
@@ -100,8 +105,8 @@ import java.util.concurrent.ConcurrentMap;
* optional.
*
* @see #CURRENT_PERSISTENCE_VERSION
* @see #readAssociationsV1(TypedXmlPullParser, int, Set)
* @see #readAssociationV1(TypedXmlPullParser, int, Set)
* @see #readAssociationsV1(TypedXmlPullParser, int, Collection)
* @see #readAssociationV1(TypedXmlPullParser, int, Collection)
* @see #readPreviouslyUsedIdsV1(TypedXmlPullParser, Map)
*
* The following snippet is a sample of a the file that is using v0 schema.
@@ -168,6 +173,23 @@ final class PersistentDataStore {
private final @NonNull ConcurrentMap<Integer, AtomicFile> mUserIdToStorageFile =
new ConcurrentHashMap<>();
void readStateForUsers(@NonNull List<UserInfo> users,
@NonNull Set<AssociationInfo> allAssociationsOut,
@NonNull SparseArray<Map<String, Set<Integer>>> previouslyUsedIdsPerUserOut) {
for (UserInfo user : users) {
final int userId = user.id;
// Previously used IDs are stored in the "out" collection per-user.
final Map<String, Set<Integer>> previouslyUsedIds = new ArrayMap<>();
// Associations for all users are stored in a single "flat" set: so we read directly
// into it.
readStateForUser(userId, allAssociationsOut, previouslyUsedIds);
// Save previously used IDs for this user into the "out" structure.
previouslyUsedIdsPerUserOut.append(userId, previouslyUsedIds);
}
}
/**
* Reads previously persisted data for the given user "into" the provided containers.
*
@@ -176,7 +198,7 @@ final class PersistentDataStore {
* @param previouslyUsedIdsPerPackageOut a container to read the used IDs "into".
*/
void readStateForUser(@UserIdInt int userId,
@NonNull Set<AssociationInfo> associationsOut,
@NonNull Collection<AssociationInfo> associationsOut,
@NonNull Map<String, Set<Integer>> previouslyUsedIdsPerPackageOut) {
Slog.i(LOG_TAG, "Reading associations for user " + userId + " from disk");
final AtomicFile file = getStorageFileForUser(userId);
@@ -237,7 +259,8 @@ final class PersistentDataStore {
* @param associations a set of user's associations.
* @param previouslyUsedIdsPerPackage a set previously used Association IDs for the user.
*/
void persistStateForUser(@UserIdInt int userId, @NonNull Set<AssociationInfo> associations,
void persistStateForUser(@UserIdInt int userId,
@NonNull Collection<AssociationInfo> associations,
@NonNull Map<String, Set<Integer>> previouslyUsedIdsPerPackage) {
Slog.i(LOG_TAG, "Writing associations for user " + userId + " to disk");
if (DEBUG) Slog.d(LOG_TAG, " > " + associations);
@@ -250,7 +273,7 @@ final class PersistentDataStore {
}
private int readStateFromFileLocked(@UserIdInt int userId, @NonNull AtomicFile file,
@NonNull String rootTag, @Nullable Set<AssociationInfo> associationsOut,
@NonNull String rootTag, @Nullable Collection<AssociationInfo> associationsOut,
@NonNull Map<String, Set<Integer>> previouslyUsedIdsPerPackageOut) {
try (FileInputStream in = file.openRead()) {
final TypedXmlPullParser parser = Xml.resolvePullParser(in);
@@ -282,7 +305,7 @@ final class PersistentDataStore {
}
private void persistStateToFileLocked(@NonNull AtomicFile file,
@Nullable Set<AssociationInfo> associations,
@Nullable Collection<AssociationInfo> associations,
@NonNull Map<String, Set<Integer>> previouslyUsedIdsPerPackage) {
file.write(out -> {
try {
@@ -321,7 +344,7 @@ final class PersistentDataStore {
}
private static void readAssociationsV0(@NonNull TypedXmlPullParser parser,
@UserIdInt int userId, @NonNull Set<AssociationInfo> out)
@UserIdInt int userId, @NonNull Collection<AssociationInfo> out)
throws XmlPullParserException, IOException {
requireStartOfTag(parser, XML_TAG_ASSOCIATIONS);
@@ -342,7 +365,8 @@ final class PersistentDataStore {
}
private static void readAssociationV0(@NonNull TypedXmlPullParser parser, @UserIdInt int userId,
int associationId, @NonNull Set<AssociationInfo> out) throws XmlPullParserException {
int associationId, @NonNull Collection<AssociationInfo> out)
throws XmlPullParserException {
requireStartOfTag(parser, XML_TAG_ASSOCIATION);
final String appPackage = readStringAttribute(parser, XML_ATTR_PACKAGE);
@@ -360,7 +384,7 @@ final class PersistentDataStore {
}
private static void readAssociationsV1(@NonNull TypedXmlPullParser parser,
@UserIdInt int userId, @NonNull Set<AssociationInfo> out)
@UserIdInt int userId, @NonNull Collection<AssociationInfo> out)
throws XmlPullParserException, IOException {
requireStartOfTag(parser, XML_TAG_ASSOCIATIONS);
@@ -374,7 +398,7 @@ final class PersistentDataStore {
}
private static void readAssociationV1(@NonNull TypedXmlPullParser parser, @UserIdInt int userId,
@NonNull Set<AssociationInfo> out) throws XmlPullParserException, IOException {
@NonNull Collection<AssociationInfo> out) throws XmlPullParserException, IOException {
requireStartOfTag(parser, XML_TAG_ASSOCIATION);
final int associationId = readIntAttribute(parser, XML_ATTR_ID);
@@ -421,9 +445,11 @@ final class PersistentDataStore {
}
private static void writeAssociations(@NonNull XmlSerializer parent,
@Nullable Set<AssociationInfo> associations) throws IOException {
@Nullable Collection<AssociationInfo> associations) throws IOException {
final XmlSerializer serializer = parent.startTag(null, XML_TAG_ASSOCIATIONS);
forEach(associations, it -> writeAssociation(serializer, it));
for (AssociationInfo association : associations) {
writeAssociation(serializer, association);
}
serializer.endTag(null, XML_TAG_ASSOCIATIONS);
}