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.TypedXmlPullParser; 49 import android.util.TypedXmlSerializer; 50 import android.util.Xml; 51 52 import com.android.internal.util.XmlUtils; 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 * 137 * <association 138 * id="3" 139 * profile="android.app.role.COMPANION_DEVICE_WATCH" 140 * package="com.sample.companion.another.app" 141 * display_name="Jhon's Chromebook" 142 * self_managed="true" 143 * notify_device_nearby="false" 144 * revoked="false" 145 * last_time_connected="1634641160229" 146 * time_approved="1634641160229"/> 147 * </associations> 148 * 149 * <previously-used-ids> 150 * <package package_name="com.sample.companion.app"> 151 * <id>2</id> 152 * </package> 153 * </previously-used-ids> 154 * </state> 155 * }</pre> 156 */ 157 @SuppressLint("LongLogTag") 158 final class PersistentDataStore { 159 private static final String TAG = "CompanionDevice_PersistentDataStore"; 160 private static final boolean DEBUG = CompanionDeviceManagerService.DEBUG; 161 162 private static final int CURRENT_PERSISTENCE_VERSION = 1; 163 164 private static final String FILE_NAME_LEGACY = "companion_device_manager_associations.xml"; 165 private static final String FILE_NAME = "companion_device_manager.xml"; 166 167 private static final String XML_TAG_STATE = "state"; 168 private static final String XML_TAG_ASSOCIATIONS = "associations"; 169 private static final String XML_TAG_ASSOCIATION = "association"; 170 private static final String XML_TAG_PREVIOUSLY_USED_IDS = "previously-used-ids"; 171 private static final String XML_TAG_PACKAGE = "package"; 172 private static final String XML_TAG_ID = "id"; 173 174 private static final String XML_ATTR_PERSISTENCE_VERSION = "persistence-version"; 175 private static final String XML_ATTR_ID = "id"; 176 // Used in <package> elements, nested within <previously-used-ids> elements. 177 private static final String XML_ATTR_PACKAGE_NAME = "package_name"; 178 // Used in <association> elements, nested within <associations> elements. 179 private static final String XML_ATTR_PACKAGE = "package"; 180 private static final String XML_ATTR_MAC_ADDRESS = "mac_address"; 181 private static final String XML_ATTR_DISPLAY_NAME = "display_name"; 182 private static final String XML_ATTR_PROFILE = "profile"; 183 private static final String XML_ATTR_SELF_MANAGED = "self_managed"; 184 private static final String XML_ATTR_NOTIFY_DEVICE_NEARBY = "notify_device_nearby"; 185 private static final String XML_ATTR_REVOKED = "revoked"; 186 private static final String XML_ATTR_TIME_APPROVED = "time_approved"; 187 private static final String XML_ATTR_LAST_TIME_CONNECTED = "last_time_connected"; 188 189 private static final String LEGACY_XML_ATTR_DEVICE = "device"; 190 191 private final @NonNull ConcurrentMap<Integer, AtomicFile> mUserIdToStorageFile = 192 new ConcurrentHashMap<>(); 193 readStateForUsers(@onNull List<UserInfo> users, @NonNull Set<AssociationInfo> allAssociationsOut, @NonNull SparseArray<Map<String, Set<Integer>>> previouslyUsedIdsPerUserOut)194 void readStateForUsers(@NonNull List<UserInfo> users, 195 @NonNull Set<AssociationInfo> allAssociationsOut, 196 @NonNull SparseArray<Map<String, Set<Integer>>> previouslyUsedIdsPerUserOut) { 197 for (UserInfo user : users) { 198 final int userId = user.id; 199 // Previously used IDs are stored in the "out" collection per-user. 200 final Map<String, Set<Integer>> previouslyUsedIds = new ArrayMap<>(); 201 202 // Associations for all users are stored in a single "flat" set: so we read directly 203 // into it. 204 final Set<AssociationInfo> associationsForUser = new HashSet<>(); 205 readStateForUser(userId, associationsForUser, previouslyUsedIds); 206 207 // Go through all the associations for the user and check if their IDs are within 208 // the allowed range (for the user). 209 final int firstAllowedId = getFirstAssociationIdForUser(userId); 210 final int lastAllowedId = getLastAssociationIdForUser(userId); 211 for (AssociationInfo association : associationsForUser) { 212 final int id = association.getId(); 213 if (id < firstAllowedId || id > lastAllowedId) { 214 Slog.e(TAG, "Wrong association ID assignment: " + id + ". " 215 + "Association belongs to u" + userId + " and thus its ID should be " 216 + "within [" + firstAllowedId + ", " + lastAllowedId + "] range."); 217 // TODO(b/224736262): try fixing (re-assigning) the ID? 218 } 219 } 220 221 // Add user's association to the "output" set. 222 allAssociationsOut.addAll(associationsForUser); 223 224 // Save previously used IDs for this user into the "out" structure. 225 previouslyUsedIdsPerUserOut.append(userId, previouslyUsedIds); 226 } 227 } 228 229 /** 230 * Reads previously persisted data for the given user "into" the provided containers. 231 * 232 * @param userId Android UserID 233 * @param associationsOut a container to read the {@link AssociationInfo}s "into". 234 * @param previouslyUsedIdsPerPackageOut a container to read the used IDs "into". 235 */ readStateForUser(@serIdInt int userId, @NonNull Collection<AssociationInfo> associationsOut, @NonNull Map<String, Set<Integer>> previouslyUsedIdsPerPackageOut)236 void readStateForUser(@UserIdInt int userId, 237 @NonNull Collection<AssociationInfo> associationsOut, 238 @NonNull Map<String, Set<Integer>> previouslyUsedIdsPerPackageOut) { 239 Slog.i(TAG, "Reading associations for user " + userId + " from disk"); 240 final AtomicFile file = getStorageFileForUser(userId); 241 if (DEBUG) Log.d(TAG, " > File=" + file.getBaseFile().getPath()); 242 243 // getStorageFileForUser() ALWAYS returns the SAME OBJECT, which allows us to synchronize 244 // accesses to the file on the file system using this AtomicFile object. 245 synchronized (file) { 246 File legacyBaseFile = null; 247 final AtomicFile readFrom; 248 final String rootTag; 249 if (!file.getBaseFile().exists()) { 250 if (DEBUG) Log.d(TAG, " > File does not exist -> Try to read legacy file"); 251 252 legacyBaseFile = getBaseLegacyStorageFileForUser(userId); 253 if (DEBUG) Log.d(TAG, " > Legacy file=" + legacyBaseFile.getPath()); 254 if (!legacyBaseFile.exists()) { 255 if (DEBUG) Log.d(TAG, " > Legacy file does not exist -> Abort"); 256 return; 257 } 258 259 readFrom = new AtomicFile(legacyBaseFile); 260 rootTag = XML_TAG_ASSOCIATIONS; 261 } else { 262 readFrom = file; 263 rootTag = XML_TAG_STATE; 264 } 265 266 if (DEBUG) Log.d(TAG, " > Reading associations..."); 267 final int version = readStateFromFileLocked(userId, readFrom, rootTag, 268 associationsOut, previouslyUsedIdsPerPackageOut); 269 if (DEBUG) { 270 Log.d(TAG, " > Done reading: " + associationsOut); 271 if (version < CURRENT_PERSISTENCE_VERSION) { 272 Log.d(TAG, " > File used old format: v." + version + " -> Re-write"); 273 } 274 } 275 276 if (legacyBaseFile != null || version < CURRENT_PERSISTENCE_VERSION) { 277 // The data is either in the legacy file or in the legacy format, or both. 278 // Save the data to right file in using the current format. 279 if (DEBUG) { 280 Log.d(TAG, " > Writing the data to " + file.getBaseFile().getPath()); 281 } 282 persistStateToFileLocked(file, associationsOut, previouslyUsedIdsPerPackageOut); 283 284 if (legacyBaseFile != null) { 285 // We saved the data to the right file, can delete the old file now. 286 if (DEBUG) Log.d(TAG, " > Deleting legacy file"); 287 legacyBaseFile.delete(); 288 } 289 } 290 } 291 } 292 293 /** 294 * Persisted data to the disk. 295 * 296 * @param userId Android UserID 297 * @param associations a set of user's associations. 298 * @param previouslyUsedIdsPerPackage a set previously used Association IDs for the user. 299 */ persistStateForUser(@serIdInt int userId, @NonNull Collection<AssociationInfo> associations, @NonNull Map<String, Set<Integer>> previouslyUsedIdsPerPackage)300 void persistStateForUser(@UserIdInt int userId, 301 @NonNull Collection<AssociationInfo> associations, 302 @NonNull Map<String, Set<Integer>> previouslyUsedIdsPerPackage) { 303 Slog.i(TAG, "Writing associations for user " + userId + " to disk"); 304 if (DEBUG) Slog.d(TAG, " > " + associations); 305 306 final AtomicFile file = getStorageFileForUser(userId); 307 if (DEBUG) Log.d(TAG, " > File=" + file.getBaseFile().getPath()); 308 // getStorageFileForUser() ALWAYS returns the SAME OBJECT, which allows us to synchronize 309 // accesses to the file on the file system using this AtomicFile object. 310 synchronized (file) { 311 persistStateToFileLocked(file, associations, previouslyUsedIdsPerPackage); 312 } 313 } 314 readStateFromFileLocked(@serIdInt int userId, @NonNull AtomicFile file, @NonNull String rootTag, @Nullable Collection<AssociationInfo> associationsOut, @NonNull Map<String, Set<Integer>> previouslyUsedIdsPerPackageOut)315 private int readStateFromFileLocked(@UserIdInt int userId, @NonNull AtomicFile file, 316 @NonNull String rootTag, @Nullable Collection<AssociationInfo> associationsOut, 317 @NonNull Map<String, Set<Integer>> previouslyUsedIdsPerPackageOut) { 318 try (FileInputStream in = file.openRead()) { 319 final TypedXmlPullParser parser = Xml.resolvePullParser(in); 320 321 XmlUtils.beginDocument(parser, rootTag); 322 final int version = readIntAttribute(parser, XML_ATTR_PERSISTENCE_VERSION, 0); 323 switch (version) { 324 case 0: 325 readAssociationsV0(parser, userId, associationsOut); 326 break; 327 case 1: 328 while (true) { 329 parser.nextTag(); 330 if (isStartOfTag(parser, XML_TAG_ASSOCIATIONS)) { 331 readAssociationsV1(parser, userId, associationsOut); 332 } else if (isStartOfTag(parser, XML_TAG_PREVIOUSLY_USED_IDS)) { 333 readPreviouslyUsedIdsV1(parser, previouslyUsedIdsPerPackageOut); 334 } else if (isEndOfTag(parser, rootTag)) { 335 break; 336 } 337 } 338 break; 339 } 340 return version; 341 } catch (XmlPullParserException | IOException e) { 342 Slog.e(TAG, "Error while reading associations file", e); 343 return -1; 344 } 345 } 346 persistStateToFileLocked(@onNull AtomicFile file, @Nullable Collection<AssociationInfo> associations, @NonNull Map<String, Set<Integer>> previouslyUsedIdsPerPackage)347 private void persistStateToFileLocked(@NonNull AtomicFile file, 348 @Nullable Collection<AssociationInfo> associations, 349 @NonNull Map<String, Set<Integer>> previouslyUsedIdsPerPackage) { 350 // Writing to file could fail, for example, if the user has been recently removed and so was 351 // their DE (/data/system_de/<user-id>/) directory. 352 writeToFileSafely(file, out -> { 353 final TypedXmlSerializer serializer = Xml.resolveSerializer(out); 354 serializer.setFeature( 355 "http://xmlpull.org/v1/doc/features.html#indent-output", true); 356 357 serializer.startDocument(null, true); 358 serializer.startTag(null, XML_TAG_STATE); 359 writeIntAttribute(serializer, 360 XML_ATTR_PERSISTENCE_VERSION, CURRENT_PERSISTENCE_VERSION); 361 362 writeAssociations(serializer, associations); 363 writePreviouslyUsedIds(serializer, previouslyUsedIdsPerPackage); 364 365 serializer.endTag(null, XML_TAG_STATE); 366 serializer.endDocument(); 367 }); 368 } 369 370 /** 371 * Creates and caches {@link AtomicFile} object that represents the back-up file for the given 372 * user. 373 * 374 * IMPORTANT: the method will ALWAYS return the same {@link AtomicFile} object, which makes it 375 * possible to synchronize reads and writes to the file using the returned object. 376 */ getStorageFileForUser(@serIdInt int userId)377 private @NonNull AtomicFile getStorageFileForUser(@UserIdInt int userId) { 378 return mUserIdToStorageFile.computeIfAbsent(userId, 379 u -> createStorageFileForUser(userId, FILE_NAME)); 380 } 381 getBaseLegacyStorageFileForUser(@serIdInt int userId)382 private static @NonNull File getBaseLegacyStorageFileForUser(@UserIdInt int userId) { 383 return new File(Environment.getUserSystemDirectory(userId), FILE_NAME_LEGACY); 384 } 385 readAssociationsV0(@onNull TypedXmlPullParser parser, @UserIdInt int userId, @NonNull Collection<AssociationInfo> out)386 private static void readAssociationsV0(@NonNull TypedXmlPullParser parser, 387 @UserIdInt int userId, @NonNull Collection<AssociationInfo> out) 388 throws XmlPullParserException, IOException { 389 requireStartOfTag(parser, XML_TAG_ASSOCIATIONS); 390 391 // Before Android T Associations didn't have IDs, so when we are upgrading from S (reading 392 // from V0) we need to generate and assign IDs to the existing Associations. 393 // It's safe to do it here, because CDM cannot create new Associations before it reads 394 // existing ones from the backup files. And the fact that we are reading from a V0 file, 395 // means that CDM hasn't assigned any IDs yet, so we can just start from the first available 396 // id for each user (eg. 1 for user 0; 100 001 - for user 1; 200 001 - for user 2; etc). 397 int associationId = getFirstAssociationIdForUser(userId); 398 while (true) { 399 parser.nextTag(); 400 if (isEndOfTag(parser, XML_TAG_ASSOCIATIONS)) break; 401 if (!isStartOfTag(parser, XML_TAG_ASSOCIATION)) continue; 402 403 readAssociationV0(parser, userId, associationId++, out); 404 } 405 } 406 readAssociationV0(@onNull TypedXmlPullParser parser, @UserIdInt int userId, int associationId, @NonNull Collection<AssociationInfo> out)407 private static void readAssociationV0(@NonNull TypedXmlPullParser parser, @UserIdInt int userId, 408 int associationId, @NonNull Collection<AssociationInfo> out) 409 throws XmlPullParserException { 410 requireStartOfTag(parser, XML_TAG_ASSOCIATION); 411 412 final String appPackage = readStringAttribute(parser, XML_ATTR_PACKAGE); 413 final String deviceAddress = readStringAttribute(parser, LEGACY_XML_ATTR_DEVICE); 414 415 if (appPackage == null || deviceAddress == null) return; 416 417 final String profile = readStringAttribute(parser, XML_ATTR_PROFILE); 418 final boolean notify = readBooleanAttribute(parser, XML_ATTR_NOTIFY_DEVICE_NEARBY); 419 final long timeApproved = readLongAttribute(parser, XML_ATTR_TIME_APPROVED, 0L); 420 421 out.add(new AssociationInfo(associationId, userId, appPackage, 422 MacAddress.fromString(deviceAddress), null, profile, 423 /* managedByCompanionApp */ false, notify, /* revoked */ false, timeApproved, 424 Long.MAX_VALUE)); 425 } 426 readAssociationsV1(@onNull TypedXmlPullParser parser, @UserIdInt int userId, @NonNull Collection<AssociationInfo> out)427 private static void readAssociationsV1(@NonNull TypedXmlPullParser parser, 428 @UserIdInt int userId, @NonNull Collection<AssociationInfo> out) 429 throws XmlPullParserException, IOException { 430 requireStartOfTag(parser, XML_TAG_ASSOCIATIONS); 431 432 while (true) { 433 parser.nextTag(); 434 if (isEndOfTag(parser, XML_TAG_ASSOCIATIONS)) break; 435 if (!isStartOfTag(parser, XML_TAG_ASSOCIATION)) continue; 436 437 readAssociationV1(parser, userId, out); 438 } 439 } 440 readAssociationV1(@onNull TypedXmlPullParser parser, @UserIdInt int userId, @NonNull Collection<AssociationInfo> out)441 private static void readAssociationV1(@NonNull TypedXmlPullParser parser, @UserIdInt int userId, 442 @NonNull Collection<AssociationInfo> out) throws XmlPullParserException, IOException { 443 requireStartOfTag(parser, XML_TAG_ASSOCIATION); 444 445 final int associationId = readIntAttribute(parser, XML_ATTR_ID); 446 final String profile = readStringAttribute(parser, XML_ATTR_PROFILE); 447 final String appPackage = readStringAttribute(parser, XML_ATTR_PACKAGE); 448 final MacAddress macAddress = stringToMacAddress( 449 readStringAttribute(parser, XML_ATTR_MAC_ADDRESS)); 450 final String displayName = readStringAttribute(parser, XML_ATTR_DISPLAY_NAME); 451 final boolean selfManaged = readBooleanAttribute(parser, XML_ATTR_SELF_MANAGED); 452 final boolean notify = readBooleanAttribute(parser, XML_ATTR_NOTIFY_DEVICE_NEARBY); 453 final boolean revoked = readBooleanAttribute(parser, XML_ATTR_REVOKED, false); 454 final long timeApproved = readLongAttribute(parser, XML_ATTR_TIME_APPROVED, 0L); 455 final long lastTimeConnected = readLongAttribute( 456 parser, XML_ATTR_LAST_TIME_CONNECTED, Long.MAX_VALUE); 457 458 final AssociationInfo associationInfo = createAssociationInfoNoThrow(associationId, userId, 459 appPackage, macAddress, displayName, profile, selfManaged, notify, revoked, 460 timeApproved, lastTimeConnected); 461 if (associationInfo != null) { 462 out.add(associationInfo); 463 } 464 } 465 readPreviouslyUsedIdsV1(@onNull TypedXmlPullParser parser, @NonNull Map<String, Set<Integer>> out)466 private static void readPreviouslyUsedIdsV1(@NonNull TypedXmlPullParser parser, 467 @NonNull Map<String, Set<Integer>> out) throws XmlPullParserException, IOException { 468 requireStartOfTag(parser, XML_TAG_PREVIOUSLY_USED_IDS); 469 470 while (true) { 471 parser.nextTag(); 472 if (isEndOfTag(parser, XML_TAG_PREVIOUSLY_USED_IDS)) break; 473 if (!isStartOfTag(parser, XML_TAG_PACKAGE)) continue; 474 475 final String packageName = readStringAttribute(parser, XML_ATTR_PACKAGE_NAME); 476 final Set<Integer> usedIds = new HashSet<>(); 477 478 while (true) { 479 parser.nextTag(); 480 if (isEndOfTag(parser, XML_TAG_PACKAGE)) break; 481 if (!isStartOfTag(parser, XML_TAG_ID)) continue; 482 483 parser.nextToken(); 484 final int id = Integer.parseInt(parser.getText()); 485 usedIds.add(id); 486 } 487 488 out.put(packageName, usedIds); 489 } 490 } 491 writeAssociations(@onNull XmlSerializer parent, @Nullable Collection<AssociationInfo> associations)492 private static void writeAssociations(@NonNull XmlSerializer parent, 493 @Nullable Collection<AssociationInfo> associations) throws IOException { 494 final XmlSerializer serializer = parent.startTag(null, XML_TAG_ASSOCIATIONS); 495 for (AssociationInfo association : associations) { 496 writeAssociation(serializer, association); 497 } 498 serializer.endTag(null, XML_TAG_ASSOCIATIONS); 499 } 500 writeAssociation(@onNull XmlSerializer parent, @NonNull AssociationInfo a)501 private static void writeAssociation(@NonNull XmlSerializer parent, @NonNull AssociationInfo a) 502 throws IOException { 503 final XmlSerializer serializer = parent.startTag(null, XML_TAG_ASSOCIATION); 504 505 writeIntAttribute(serializer, XML_ATTR_ID, a.getId()); 506 writeStringAttribute(serializer, XML_ATTR_PROFILE, a.getDeviceProfile()); 507 writeStringAttribute(serializer, XML_ATTR_PACKAGE, a.getPackageName()); 508 writeStringAttribute(serializer, XML_ATTR_MAC_ADDRESS, a.getDeviceMacAddressAsString()); 509 writeStringAttribute(serializer, XML_ATTR_DISPLAY_NAME, a.getDisplayName()); 510 writeBooleanAttribute(serializer, XML_ATTR_SELF_MANAGED, a.isSelfManaged()); 511 writeBooleanAttribute( 512 serializer, XML_ATTR_NOTIFY_DEVICE_NEARBY, a.isNotifyOnDeviceNearby()); 513 writeBooleanAttribute( 514 serializer, XML_ATTR_REVOKED, a.isRevoked()); 515 writeLongAttribute(serializer, XML_ATTR_TIME_APPROVED, a.getTimeApprovedMs()); 516 writeLongAttribute( 517 serializer, XML_ATTR_LAST_TIME_CONNECTED, a.getLastTimeConnectedMs()); 518 519 serializer.endTag(null, XML_TAG_ASSOCIATION); 520 } 521 writePreviouslyUsedIds(@onNull XmlSerializer parent, @NonNull Map<String, Set<Integer>> previouslyUsedIdsPerPackage)522 private static void writePreviouslyUsedIds(@NonNull XmlSerializer parent, 523 @NonNull Map<String, Set<Integer>> previouslyUsedIdsPerPackage) throws IOException { 524 final XmlSerializer serializer = parent.startTag(null, XML_TAG_PREVIOUSLY_USED_IDS); 525 for (Map.Entry<String, Set<Integer>> entry : previouslyUsedIdsPerPackage.entrySet()) { 526 writePreviouslyUsedIdsForPackage(serializer, entry.getKey(), entry.getValue()); 527 } 528 serializer.endTag(null, XML_TAG_PREVIOUSLY_USED_IDS); 529 } 530 writePreviouslyUsedIdsForPackage(@onNull XmlSerializer parent, @NonNull String packageName, @NonNull Set<Integer> previouslyUsedIds)531 private static void writePreviouslyUsedIdsForPackage(@NonNull XmlSerializer parent, 532 @NonNull String packageName, @NonNull Set<Integer> previouslyUsedIds) 533 throws IOException { 534 final XmlSerializer serializer = parent.startTag(null, XML_TAG_PACKAGE); 535 writeStringAttribute(serializer, XML_ATTR_PACKAGE_NAME, packageName); 536 forEach(previouslyUsedIds, id -> serializer.startTag(null, XML_TAG_ID) 537 .text(Integer.toString(id)) 538 .endTag(null, XML_TAG_ID)); 539 serializer.endTag(null, XML_TAG_PACKAGE); 540 } 541 requireStartOfTag(@onNull XmlPullParser parser, @NonNull String tag)542 private static void requireStartOfTag(@NonNull XmlPullParser parser, @NonNull String tag) 543 throws XmlPullParserException { 544 if (isStartOfTag(parser, tag)) return; 545 throw new XmlPullParserException( 546 "Should be at the start of \"" + XML_TAG_ASSOCIATIONS + "\" tag"); 547 } 548 stringToMacAddress(@ullable String address)549 private static @Nullable MacAddress stringToMacAddress(@Nullable String address) { 550 return address != null ? MacAddress.fromString(address) : null; 551 } 552 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)553 private static AssociationInfo createAssociationInfoNoThrow(int associationId, 554 @UserIdInt int userId, @NonNull String appPackage, @Nullable MacAddress macAddress, 555 @Nullable CharSequence displayName, @Nullable String profile, boolean selfManaged, 556 boolean notify, boolean revoked, long timeApproved, long lastTimeConnected) { 557 AssociationInfo associationInfo = null; 558 try { 559 associationInfo = new AssociationInfo(associationId, userId, appPackage, macAddress, 560 displayName, profile, selfManaged, notify, revoked, timeApproved, 561 lastTimeConnected); 562 } catch (Exception e) { 563 if (DEBUG) Log.w(TAG, "Could not create AssociationInfo", e); 564 } 565 return associationInfo; 566 } 567 } 568