1 /* 2 * Copyright (C) 2021 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.server.companion; 18 19 import static com.android.internal.util.CollectionUtils.forEach; 20 import static com.android.internal.util.XmlUtils.readBooleanAttribute; 21 import static com.android.internal.util.XmlUtils.readIntAttribute; 22 import static com.android.internal.util.XmlUtils.readLongAttribute; 23 import static com.android.internal.util.XmlUtils.readStringAttribute; 24 import static com.android.internal.util.XmlUtils.writeBooleanAttribute; 25 import static com.android.internal.util.XmlUtils.writeIntAttribute; 26 import static com.android.internal.util.XmlUtils.writeLongAttribute; 27 import static com.android.internal.util.XmlUtils.writeStringAttribute; 28 import static com.android.server.companion.CompanionDeviceManagerService.getFirstAssociationIdForUser; 29 import static com.android.server.companion.CompanionDeviceManagerService.getLastAssociationIdForUser; 30 import static com.android.server.companion.DataStoreUtils.createStorageFileForUser; 31 import static com.android.server.companion.DataStoreUtils.isEndOfTag; 32 import static com.android.server.companion.DataStoreUtils.isStartOfTag; 33 import static com.android.server.companion.DataStoreUtils.writeToFileSafely; 34 35 import android.annotation.NonNull; 36 import android.annotation.Nullable; 37 import android.annotation.SuppressLint; 38 import android.annotation.UserIdInt; 39 import android.companion.AssociationInfo; 40 import android.content.pm.UserInfo; 41 import android.net.MacAddress; 42 import android.os.Environment; 43 import android.util.ArrayMap; 44 import android.util.AtomicFile; 45 import android.util.Log; 46 import android.util.Slog; 47 import android.util.SparseArray; 48 import android.util.Xml; 49 50 import com.android.internal.util.XmlUtils; 51 import com.android.modules.utils.TypedXmlPullParser; 52 import com.android.modules.utils.TypedXmlSerializer; 53 54 import org.xmlpull.v1.XmlPullParser; 55 import org.xmlpull.v1.XmlPullParserException; 56 import org.xmlpull.v1.XmlSerializer; 57 58 import java.io.File; 59 import java.io.FileInputStream; 60 import java.io.IOException; 61 import java.util.Collection; 62 import java.util.HashSet; 63 import java.util.List; 64 import java.util.Map; 65 import java.util.Set; 66 import java.util.concurrent.ConcurrentHashMap; 67 import java.util.concurrent.ConcurrentMap; 68 69 /** 70 * The class responsible for persisting Association records and other related information (such as 71 * previously used IDs) to a disk, and reading the data back from the disk. 72 * 73 * <p> 74 * Before Android T the data was stored in "companion_device_manager_associations.xml" file in 75 * {@link Environment#getUserSystemDirectory(int) /data/system/user/}. 76 * 77 * See {@link #getBaseLegacyStorageFileForUser(int) getBaseLegacyStorageFileForUser()}. 78 * 79 * <p> 80 * Before Android T the data was stored using the v0 schema. See: 81 * <ul> 82 * <li>{@link #readAssociationsV0(TypedXmlPullParser, int, Collection) readAssociationsV0()}. 83 * <li>{@link #readAssociationV0(TypedXmlPullParser, int, int, Collection) readAssociationV0()}. 84 * </ul> 85 * 86 * The following snippet is a sample of a file that is using v0 schema. 87 * <pre>{@code 88 * <associations> 89 * <association 90 * package="com.sample.companion.app" 91 * device="AA:BB:CC:DD:EE:00" 92 * time_approved="1634389553216" /> 93 * <association 94 * package="com.another.sample.companion.app" 95 * device="AA:BB:CC:DD:EE:01" 96 * profile="android.app.role.COMPANION_DEVICE_WATCH" 97 * notify_device_nearby="false" 98 * time_approved="1634389752662" /> 99 * </associations> 100 * }</pre> 101 * 102 * <p> 103 * Since Android T the data is stored to "companion_device_manager.xml" file in 104 * {@link Environment#getDataSystemDeDirectory(int) /data/system_de/}. 105 * 106 * See {@link DataStoreUtils#getBaseStorageFileForUser(int, String)} 107 * 108 * <p> 109 * Since Android T the data is stored using the v1 schema. 110 * 111 * In the v1 schema, a list of the previously used IDs is stored along with the association 112 * records. 113 * 114 * V1 schema adds a new optional "display_name" attribute, and makes the "mac_address" attribute 115 * optional. 116 * <ul> 117 * <li> {@link #CURRENT_PERSISTENCE_VERSION} 118 * <li> {@link #readAssociationsV1(TypedXmlPullParser, int, Collection) readAssociationsV1()} 119 * <li> {@link #readAssociationV1(TypedXmlPullParser, int, Collection) readAssociationV1()} 120 * <li> {@link #readPreviouslyUsedIdsV1(TypedXmlPullParser, Map) readPreviouslyUsedIdsV1()} 121 * </ul> 122 * 123 * The following snippet is a sample of a file that is using v1 schema. 124 * <pre>{@code 125 * <state persistence-version="1"> 126 * <associations> 127 * <association 128 * id="1" 129 * package="com.sample.companion.app" 130 * mac_address="AA:BB:CC:DD:EE:00" 131 * self_managed="false" 132 * notify_device_nearby="false" 133 * revoked="false" 134 * last_time_connected="1634641160229" 135 * time_approved="1634389553216" 136 * system_data_sync_flags="0"/> 137 * 138 * <association 139 * id="3" 140 * profile="android.app.role.COMPANION_DEVICE_WATCH" 141 * package="com.sample.companion.another.app" 142 * display_name="Jhon's Chromebook" 143 * self_managed="true" 144 * notify_device_nearby="false" 145 * revoked="false" 146 * last_time_connected="1634641160229" 147 * time_approved="1634641160229" 148 * system_data_sync_flags="1"/> 149 * </associations> 150 * 151 * <previously-used-ids> 152 * <package package_name="com.sample.companion.app"> 153 * <id>2</id> 154 * </package> 155 * </previously-used-ids> 156 * </state> 157 * }</pre> 158 */ 159 @SuppressLint("LongLogTag") 160 final class PersistentDataStore { 161 private static final String TAG = "CompanionDevice_PersistentDataStore"; 162 private static final boolean DEBUG = CompanionDeviceManagerService.DEBUG; 163 164 private static final int CURRENT_PERSISTENCE_VERSION = 1; 165 166 private static final String FILE_NAME_LEGACY = "companion_device_manager_associations.xml"; 167 private static final String FILE_NAME = "companion_device_manager.xml"; 168 169 private static final String XML_TAG_STATE = "state"; 170 private static final String XML_TAG_ASSOCIATIONS = "associations"; 171 private static final String XML_TAG_ASSOCIATION = "association"; 172 private static final String XML_TAG_PREVIOUSLY_USED_IDS = "previously-used-ids"; 173 private static final String XML_TAG_PACKAGE = "package"; 174 private static final String XML_TAG_ID = "id"; 175 176 private static final String XML_ATTR_PERSISTENCE_VERSION = "persistence-version"; 177 private static final String XML_ATTR_ID = "id"; 178 // Used in <package> elements, nested within <previously-used-ids> elements. 179 private static final String XML_ATTR_PACKAGE_NAME = "package_name"; 180 // Used in <association> elements, nested within <associations> elements. 181 private static final String XML_ATTR_PACKAGE = "package"; 182 private static final String XML_ATTR_MAC_ADDRESS = "mac_address"; 183 private static final String XML_ATTR_DISPLAY_NAME = "display_name"; 184 private static final String XML_ATTR_PROFILE = "profile"; 185 private static final String XML_ATTR_SELF_MANAGED = "self_managed"; 186 private static final String XML_ATTR_NOTIFY_DEVICE_NEARBY = "notify_device_nearby"; 187 private static final String XML_ATTR_REVOKED = "revoked"; 188 private static final String XML_ATTR_TIME_APPROVED = "time_approved"; 189 private static final String XML_ATTR_LAST_TIME_CONNECTED = "last_time_connected"; 190 private static final String XML_ATTR_SYSTEM_DATA_SYNC_FLAGS = "system_data_sync_flags"; 191 192 private static final String LEGACY_XML_ATTR_DEVICE = "device"; 193 194 private final @NonNull ConcurrentMap<Integer, AtomicFile> mUserIdToStorageFile = 195 new ConcurrentHashMap<>(); 196 readStateForUsers(@onNull List<UserInfo> users, @NonNull Set<AssociationInfo> allAssociationsOut, @NonNull SparseArray<Map<String, Set<Integer>>> previouslyUsedIdsPerUserOut)197 void readStateForUsers(@NonNull List<UserInfo> users, 198 @NonNull Set<AssociationInfo> allAssociationsOut, 199 @NonNull SparseArray<Map<String, Set<Integer>>> previouslyUsedIdsPerUserOut) { 200 for (UserInfo user : users) { 201 final int userId = user.id; 202 // Previously used IDs are stored in the "out" collection per-user. 203 final Map<String, Set<Integer>> previouslyUsedIds = new ArrayMap<>(); 204 205 // Associations for all users are stored in a single "flat" set: so we read directly 206 // into it. 207 final Set<AssociationInfo> associationsForUser = new HashSet<>(); 208 readStateForUser(userId, associationsForUser, previouslyUsedIds); 209 210 // Go through all the associations for the user and check if their IDs are within 211 // the allowed range (for the user). 212 final int firstAllowedId = getFirstAssociationIdForUser(userId); 213 final int lastAllowedId = getLastAssociationIdForUser(userId); 214 for (AssociationInfo association : associationsForUser) { 215 final int id = association.getId(); 216 if (id < firstAllowedId || id > lastAllowedId) { 217 Slog.e(TAG, "Wrong association ID assignment: " + id + ". " 218 + "Association belongs to u" + userId + " and thus its ID should be " 219 + "within [" + firstAllowedId + ", " + lastAllowedId + "] range."); 220 // TODO(b/224736262): try fixing (re-assigning) the ID? 221 } 222 } 223 224 // Add user's association to the "output" set. 225 allAssociationsOut.addAll(associationsForUser); 226 227 // Save previously used IDs for this user into the "out" structure. 228 previouslyUsedIdsPerUserOut.append(userId, previouslyUsedIds); 229 } 230 } 231 232 /** 233 * Reads previously persisted data for the given user "into" the provided containers. 234 * 235 * Note that {@link AssociationInfo#getAssociatedDevice()} will always be {@code null} after 236 * retrieval from this datastore because it is not persisted (by design). This means that 237 * persisted data is not guaranteed to be identical to the initial data that was stored at the 238 * time of association. 239 * 240 * @param userId Android UserID 241 * @param associationsOut a container to read the {@link AssociationInfo}s "into". 242 * @param previouslyUsedIdsPerPackageOut a container to read the used IDs "into". 243 */ readStateForUser(@serIdInt int userId, @NonNull Collection<AssociationInfo> associationsOut, @NonNull Map<String, Set<Integer>> previouslyUsedIdsPerPackageOut)244 void readStateForUser(@UserIdInt int userId, 245 @NonNull Collection<AssociationInfo> associationsOut, 246 @NonNull Map<String, Set<Integer>> previouslyUsedIdsPerPackageOut) { 247 Slog.i(TAG, "Reading associations for user " + userId + " from disk"); 248 final AtomicFile file = getStorageFileForUser(userId); 249 if (DEBUG) Log.d(TAG, " > File=" + file.getBaseFile().getPath()); 250 251 // getStorageFileForUser() ALWAYS returns the SAME OBJECT, which allows us to synchronize 252 // accesses to the file on the file system using this AtomicFile object. 253 synchronized (file) { 254 File legacyBaseFile = null; 255 final AtomicFile readFrom; 256 final String rootTag; 257 if (!file.getBaseFile().exists()) { 258 if (DEBUG) Log.d(TAG, " > File does not exist -> Try to read legacy file"); 259 260 legacyBaseFile = getBaseLegacyStorageFileForUser(userId); 261 if (DEBUG) Log.d(TAG, " > Legacy file=" + legacyBaseFile.getPath()); 262 if (!legacyBaseFile.exists()) { 263 if (DEBUG) Log.d(TAG, " > Legacy file does not exist -> Abort"); 264 return; 265 } 266 267 readFrom = new AtomicFile(legacyBaseFile); 268 rootTag = XML_TAG_ASSOCIATIONS; 269 } else { 270 readFrom = file; 271 rootTag = XML_TAG_STATE; 272 } 273 274 if (DEBUG) Log.d(TAG, " > Reading associations..."); 275 final int version = readStateFromFileLocked(userId, readFrom, rootTag, 276 associationsOut, previouslyUsedIdsPerPackageOut); 277 if (DEBUG) { 278 Log.d(TAG, " > Done reading: " + associationsOut); 279 if (version < CURRENT_PERSISTENCE_VERSION) { 280 Log.d(TAG, " > File used old format: v." + version + " -> Re-write"); 281 } 282 } 283 284 if (legacyBaseFile != null || version < CURRENT_PERSISTENCE_VERSION) { 285 // The data is either in the legacy file or in the legacy format, or both. 286 // Save the data to right file in using the current format. 287 if (DEBUG) { 288 Log.d(TAG, " > Writing the data to " + file.getBaseFile().getPath()); 289 } 290 persistStateToFileLocked(file, associationsOut, previouslyUsedIdsPerPackageOut); 291 292 if (legacyBaseFile != null) { 293 // We saved the data to the right file, can delete the old file now. 294 if (DEBUG) Log.d(TAG, " > Deleting legacy file"); 295 legacyBaseFile.delete(); 296 } 297 } 298 } 299 } 300 301 /** 302 * Persisted data to the disk. 303 * 304 * Note that associatedDevice field in {@link AssociationInfo} is not persisted by this 305 * datastore implementation. 306 * 307 * @param userId Android UserID 308 * @param associations a set of user's associations. 309 * @param previouslyUsedIdsPerPackage a set previously used Association IDs for the user. 310 */ persistStateForUser(@serIdInt int userId, @NonNull Collection<AssociationInfo> associations, @NonNull Map<String, Set<Integer>> previouslyUsedIdsPerPackage)311 void persistStateForUser(@UserIdInt int userId, 312 @NonNull Collection<AssociationInfo> associations, 313 @NonNull Map<String, Set<Integer>> previouslyUsedIdsPerPackage) { 314 Slog.i(TAG, "Writing associations for user " + userId + " to disk"); 315 if (DEBUG) Slog.d(TAG, " > " + associations); 316 317 final AtomicFile file = getStorageFileForUser(userId); 318 if (DEBUG) Log.d(TAG, " > File=" + file.getBaseFile().getPath()); 319 // getStorageFileForUser() ALWAYS returns the SAME OBJECT, which allows us to synchronize 320 // accesses to the file on the file system using this AtomicFile object. 321 synchronized (file) { 322 persistStateToFileLocked(file, associations, previouslyUsedIdsPerPackage); 323 } 324 } 325 readStateFromFileLocked(@serIdInt int userId, @NonNull AtomicFile file, @NonNull String rootTag, @Nullable Collection<AssociationInfo> associationsOut, @NonNull Map<String, Set<Integer>> previouslyUsedIdsPerPackageOut)326 private int readStateFromFileLocked(@UserIdInt int userId, @NonNull AtomicFile file, 327 @NonNull String rootTag, @Nullable Collection<AssociationInfo> associationsOut, 328 @NonNull Map<String, Set<Integer>> previouslyUsedIdsPerPackageOut) { 329 try (FileInputStream in = file.openRead()) { 330 final TypedXmlPullParser parser = Xml.resolvePullParser(in); 331 332 XmlUtils.beginDocument(parser, rootTag); 333 final int version = readIntAttribute(parser, XML_ATTR_PERSISTENCE_VERSION, 0); 334 switch (version) { 335 case 0: 336 readAssociationsV0(parser, userId, associationsOut); 337 break; 338 case 1: 339 while (true) { 340 parser.nextTag(); 341 if (isStartOfTag(parser, XML_TAG_ASSOCIATIONS)) { 342 readAssociationsV1(parser, userId, associationsOut); 343 } else if (isStartOfTag(parser, XML_TAG_PREVIOUSLY_USED_IDS)) { 344 readPreviouslyUsedIdsV1(parser, previouslyUsedIdsPerPackageOut); 345 } else if (isEndOfTag(parser, rootTag)) { 346 break; 347 } 348 } 349 break; 350 } 351 return version; 352 } catch (XmlPullParserException | IOException e) { 353 Slog.e(TAG, "Error while reading associations file", e); 354 return -1; 355 } 356 } 357 persistStateToFileLocked(@onNull AtomicFile file, @Nullable Collection<AssociationInfo> associations, @NonNull Map<String, Set<Integer>> previouslyUsedIdsPerPackage)358 private void persistStateToFileLocked(@NonNull AtomicFile file, 359 @Nullable Collection<AssociationInfo> associations, 360 @NonNull Map<String, Set<Integer>> previouslyUsedIdsPerPackage) { 361 // Writing to file could fail, for example, if the user has been recently removed and so was 362 // their DE (/data/system_de/<user-id>/) directory. 363 writeToFileSafely(file, out -> { 364 final TypedXmlSerializer serializer = Xml.resolveSerializer(out); 365 serializer.setFeature( 366 "http://xmlpull.org/v1/doc/features.html#indent-output", true); 367 368 serializer.startDocument(null, true); 369 serializer.startTag(null, XML_TAG_STATE); 370 writeIntAttribute(serializer, 371 XML_ATTR_PERSISTENCE_VERSION, CURRENT_PERSISTENCE_VERSION); 372 373 writeAssociations(serializer, associations); 374 writePreviouslyUsedIds(serializer, previouslyUsedIdsPerPackage); 375 376 serializer.endTag(null, XML_TAG_STATE); 377 serializer.endDocument(); 378 }); 379 } 380 381 /** 382 * Creates and caches {@link AtomicFile} object that represents the back-up file for the given 383 * user. 384 * 385 * IMPORTANT: the method will ALWAYS return the same {@link AtomicFile} object, which makes it 386 * possible to synchronize reads and writes to the file using the returned object. 387 */ getStorageFileForUser(@serIdInt int userId)388 private @NonNull AtomicFile getStorageFileForUser(@UserIdInt int userId) { 389 return mUserIdToStorageFile.computeIfAbsent(userId, 390 u -> createStorageFileForUser(userId, FILE_NAME)); 391 } 392 getBaseLegacyStorageFileForUser(@serIdInt int userId)393 private static @NonNull File getBaseLegacyStorageFileForUser(@UserIdInt int userId) { 394 return new File(Environment.getUserSystemDirectory(userId), FILE_NAME_LEGACY); 395 } 396 readAssociationsV0(@onNull TypedXmlPullParser parser, @UserIdInt int userId, @NonNull Collection<AssociationInfo> out)397 private static void readAssociationsV0(@NonNull TypedXmlPullParser parser, 398 @UserIdInt int userId, @NonNull Collection<AssociationInfo> out) 399 throws XmlPullParserException, IOException { 400 requireStartOfTag(parser, XML_TAG_ASSOCIATIONS); 401 402 // Before Android T Associations didn't have IDs, so when we are upgrading from S (reading 403 // from V0) we need to generate and assign IDs to the existing Associations. 404 // It's safe to do it here, because CDM cannot create new Associations before it reads 405 // existing ones from the backup files. And the fact that we are reading from a V0 file, 406 // means that CDM hasn't assigned any IDs yet, so we can just start from the first available 407 // id for each user (eg. 1 for user 0; 100 001 - for user 1; 200 001 - for user 2; etc). 408 int associationId = getFirstAssociationIdForUser(userId); 409 while (true) { 410 parser.nextTag(); 411 if (isEndOfTag(parser, XML_TAG_ASSOCIATIONS)) break; 412 if (!isStartOfTag(parser, XML_TAG_ASSOCIATION)) continue; 413 414 readAssociationV0(parser, userId, associationId++, out); 415 } 416 } 417 readAssociationV0(@onNull TypedXmlPullParser parser, @UserIdInt int userId, int associationId, @NonNull Collection<AssociationInfo> out)418 private static void readAssociationV0(@NonNull TypedXmlPullParser parser, @UserIdInt int userId, 419 int associationId, @NonNull Collection<AssociationInfo> out) 420 throws XmlPullParserException { 421 requireStartOfTag(parser, XML_TAG_ASSOCIATION); 422 423 final String appPackage = readStringAttribute(parser, XML_ATTR_PACKAGE); 424 final String deviceAddress = readStringAttribute(parser, LEGACY_XML_ATTR_DEVICE); 425 426 if (appPackage == null || deviceAddress == null) return; 427 428 final String profile = readStringAttribute(parser, XML_ATTR_PROFILE); 429 final boolean notify = readBooleanAttribute(parser, XML_ATTR_NOTIFY_DEVICE_NEARBY); 430 final long timeApproved = readLongAttribute(parser, XML_ATTR_TIME_APPROVED, 0L); 431 432 out.add(new AssociationInfo(associationId, userId, appPackage, 433 MacAddress.fromString(deviceAddress), null, profile, null, 434 /* managedByCompanionApp */ false, notify, /* revoked */ false, timeApproved, 435 Long.MAX_VALUE, /* systemDataSyncFlags */ 0)); 436 } 437 readAssociationsV1(@onNull TypedXmlPullParser parser, @UserIdInt int userId, @NonNull Collection<AssociationInfo> out)438 private static void readAssociationsV1(@NonNull TypedXmlPullParser parser, 439 @UserIdInt int userId, @NonNull Collection<AssociationInfo> out) 440 throws XmlPullParserException, IOException { 441 requireStartOfTag(parser, XML_TAG_ASSOCIATIONS); 442 443 while (true) { 444 parser.nextTag(); 445 if (isEndOfTag(parser, XML_TAG_ASSOCIATIONS)) break; 446 if (!isStartOfTag(parser, XML_TAG_ASSOCIATION)) continue; 447 448 readAssociationV1(parser, userId, out); 449 } 450 } 451 readAssociationV1(@onNull TypedXmlPullParser parser, @UserIdInt int userId, @NonNull Collection<AssociationInfo> out)452 private static void readAssociationV1(@NonNull TypedXmlPullParser parser, @UserIdInt int userId, 453 @NonNull Collection<AssociationInfo> out) throws XmlPullParserException, IOException { 454 requireStartOfTag(parser, XML_TAG_ASSOCIATION); 455 456 final int associationId = readIntAttribute(parser, XML_ATTR_ID); 457 final String profile = readStringAttribute(parser, XML_ATTR_PROFILE); 458 final String appPackage = readStringAttribute(parser, XML_ATTR_PACKAGE); 459 final MacAddress macAddress = stringToMacAddress( 460 readStringAttribute(parser, XML_ATTR_MAC_ADDRESS)); 461 final String displayName = readStringAttribute(parser, XML_ATTR_DISPLAY_NAME); 462 final boolean selfManaged = readBooleanAttribute(parser, XML_ATTR_SELF_MANAGED); 463 final boolean notify = readBooleanAttribute(parser, XML_ATTR_NOTIFY_DEVICE_NEARBY); 464 final boolean revoked = readBooleanAttribute(parser, XML_ATTR_REVOKED, false); 465 final long timeApproved = readLongAttribute(parser, XML_ATTR_TIME_APPROVED, 0L); 466 final long lastTimeConnected = readLongAttribute( 467 parser, XML_ATTR_LAST_TIME_CONNECTED, Long.MAX_VALUE); 468 final int systemDataSyncFlags = readIntAttribute(parser, 469 XML_ATTR_SYSTEM_DATA_SYNC_FLAGS, 0); 470 471 final AssociationInfo associationInfo = createAssociationInfoNoThrow(associationId, userId, 472 appPackage, macAddress, displayName, profile, selfManaged, notify, revoked, 473 timeApproved, lastTimeConnected, systemDataSyncFlags); 474 if (associationInfo != null) { 475 out.add(associationInfo); 476 } 477 } 478 readPreviouslyUsedIdsV1(@onNull TypedXmlPullParser parser, @NonNull Map<String, Set<Integer>> out)479 private static void readPreviouslyUsedIdsV1(@NonNull TypedXmlPullParser parser, 480 @NonNull Map<String, Set<Integer>> out) throws XmlPullParserException, IOException { 481 requireStartOfTag(parser, XML_TAG_PREVIOUSLY_USED_IDS); 482 483 while (true) { 484 parser.nextTag(); 485 if (isEndOfTag(parser, XML_TAG_PREVIOUSLY_USED_IDS)) break; 486 if (!isStartOfTag(parser, XML_TAG_PACKAGE)) continue; 487 488 final String packageName = readStringAttribute(parser, XML_ATTR_PACKAGE_NAME); 489 final Set<Integer> usedIds = new HashSet<>(); 490 491 while (true) { 492 parser.nextTag(); 493 if (isEndOfTag(parser, XML_TAG_PACKAGE)) break; 494 if (!isStartOfTag(parser, XML_TAG_ID)) continue; 495 496 parser.nextToken(); 497 final int id = Integer.parseInt(parser.getText()); 498 usedIds.add(id); 499 } 500 501 out.put(packageName, usedIds); 502 } 503 } 504 writeAssociations(@onNull XmlSerializer parent, @Nullable Collection<AssociationInfo> associations)505 private static void writeAssociations(@NonNull XmlSerializer parent, 506 @Nullable Collection<AssociationInfo> associations) throws IOException { 507 final XmlSerializer serializer = parent.startTag(null, XML_TAG_ASSOCIATIONS); 508 for (AssociationInfo association : associations) { 509 writeAssociation(serializer, association); 510 } 511 serializer.endTag(null, XML_TAG_ASSOCIATIONS); 512 } 513 writeAssociation(@onNull XmlSerializer parent, @NonNull AssociationInfo a)514 private static void writeAssociation(@NonNull XmlSerializer parent, @NonNull AssociationInfo a) 515 throws IOException { 516 final XmlSerializer serializer = parent.startTag(null, XML_TAG_ASSOCIATION); 517 518 writeIntAttribute(serializer, XML_ATTR_ID, a.getId()); 519 writeStringAttribute(serializer, XML_ATTR_PROFILE, a.getDeviceProfile()); 520 writeStringAttribute(serializer, XML_ATTR_PACKAGE, a.getPackageName()); 521 writeStringAttribute(serializer, XML_ATTR_MAC_ADDRESS, a.getDeviceMacAddressAsString()); 522 writeStringAttribute(serializer, XML_ATTR_DISPLAY_NAME, a.getDisplayName()); 523 writeBooleanAttribute(serializer, XML_ATTR_SELF_MANAGED, a.isSelfManaged()); 524 writeBooleanAttribute( 525 serializer, XML_ATTR_NOTIFY_DEVICE_NEARBY, a.isNotifyOnDeviceNearby()); 526 writeBooleanAttribute( 527 serializer, XML_ATTR_REVOKED, a.isRevoked()); 528 writeLongAttribute(serializer, XML_ATTR_TIME_APPROVED, a.getTimeApprovedMs()); 529 writeLongAttribute( 530 serializer, XML_ATTR_LAST_TIME_CONNECTED, a.getLastTimeConnectedMs()); 531 writeIntAttribute(serializer, XML_ATTR_SYSTEM_DATA_SYNC_FLAGS, a.getSystemDataSyncFlags()); 532 533 serializer.endTag(null, XML_TAG_ASSOCIATION); 534 } 535 writePreviouslyUsedIds(@onNull XmlSerializer parent, @NonNull Map<String, Set<Integer>> previouslyUsedIdsPerPackage)536 private static void writePreviouslyUsedIds(@NonNull XmlSerializer parent, 537 @NonNull Map<String, Set<Integer>> previouslyUsedIdsPerPackage) throws IOException { 538 final XmlSerializer serializer = parent.startTag(null, XML_TAG_PREVIOUSLY_USED_IDS); 539 for (Map.Entry<String, Set<Integer>> entry : previouslyUsedIdsPerPackage.entrySet()) { 540 writePreviouslyUsedIdsForPackage(serializer, entry.getKey(), entry.getValue()); 541 } 542 serializer.endTag(null, XML_TAG_PREVIOUSLY_USED_IDS); 543 } 544 writePreviouslyUsedIdsForPackage(@onNull XmlSerializer parent, @NonNull String packageName, @NonNull Set<Integer> previouslyUsedIds)545 private static void writePreviouslyUsedIdsForPackage(@NonNull XmlSerializer parent, 546 @NonNull String packageName, @NonNull Set<Integer> previouslyUsedIds) 547 throws IOException { 548 final XmlSerializer serializer = parent.startTag(null, XML_TAG_PACKAGE); 549 writeStringAttribute(serializer, XML_ATTR_PACKAGE_NAME, packageName); 550 forEach(previouslyUsedIds, id -> serializer.startTag(null, XML_TAG_ID) 551 .text(Integer.toString(id)) 552 .endTag(null, XML_TAG_ID)); 553 serializer.endTag(null, XML_TAG_PACKAGE); 554 } 555 requireStartOfTag(@onNull XmlPullParser parser, @NonNull String tag)556 private static void requireStartOfTag(@NonNull XmlPullParser parser, @NonNull String tag) 557 throws XmlPullParserException { 558 if (isStartOfTag(parser, tag)) return; 559 throw new XmlPullParserException( 560 "Should be at the start of \"" + XML_TAG_ASSOCIATIONS + "\" tag"); 561 } 562 stringToMacAddress(@ullable String address)563 private static @Nullable MacAddress stringToMacAddress(@Nullable String address) { 564 return address != null ? MacAddress.fromString(address) : null; 565 } 566 createAssociationInfoNoThrow(int associationId, @UserIdInt int userId, @NonNull String appPackage, @Nullable MacAddress macAddress, @Nullable CharSequence displayName, @Nullable String profile, boolean selfManaged, boolean notify, boolean revoked, long timeApproved, long lastTimeConnected, int systemDataSyncFlags)567 private static AssociationInfo createAssociationInfoNoThrow(int associationId, 568 @UserIdInt int userId, @NonNull String appPackage, @Nullable MacAddress macAddress, 569 @Nullable CharSequence displayName, @Nullable String profile, boolean selfManaged, 570 boolean notify, boolean revoked, long timeApproved, long lastTimeConnected, 571 int systemDataSyncFlags) { 572 AssociationInfo associationInfo = null; 573 try { 574 // We do not persist AssociatedDevice, which means that AssociationInfo retrieved from 575 // datastore is not guaranteed to be identical to the one from initial association. 576 associationInfo = new AssociationInfo(associationId, userId, appPackage, macAddress, 577 displayName, profile, null, selfManaged, notify, revoked, 578 timeApproved, lastTimeConnected, systemDataSyncFlags); 579 } catch (Exception e) { 580 if (DEBUG) Log.w(TAG, "Could not create AssociationInfo", e); 581 } 582 return associationInfo; 583 } 584 } 585