• 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;
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