• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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