diff --git a/core/java/android/companion/AssociationInfo.java b/core/java/android/companion/AssociationInfo.java index 3f02aa28ae05c..78d5137ebfbaa 100644 --- a/core/java/android/companion/AssociationInfo.java +++ b/core/java/android/companion/AssociationInfo.java @@ -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{" diff --git a/services/companion/java/com/android/server/companion/AssociationRequestsProcessor.java b/services/companion/java/com/android/server/companion/AssociationRequestsProcessor.java index 637994f6d73d2..1914164f195c4 100644 --- a/services/companion/java/com/android/server/companion/AssociationRequestsProcessor.java +++ b/services/companion/java/com/android/server/companion/AssociationRequestsProcessor.java @@ -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 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 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); diff --git a/services/companion/java/com/android/server/companion/AssociationStore.java b/services/companion/java/com/android/server/companion/AssociationStore.java new file mode 100644 index 0000000000000..58fc8f7fe5b64 --- /dev/null +++ b/services/companion/java/com/android/server/companion/AssociationStore.java @@ -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 getAssociations(); + + /** + * @return a {@link List} of associations that belong to the user. + */ + @NonNull + List getAssociationsForUser(@UserIdInt int userId); + + /** + * @return a {@link List} of association that belong to the package. + */ + @NonNull + List 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 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 + ")"; + } + } +} diff --git a/services/companion/java/com/android/server/companion/AssociationStoreImpl.java b/services/companion/java/com/android/server/companion/AssociationStoreImpl.java new file mode 100644 index 0000000000000..3f0200ea584f9 --- /dev/null +++ b/services/companion/java/com/android/server/companion/AssociationStoreImpl.java @@ -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. + *
    + *
  • {@link #addAssociation(AssociationInfo)} + *
  • {@link #removeAssociation(int)} + *
  • {@link #updateAssociation(AssociationInfo)} + *
+ * + * 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 mIdMap; + @GuardedBy("mLock") + private final Map> mAddressMap; + @GuardedBy("mLock") + private final SparseArray> mCachedPerUser = new SparseArray<>(); + + @GuardedBy("mListeners") + private final Set mListeners = new LinkedHashSet<>(); + + AssociationStoreImpl(Collection 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 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 getAssociations() { + final Collection allAssociations; + synchronized (mLock) { + allAssociations = mIdMap.values(); + } + return Collections.unmodifiableCollection(allAssociations); + } + + public @NonNull List getAssociationsForUser(@UserIdInt int userId) { + synchronized (mLock) { + return getAssociationsForUserLocked(userId); + } + } + + public @NonNull List getAssociationsForPackage( + @UserIdInt int userId, @NonNull String packageName) { + final List associationsForUser = getAssociationsForUser(userId); + final List 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 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 getAssociationsByAddress(@NonNull String macAddress) { + final MacAddress address = MacAddress.fromString(macAddress); + + synchronized (mLock) { + final Set ids = mAddressMap.get(address); + if (ids == null) return Collections.emptyList(); + + final List 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 getAssociationsForUserLocked(@UserIdInt int userId) { + final List cached = mCachedPerUser.get(userId); + if (cached != null) { + return cached; + } + + final List associationsForUser = new ArrayList<>(); + for (AssociationInfo association : mIdMap.values()) { + if (association.getUserId() == userId) { + associationsForUser.add(association); + } + } + final List 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; + } + } + } + } +} diff --git a/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java b/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java index a06672b29d742..5aa1c933ad873 100644 --- a/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java +++ b/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java @@ -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 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> 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>> 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 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 associations = getAllAssociationsForUser(userId); + final List associations = mAssociationStore.getAssociationsForUser(userId); if (associations.isEmpty()) return; @@ -293,42 +267,18 @@ public class CompanionDeviceManagerService extends SystemService { MINUTES.toMillis(10)); } - @NonNull - Set 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 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 getAllAssociations() { - final long identity = Binder.clearCallingIdentity(); - try { - final Set 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 associations = getAllAssociationsForUser(userId); - if (associations == null) { - continue; - } + final List 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 updatedAssociations = + mAssociationStore.getAssociationsForUser(userId); + final Map> 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 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> usedIdsForUser = mPreviouslyUsedIds.get(userId); + if (usedIdsForUser == null) { + usedIdsForUser = new HashMap<>(); + mPreviouslyUsedIds.put(userId, usedIdsForUser); + } + + final Set 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 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> getPreviouslyUsedIdsForUser(@UserIdInt int userId) { + synchronized (mPreviouslyUsedIds) { + return getPreviouslyUsedIdsForUserLocked(userId); + } + } + + @GuardedBy("mPreviouslyUsedIds") + @NonNull + private Map> getPreviouslyUsedIdsForUserLocked(@UserIdInt int userId) { + final Map> usedIdsForUser = mPreviouslyUsedIds.get(userId); + if (usedIdsForUser == null) { + return Collections.emptyMap(); + } + return deepUnmodifiableCopy(usedIdsForUser); + } + + @GuardedBy("mPreviouslyUsedIds") @NonNull private Set getPreviouslyUsedIdsForPackageLocked( @UserIdInt int userId, @NonNull String packageName) { - final Set previouslyUsedIds = mPreviouslyUsedIds.get(userId).get(packageName); - if (previouslyUsedIds != null) return previouslyUsedIds; - return emptySet(); + // "Deeply unmodifiable" map: the map itself and the Set values it contains are all + // unmodifiable. + final Map> usedIdsForUser = getPreviouslyUsedIdsForUserLocked(userId); + final Set 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> update, - int userId) { - final List associationList; - synchronized (mLock) { - if (DEBUG) Slog.d(LOG_TAG, "Updating Associations set..."); - - final Set prevAssociations = getAllAssociationsForUser(userId); - if (DEBUG) Slog.d(LOG_TAG, " > Before : " + prevAssociations + "..."); - - final Set 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 associations) { + private void updateAtm(int userId, List associations) { final Set 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 associations = new ArraySet<>(); - final Map> 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 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 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 getAllAssociations(String deviceAddress) { - List aliveUsers = mUserManager.getAliveUsers(); - Set 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 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 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 getBleScanFilters() { ArrayList result = new ArrayList<>(); ArraySet 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 Set filterOut( - @NonNull Set set, @NonNull Predicate predicate) { - return CollectionUtils.filter(set, predicate.negate()); - } - - private Map> deepCopy(Map> orig) { - final Map> 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 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 associationsForPackage = + mAssociationStore.getAssociationsForPackage(userId, packageName); + for (AssociationInfo association : associationsForPackage) { + mAssociationStore.removeAssociation(association.getId()); + } + } + + private static Map> deepUnmodifiableCopy(Map> orig) { + final Map> copy = new HashMap<>(); + + for (Map.Entry> entry : orig.entrySet()) { + final Set valueCopy = new HashSet<>(entry.getValue()); + copy.put(entry.getKey(), Collections.unmodifiableSet(valueCopy)); + } + + return Collections.unmodifiableMap(copy); + } } diff --git a/services/companion/java/com/android/server/companion/CompanionDeviceShellCommand.java b/services/companion/java/com/android/server/companion/CompanionDeviceShellCommand.java index 5cb30797c0f43..5c0571d801aa9 100644 --- a/services/companion/java/com/android/server/companion/CompanionDeviceShellCommand.java +++ b/services/companion/java/com/android/server/companion/CompanionDeviceShellCommand.java @@ -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 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; diff --git a/services/companion/java/com/android/server/companion/PersistentDataStore.java b/services/companion/java/com/android/server/companion/PersistentDataStore.java index 87558dfd4ffd0..e2a814bba0158 100644 --- a/services/companion/java/com/android/server/companion/PersistentDataStore.java +++ b/services/companion/java/com/android/server/companion/PersistentDataStore.java @@ -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. *
{@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 mUserIdToStorageFile =
             new ConcurrentHashMap<>();
 
+    void readStateForUsers(@NonNull List users,
+            @NonNull Set allAssociationsOut,
+            @NonNull SparseArray>> previouslyUsedIdsPerUserOut) {
+        for (UserInfo user : users) {
+            final int userId = user.id;
+            // Previously used IDs are stored in the "out" collection per-user.
+            final Map> 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 associationsOut,
+            @NonNull Collection associationsOut,
             @NonNull Map> 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 associations,
+    void persistStateForUser(@UserIdInt int userId,
+            @NonNull Collection associations,
             @NonNull Map> 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 associationsOut,
+            @NonNull String rootTag, @Nullable Collection associationsOut,
             @NonNull Map> 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 associations,
+            @Nullable Collection associations,
             @NonNull Map> previouslyUsedIdsPerPackage) {
         file.write(out -> {
             try {
@@ -321,7 +344,7 @@ final class PersistentDataStore {
     }
 
     private static void readAssociationsV0(@NonNull TypedXmlPullParser parser,
-            @UserIdInt int userId, @NonNull Set out)
+            @UserIdInt int userId, @NonNull Collection 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 out) throws XmlPullParserException {
+            int associationId, @NonNull Collection 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 out)
+            @UserIdInt int userId, @NonNull Collection 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 out) throws XmlPullParserException, IOException {
+            @NonNull Collection 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 associations) throws IOException {
+            @Nullable Collection 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);
     }