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.association; 18 19 import static com.android.internal.util.XmlUtils.readBooleanAttribute; 20 import static com.android.internal.util.XmlUtils.readByteArrayAttribute; 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.writeByteArrayAttribute; 26 import static com.android.internal.util.XmlUtils.writeIntAttribute; 27 import static com.android.internal.util.XmlUtils.writeLongAttribute; 28 import static com.android.internal.util.XmlUtils.writeStringAttribute; 29 import static com.android.server.companion.utils.AssociationUtils.getFirstAssociationIdForUser; 30 import static com.android.server.companion.utils.DataStoreUtils.createStorageFileForUser; 31 import static com.android.server.companion.utils.DataStoreUtils.fileToByteArray; 32 import static com.android.server.companion.utils.DataStoreUtils.isEndOfTag; 33 import static com.android.server.companion.utils.DataStoreUtils.isStartOfTag; 34 import static com.android.server.companion.utils.DataStoreUtils.writeToFileSafely; 35 36 import android.annotation.NonNull; 37 import android.annotation.Nullable; 38 import android.annotation.SuppressLint; 39 import android.annotation.UserIdInt; 40 import android.companion.AssociationInfo; 41 import android.companion.DeviceId; 42 import android.graphics.drawable.Icon; 43 import android.net.MacAddress; 44 import android.os.Environment; 45 import android.util.AtomicFile; 46 import android.util.Slog; 47 import android.util.Xml; 48 49 import com.android.internal.util.XmlUtils; 50 import com.android.modules.utils.TypedXmlPullParser; 51 import com.android.modules.utils.TypedXmlSerializer; 52 53 import org.xmlpull.v1.XmlPullParser; 54 import org.xmlpull.v1.XmlPullParserException; 55 import org.xmlpull.v1.XmlSerializer; 56 57 import java.io.ByteArrayInputStream; 58 import java.io.ByteArrayOutputStream; 59 import java.io.File; 60 import java.io.FileInputStream; 61 import java.io.FileNotFoundException; 62 import java.io.IOException; 63 import java.io.InputStream; 64 import java.util.HashMap; 65 import java.util.List; 66 import java.util.Map; 67 import java.util.concurrent.ConcurrentHashMap; 68 import java.util.concurrent.ConcurrentMap; 69 70 /** 71 * IMPORTANT: This class should NOT be directly used except {@link AssociationStore} 72 * 73 * The class responsible for persisting Association records and other related information (such as 74 * previously used IDs) to a disk, and reading the data back from the disk. 75 * 76 * <p> 77 * Before Android T the data was stored in "companion_device_manager_associations.xml" file in 78 * {@link Environment#getUserSystemDirectory(int) /data/system/user/}. 79 * 80 * See {@link #getBaseLegacyStorageFileForUser(int) getBaseLegacyStorageFileForUser()}. 81 * 82 * <p> 83 * Before Android T the data was stored using the v0 schema. See: 84 * <ul> 85 * <li>{@link #readAssociationsV0(TypedXmlPullParser, int) readAssociationsV0()}. 86 * <li>{@link #readAssociationV0(TypedXmlPullParser, int, int) readAssociationV0()}. 87 * </ul> 88 * 89 * The following snippet is a sample of a file that is using v0 schema. 90 * <pre>{@code 91 * <associations> 92 * <association 93 * package="com.sample.companion.app" 94 * device="AA:BB:CC:DD:EE:00" 95 * time_approved="1634389553216" /> 96 * <association 97 * package="com.another.sample.companion.app" 98 * device="AA:BB:CC:DD:EE:01" 99 * profile="android.app.role.COMPANION_DEVICE_WATCH" 100 * notify_device_nearby="false" 101 * time_approved="1634389752662" /> 102 * </associations> 103 * }</pre> 104 * 105 * <p> 106 * Since Android T the data is stored to "companion_device_manager.xml" file in 107 * {@link Environment#getDataSystemDeDirectory(int) /data/system_de/}. 108 * 109 * <p> 110 * Since Android T the data is stored using the v1 schema. 111 * 112 * In the v1 schema, a list of the previously used IDs is stored along with the association 113 * records. 114 * 115 * V1 schema adds a new optional "display_name" attribute, and makes the "mac_address" attribute 116 * optional. 117 * <ul> 118 * <li> {@link #CURRENT_PERSISTENCE_VERSION} 119 * <li> {@link #readAssociationsV1(TypedXmlPullParser, int) readAssociationsV1()} 120 * <li> {@link #readAssociationV1(TypedXmlPullParser, int) readAssociationV1()} 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 max-id="3"> 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 * device_icon="device_icon"> 138 * <device_id 139 * custom_device_id="1234"/> 140 * </association> 141 * <association 142 * id="3" 143 * profile="android.app.role.COMPANION_DEVICE_WATCH" 144 * package="com.sample.companion.another.app" 145 * display_name="Jhon's Chromebook" 146 * self_managed="true" 147 * notify_device_nearby="false" 148 * revoked="false" 149 * last_time_connected="1634641160229" 150 * time_approved="1634641160229" 151 * system_data_sync_flags="1" 152 * device_icon="device_icon"> 153 * <device_id 154 * custom_device_id="1234"/> 155 * </association> 156 * </associations> 157 * </state> 158 * }</pre> 159 */ 160 @SuppressLint("LongLogTag") 161 public final class AssociationDiskStore { 162 private static final String TAG = "CDM_AssociationDiskStore"; 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 private static final String FILE_NAME_LAST_REMOVED_ASSOCIATION = "last_removed_association.txt"; 169 170 private static final String XML_TAG_STATE = "state"; 171 private static final String XML_TAG_ASSOCIATIONS = "associations"; 172 private static final String XML_TAG_ASSOCIATION = "association"; 173 private static final String XML_TAG_DEVICE_ID = "device_id"; 174 175 private static final String XML_ATTR_PERSISTENCE_VERSION = "persistence-version"; 176 private static final String XML_ATTR_MAX_ID = "max-id"; 177 private static final String XML_ATTR_ID = "id"; 178 private static final String XML_ATTR_PACKAGE = "package"; 179 private static final String XML_ATTR_MAC_ADDRESS = "mac_address"; 180 private static final String XML_ATTR_DISPLAY_NAME = "display_name"; 181 private static final String XML_ATTR_PROFILE = "profile"; 182 private static final String XML_ATTR_SELF_MANAGED = "self_managed"; 183 private static final String XML_ATTR_NOTIFY_DEVICE_NEARBY = "notify_device_nearby"; 184 private static final String XML_ATTR_REVOKED = "revoked"; 185 private static final String XML_ATTR_PENDING = "pending"; 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 private static final String XML_ATTR_SYSTEM_DATA_SYNC_FLAGS = "system_data_sync_flags"; 189 private static final String XML_ATTR_DEVICE_ICON = "device_icon"; 190 private static final String XML_ATTR_CUSTOM_DEVICE_ID = "custom_device_id"; 191 private static final String XML_ATTR_MAC_ADDRESS_DEVICE_ID = "mac_address_device_id"; 192 193 private static final String LEGACY_XML_ATTR_DEVICE = "device"; 194 195 private final @NonNull ConcurrentMap<Integer, AtomicFile> mUserIdToStorageFile = 196 new ConcurrentHashMap<>(); 197 198 /** 199 * Read all associations for given users 200 */ readAssociationsByUsers(@onNull List<Integer> userIds)201 public Map<Integer, Associations> readAssociationsByUsers(@NonNull List<Integer> userIds) { 202 Map<Integer, Associations> userToAssociationsMap = new HashMap<>(); 203 for (int userId : userIds) { 204 userToAssociationsMap.put(userId, readAssociationsByUser(userId)); 205 } 206 return userToAssociationsMap; 207 } 208 209 /** 210 * Reads previously persisted data for the given user "into" the provided containers. 211 * 212 * Note that {@link AssociationInfo#getAssociatedDevice()} will always be {@code null} after 213 * retrieval from this datastore because it is not persisted (by design). This means that 214 * persisted data is not guaranteed to be identical to the initial data that was stored at the 215 * time of association. 216 */ 217 @NonNull readAssociationsByUser(@serIdInt int userId)218 private Associations readAssociationsByUser(@UserIdInt int userId) { 219 Slog.i(TAG, "Reading associations for user " + userId + " from disk."); 220 final AtomicFile file = getStorageFileForUser(userId); 221 Associations associations; 222 223 // getStorageFileForUser() ALWAYS returns the SAME OBJECT, which allows us to synchronize 224 // accesses to the file on the file system using this AtomicFile object. 225 synchronized (file) { 226 File legacyBaseFile = null; 227 final AtomicFile readFrom; 228 final String rootTag; 229 if (!file.getBaseFile().exists()) { 230 legacyBaseFile = getBaseLegacyStorageFileForUser(userId); 231 if (!legacyBaseFile.exists()) { 232 return new Associations(); 233 } 234 235 readFrom = new AtomicFile(legacyBaseFile); 236 rootTag = XML_TAG_ASSOCIATIONS; 237 } else { 238 readFrom = file; 239 rootTag = XML_TAG_STATE; 240 } 241 242 associations = readAssociationsFromFile(userId, readFrom, rootTag); 243 244 if (legacyBaseFile != null || associations.getVersion() < CURRENT_PERSISTENCE_VERSION) { 245 // The data is either in the legacy file or in the legacy format, or both. 246 // Save the data to right file in using the current format. 247 writeAssociationsToFile(file, associations); 248 249 if (legacyBaseFile != null) { 250 // We saved the data to the right file, can delete the old file now. 251 legacyBaseFile.delete(); 252 } 253 } 254 } 255 return associations; 256 } 257 258 /** 259 * Write associations to disk for the user. 260 */ writeAssociationsForUser(@serIdInt int userId, @NonNull Associations associations)261 public void writeAssociationsForUser(@UserIdInt int userId, 262 @NonNull Associations associations) { 263 Slog.i(TAG, "Writing associations for user " + userId + " to disk"); 264 265 final AtomicFile file = getStorageFileForUser(userId); 266 // getStorageFileForUser() ALWAYS returns the SAME OBJECT, which allows us to synchronize 267 // accesses to the file on the file system using this AtomicFile object. 268 synchronized (file) { 269 writeAssociationsToFile(file, associations); 270 } 271 } 272 273 /** 274 * Read the last removed association from disk. 275 */ readLastRemovedAssociation(@serIdInt int userId)276 public String readLastRemovedAssociation(@UserIdInt int userId) { 277 final AtomicFile file = createStorageFileForUser( 278 userId, FILE_NAME_LAST_REMOVED_ASSOCIATION); 279 StringBuilder sb = new StringBuilder(); 280 int c; 281 try (FileInputStream fis = file.openRead()) { 282 while ((c = fis.read()) != -1) { 283 sb.append((char) c); 284 } 285 fis.close(); 286 return sb.toString(); 287 } catch (FileNotFoundException e) { 288 Slog.e(TAG, "File " + file + " for user=" + userId + " doesn't exist."); 289 return null; 290 } catch (IOException e) { 291 Slog.e(TAG, "Can't read file " + file + " for user=" + userId); 292 return null; 293 } 294 } 295 296 /** 297 * Write the last removed association to disk. 298 */ writeLastRemovedAssociation(AssociationInfo association, String reason)299 public void writeLastRemovedAssociation(AssociationInfo association, String reason) { 300 Slog.i(TAG, "Writing last removed association=" + association.getId() + " to disk..."); 301 302 // Remove indirect identifier i.e. Mac Address 303 AssociationInfo.Builder builder = new AssociationInfo.Builder(association) 304 .setDeviceMacAddress(null); 305 // Set a placeholder display name if it's null because Mac Address and display name can't be 306 // both null. 307 if (association.getDisplayName() == null) { 308 builder.setDisplayName(""); 309 } 310 AssociationInfo redactedAssociation = builder.build(); 311 312 final AtomicFile file = createStorageFileForUser( 313 redactedAssociation.getUserId(), FILE_NAME_LAST_REMOVED_ASSOCIATION); 314 writeToFileSafely(file, out -> { 315 out.write(String.valueOf(System.currentTimeMillis()).getBytes()); 316 out.write(' '); 317 out.write(reason.getBytes()); 318 out.write(' '); 319 out.write(redactedAssociation.toString().getBytes()); 320 }); 321 } 322 323 @NonNull readAssociationsFromFile(@serIdInt int userId, @NonNull AtomicFile file, @NonNull String rootTag)324 private static Associations readAssociationsFromFile(@UserIdInt int userId, 325 @NonNull AtomicFile file, @NonNull String rootTag) { 326 try (FileInputStream in = file.openRead()) { 327 return readAssociationsFromInputStream(userId, in, rootTag); 328 } catch (XmlPullParserException | IOException e) { 329 Slog.e(TAG, "Error while reading associations file", e); 330 return new Associations(); 331 } 332 } 333 334 @NonNull readAssociationsFromInputStream(@serIdInt int userId, @NonNull InputStream in, @NonNull String rootTag)335 private static Associations readAssociationsFromInputStream(@UserIdInt int userId, 336 @NonNull InputStream in, @NonNull String rootTag) 337 throws XmlPullParserException, IOException { 338 final TypedXmlPullParser parser = Xml.resolvePullParser(in); 339 XmlUtils.beginDocument(parser, rootTag); 340 341 final int version = readIntAttribute(parser, XML_ATTR_PERSISTENCE_VERSION, 0); 342 Associations associations = new Associations(); 343 344 switch (version) { 345 case 0: 346 associations = readAssociationsV0(parser, userId); 347 break; 348 case 1: 349 while (true) { 350 parser.nextTag(); 351 if (isStartOfTag(parser, XML_TAG_ASSOCIATIONS)) { 352 associations = readAssociationsV1(parser, userId); 353 } else if (isEndOfTag(parser, rootTag)) { 354 break; 355 } 356 } 357 break; 358 } 359 return associations; 360 } 361 writeAssociationsToFile(@onNull AtomicFile file, @NonNull Associations associations)362 private void writeAssociationsToFile(@NonNull AtomicFile file, 363 @NonNull Associations associations) { 364 // Writing to file could fail, for example, if the user has been recently removed and so was 365 // their DE (/data/system_de/<user-id>/) directory. 366 writeToFileSafely(file, out -> { 367 final TypedXmlSerializer serializer = Xml.resolveSerializer(out); 368 serializer.setFeature("http://xmlpull.org/v1/doc/features.html#indent-output", true); 369 serializer.startDocument(null, true); 370 serializer.startTag(null, XML_TAG_STATE); 371 writeIntAttribute(serializer, 372 XML_ATTR_PERSISTENCE_VERSION, CURRENT_PERSISTENCE_VERSION); 373 writeAssociations(serializer, associations); 374 serializer.endTag(null, XML_TAG_STATE); 375 serializer.endDocument(); 376 }); 377 } 378 379 /** 380 * Creates and caches {@link AtomicFile} object that represents the back-up file for the given 381 * user. 382 * 383 * IMPORTANT: the method will ALWAYS return the same {@link AtomicFile} object, which makes it 384 * possible to synchronize reads and writes to the file using the returned object. 385 */ 386 @NonNull getStorageFileForUser(@serIdInt int userId)387 private AtomicFile getStorageFileForUser(@UserIdInt int userId) { 388 return mUserIdToStorageFile.computeIfAbsent(userId, 389 u -> createStorageFileForUser(userId, FILE_NAME)); 390 } 391 392 /** 393 * Get associations backup payload from disk 394 */ getBackupPayload(@serIdInt int userId)395 public byte[] getBackupPayload(@UserIdInt int userId) { 396 Slog.i(TAG, "Fetching stored state data for user " + userId + " from disk"); 397 final AtomicFile file = getStorageFileForUser(userId); 398 399 synchronized (file) { 400 return fileToByteArray(file); 401 } 402 } 403 404 /** 405 * Convert payload to a set of associations 406 */ readAssociationsFromPayload(byte[] payload, @UserIdInt int userId)407 public static Associations readAssociationsFromPayload(byte[] payload, @UserIdInt int userId) { 408 try (ByteArrayInputStream in = new ByteArrayInputStream(payload)) { 409 return readAssociationsFromInputStream(userId, in, XML_TAG_STATE); 410 } catch (XmlPullParserException | IOException e) { 411 Slog.e(TAG, "Error while reading associations file", e); 412 return new Associations(); 413 } 414 } 415 getBaseLegacyStorageFileForUser(@serIdInt int userId)416 private static @NonNull File getBaseLegacyStorageFileForUser(@UserIdInt int userId) { 417 return new File(Environment.getUserSystemDirectory(userId), FILE_NAME_LEGACY); 418 } 419 readAssociationsV0(@onNull TypedXmlPullParser parser, @UserIdInt int userId)420 private static Associations readAssociationsV0(@NonNull TypedXmlPullParser parser, 421 @UserIdInt int userId) 422 throws XmlPullParserException, IOException { 423 requireStartOfTag(parser, XML_TAG_ASSOCIATIONS); 424 425 // Before Android T Associations didn't have IDs, so when we are upgrading from S (reading 426 // from V0) we need to generate and assign IDs to the existing Associations. 427 // It's safe to do it here, because CDM cannot create new Associations before it reads 428 // existing ones from the backup files. And the fact that we are reading from a V0 file, 429 // means that CDM hasn't assigned any IDs yet, so we can just start from the first available 430 // id for each user (eg. 1 for user 0; 100 001 - for user 1; 200 001 - for user 2; etc). 431 int associationId = getFirstAssociationIdForUser(userId); 432 Associations associations = new Associations(); 433 associations.setVersion(0); 434 435 while (true) { 436 parser.nextTag(); 437 if (isEndOfTag(parser, XML_TAG_ASSOCIATIONS)) break; 438 if (!isStartOfTag(parser, XML_TAG_ASSOCIATION)) continue; 439 440 associations.addAssociation(readAssociationV0(parser, userId, associationId++)); 441 } 442 443 associations.setMaxId(associationId - 1); 444 445 return associations; 446 } 447 readAssociationV0(@onNull TypedXmlPullParser parser, @UserIdInt int userId, int associationId)448 private static AssociationInfo readAssociationV0(@NonNull TypedXmlPullParser parser, 449 @UserIdInt int userId, int associationId) 450 throws XmlPullParserException { 451 requireStartOfTag(parser, XML_TAG_ASSOCIATION); 452 453 final String appPackage = readStringAttribute(parser, XML_ATTR_PACKAGE); 454 final String deviceAddress = readStringAttribute(parser, LEGACY_XML_ATTR_DEVICE); 455 final String profile = readStringAttribute(parser, XML_ATTR_PROFILE); 456 final boolean notify = readBooleanAttribute(parser, XML_ATTR_NOTIFY_DEVICE_NEARBY); 457 final long timeApproved = readLongAttribute(parser, XML_ATTR_TIME_APPROVED, 0L); 458 459 return new AssociationInfo(associationId, userId, appPackage, 460 MacAddress.fromString(deviceAddress), null, profile, null, 461 /* managedByCompanionApp */ false, notify, /* revoked */ false, /* pending */ false, 462 timeApproved, Long.MAX_VALUE, /* systemDataSyncFlags */ 0, /* deviceIcon */ null, 463 /* deviceId */ null); 464 } 465 readAssociationsV1(@onNull TypedXmlPullParser parser, @UserIdInt int userId)466 private static Associations readAssociationsV1(@NonNull TypedXmlPullParser parser, 467 @UserIdInt int userId) 468 throws XmlPullParserException, IOException { 469 requireStartOfTag(parser, XML_TAG_ASSOCIATIONS); 470 471 // For old builds that don't have max-id attr, 472 // default maxId to 0 and get the maxId out of all association ids. 473 int maxId = readIntAttribute(parser, XML_ATTR_MAX_ID, 0); 474 Associations associations = new Associations(); 475 associations.setVersion(1); 476 477 while (true) { 478 parser.nextTag(); 479 if (isEndOfTag(parser, XML_TAG_ASSOCIATIONS)) break; 480 if (!isStartOfTag(parser, XML_TAG_ASSOCIATION)) continue; 481 482 AssociationInfo association = readAssociationV1(parser, userId); 483 associations.addAssociation(association); 484 485 maxId = Math.max(maxId, association.getId()); 486 } 487 488 associations.setMaxId(maxId); 489 490 return associations; 491 } 492 readAssociationV1(@onNull TypedXmlPullParser parser, @UserIdInt int userId)493 private static AssociationInfo readAssociationV1(@NonNull TypedXmlPullParser parser, 494 @UserIdInt int userId) 495 throws XmlPullParserException, IOException { 496 requireStartOfTag(parser, XML_TAG_ASSOCIATION); 497 498 final int associationId = readIntAttribute(parser, XML_ATTR_ID); 499 final String profile = readStringAttribute(parser, XML_ATTR_PROFILE); 500 final String appPackage = readStringAttribute(parser, XML_ATTR_PACKAGE); 501 final MacAddress macAddress = stringToMacAddress( 502 readStringAttribute(parser, XML_ATTR_MAC_ADDRESS)); 503 final String displayName = readStringAttribute(parser, XML_ATTR_DISPLAY_NAME); 504 final boolean selfManaged = readBooleanAttribute(parser, XML_ATTR_SELF_MANAGED); 505 final boolean notify = readBooleanAttribute(parser, XML_ATTR_NOTIFY_DEVICE_NEARBY); 506 final boolean revoked = readBooleanAttribute(parser, XML_ATTR_REVOKED, false); 507 final boolean pending = readBooleanAttribute(parser, XML_ATTR_PENDING, false); 508 final long timeApproved = readLongAttribute(parser, XML_ATTR_TIME_APPROVED, 0L); 509 final long lastTimeConnected = readLongAttribute( 510 parser, XML_ATTR_LAST_TIME_CONNECTED, Long.MAX_VALUE); 511 final int systemDataSyncFlags = readIntAttribute(parser, 512 XML_ATTR_SYSTEM_DATA_SYNC_FLAGS, 0); 513 final Icon deviceIcon = byteArrayToIcon( 514 readByteArrayAttribute(parser, XML_ATTR_DEVICE_ICON)); 515 parser.nextTag(); 516 final DeviceId deviceId = readDeviceId(parser); 517 518 return new AssociationInfo(associationId, userId, appPackage, macAddress, displayName, 519 profile, null, selfManaged, notify, revoked, pending, timeApproved, 520 lastTimeConnected, systemDataSyncFlags, deviceIcon, deviceId); 521 } 522 readDeviceId(@onNull TypedXmlPullParser parser)523 private static DeviceId readDeviceId(@NonNull TypedXmlPullParser parser) 524 throws XmlPullParserException { 525 if (isStartOfTag(parser, XML_TAG_DEVICE_ID)) { 526 final String customDeviceId = readStringAttribute( 527 parser, XML_ATTR_CUSTOM_DEVICE_ID); 528 final MacAddress macAddress = stringToMacAddress( 529 readStringAttribute(parser, XML_ATTR_MAC_ADDRESS_DEVICE_ID)); 530 531 return new DeviceId(customDeviceId, macAddress); 532 } 533 534 return null; 535 } 536 writeAssociations(@onNull XmlSerializer parent, @NonNull Associations associations)537 private static void writeAssociations(@NonNull XmlSerializer parent, 538 @NonNull Associations associations) 539 throws IOException { 540 final XmlSerializer serializer = parent.startTag(null, XML_TAG_ASSOCIATIONS); 541 writeIntAttribute(serializer, XML_ATTR_MAX_ID, associations.getMaxId()); 542 for (AssociationInfo association : associations.getAssociations()) { 543 writeAssociation(serializer, association); 544 } 545 serializer.endTag(null, XML_TAG_ASSOCIATIONS); 546 } 547 writeAssociation(@onNull XmlSerializer parent, @NonNull AssociationInfo a)548 private static void writeAssociation(@NonNull XmlSerializer parent, @NonNull AssociationInfo a) 549 throws IOException { 550 final XmlSerializer serializer = parent.startTag(null, XML_TAG_ASSOCIATION); 551 552 writeIntAttribute(serializer, XML_ATTR_ID, a.getId()); 553 writeStringAttribute(serializer, XML_ATTR_PROFILE, a.getDeviceProfile()); 554 writeStringAttribute(serializer, XML_ATTR_PACKAGE, a.getPackageName()); 555 writeStringAttribute(serializer, XML_ATTR_MAC_ADDRESS, a.getDeviceMacAddressAsString()); 556 writeStringAttribute(serializer, XML_ATTR_DISPLAY_NAME, a.getDisplayName()); 557 writeBooleanAttribute(serializer, XML_ATTR_SELF_MANAGED, a.isSelfManaged()); 558 writeBooleanAttribute( 559 serializer, XML_ATTR_NOTIFY_DEVICE_NEARBY, a.isNotifyOnDeviceNearby()); 560 writeBooleanAttribute(serializer, XML_ATTR_REVOKED, a.isRevoked()); 561 writeBooleanAttribute(serializer, XML_ATTR_PENDING, a.isPending()); 562 writeLongAttribute(serializer, XML_ATTR_TIME_APPROVED, a.getTimeApprovedMs()); 563 writeLongAttribute( 564 serializer, XML_ATTR_LAST_TIME_CONNECTED, a.getLastTimeConnectedMs()); 565 writeIntAttribute(serializer, XML_ATTR_SYSTEM_DATA_SYNC_FLAGS, a.getSystemDataSyncFlags()); 566 writeByteArrayAttribute( 567 serializer, XML_ATTR_DEVICE_ICON, iconToByteArray(a.getDeviceIcon())); 568 569 writeDeviceId(serializer, a); 570 serializer.endTag(null, XML_TAG_ASSOCIATION); 571 } 572 writeDeviceId(XmlSerializer parent, @NonNull AssociationInfo a)573 private static void writeDeviceId(XmlSerializer parent, @NonNull AssociationInfo a) 574 throws IOException { 575 if (a.getDeviceId() != null) { 576 final XmlSerializer serializer = parent.startTag(null, XML_TAG_DEVICE_ID); 577 writeStringAttribute( 578 serializer, 579 XML_ATTR_CUSTOM_DEVICE_ID, 580 a.getDeviceId().getCustomId() 581 ); 582 writeStringAttribute( 583 serializer, 584 XML_ATTR_MAC_ADDRESS_DEVICE_ID, 585 a.getDeviceId().getMacAddressAsString() 586 ); 587 serializer.endTag(null, XML_TAG_DEVICE_ID); 588 } 589 } 590 requireStartOfTag(@onNull XmlPullParser parser, @NonNull String tag)591 private static void requireStartOfTag(@NonNull XmlPullParser parser, @NonNull String tag) 592 throws XmlPullParserException { 593 if (isStartOfTag(parser, tag)) return; 594 throw new XmlPullParserException( 595 "Should be at the start of \"" + tag + "\" tag"); 596 } 597 stringToMacAddress(@ullable String address)598 private static @Nullable MacAddress stringToMacAddress(@Nullable String address) { 599 return address != null ? MacAddress.fromString(address) : null; 600 } 601 iconToByteArray(Icon deviceIcon)602 private static byte[] iconToByteArray(Icon deviceIcon) 603 throws IOException { 604 if (deviceIcon == null) { 605 return null; 606 } 607 608 ByteArrayOutputStream byteStream = new ByteArrayOutputStream(); 609 deviceIcon.writeToStream(byteStream); 610 return byteStream.toByteArray(); 611 } 612 byteArrayToIcon(byte[] bytes)613 private static Icon byteArrayToIcon(byte[] bytes) throws IOException { 614 if (bytes == null) { 615 return null; 616 } 617 618 ByteArrayInputStream byteStream = new ByteArrayInputStream(bytes); 619 return Icon.createFromStream(byteStream); 620 } 621 } 622