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