• 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.readIntAttribute;
21 import static com.android.internal.util.XmlUtils.readLongAttribute;
22 import static com.android.internal.util.XmlUtils.readStringAttribute;
23 import static com.android.internal.util.XmlUtils.writeBooleanAttribute;
24 import static com.android.internal.util.XmlUtils.writeIntAttribute;
25 import static com.android.internal.util.XmlUtils.writeLongAttribute;
26 import static com.android.internal.util.XmlUtils.writeStringAttribute;
27 import static com.android.server.companion.utils.AssociationUtils.getFirstAssociationIdForUser;
28 import static com.android.server.companion.utils.DataStoreUtils.createStorageFileForUser;
29 import static com.android.server.companion.utils.DataStoreUtils.fileToByteArray;
30 import static com.android.server.companion.utils.DataStoreUtils.isEndOfTag;
31 import static com.android.server.companion.utils.DataStoreUtils.isStartOfTag;
32 import static com.android.server.companion.utils.DataStoreUtils.writeToFileSafely;
33 
34 import android.annotation.NonNull;
35 import android.annotation.Nullable;
36 import android.annotation.SuppressLint;
37 import android.annotation.UserIdInt;
38 import android.companion.AssociationInfo;
39 import android.net.MacAddress;
40 import android.os.Environment;
41 import android.util.AtomicFile;
42 import android.util.Slog;
43 import android.util.Xml;
44 
45 import com.android.internal.util.XmlUtils;
46 import com.android.modules.utils.TypedXmlPullParser;
47 import com.android.modules.utils.TypedXmlSerializer;
48 
49 import org.xmlpull.v1.XmlPullParser;
50 import org.xmlpull.v1.XmlPullParserException;
51 import org.xmlpull.v1.XmlSerializer;
52 
53 import java.io.ByteArrayInputStream;
54 import java.io.File;
55 import java.io.FileInputStream;
56 import java.io.IOException;
57 import java.io.InputStream;
58 import java.util.HashMap;
59 import java.util.List;
60 import java.util.Map;
61 import java.util.concurrent.ConcurrentHashMap;
62 import java.util.concurrent.ConcurrentMap;
63 
64 /**
65  * IMPORTANT: This class should NOT be directly used except {@link AssociationStore}
66  *
67  * The class responsible for persisting Association records and other related information (such as
68  * previously used IDs) to a disk, and reading the data back from the disk.
69  *
70  * <p>
71  * Before Android T the data was stored in "companion_device_manager_associations.xml" file in
72  * {@link Environment#getUserSystemDirectory(int) /data/system/user/}.
73  *
74  * See {@link #getBaseLegacyStorageFileForUser(int) getBaseLegacyStorageFileForUser()}.
75  *
76  * <p>
77  * Before Android T the data was stored using the v0 schema. See:
78  * <ul>
79  * <li>{@link #readAssociationsV0(TypedXmlPullParser, int) readAssociationsV0()}.
80  * <li>{@link #readAssociationV0(TypedXmlPullParser, int, int) readAssociationV0()}.
81  * </ul>
82  *
83  * The following snippet is a sample of a file that is using v0 schema.
84  * <pre>{@code
85  * <associations>
86  *   <association
87  *     package="com.sample.companion.app"
88  *     device="AA:BB:CC:DD:EE:00"
89  *     time_approved="1634389553216" />
90  *   <association
91  *     package="com.another.sample.companion.app"
92  *     device="AA:BB:CC:DD:EE:01"
93  *     profile="android.app.role.COMPANION_DEVICE_WATCH"
94  *     notify_device_nearby="false"
95  *     time_approved="1634389752662" />
96  * </associations>
97  * }</pre>
98  *
99  * <p>
100  * Since Android T the data is stored to "companion_device_manager.xml" file in
101  * {@link Environment#getDataSystemDeDirectory(int) /data/system_de/}.
102  *
103  * <p>
104  * Since Android T the data is stored using the v1 schema.
105  *
106  * In the v1 schema, a list of the previously used IDs is stored along with the association
107  * records.
108  *
109  * V1 schema adds a new optional "display_name" attribute, and makes the "mac_address" attribute
110  * optional.
111  * <ul>
112  * <li> {@link #CURRENT_PERSISTENCE_VERSION}
113  * <li> {@link #readAssociationsV1(TypedXmlPullParser, int) readAssociationsV1()}
114  * <li> {@link #readAssociationV1(TypedXmlPullParser, int) readAssociationV1()}
115  * </ul>
116  *
117  * The following snippet is a sample of a file that is using v1 schema.
118  * <pre>{@code
119  * <state persistence-version="1">
120  *     <associations max-id="3">
121  *         <association
122  *             id="1"
123  *             package="com.sample.companion.app"
124  *             mac_address="AA:BB:CC:DD:EE:00"
125  *             self_managed="false"
126  *             notify_device_nearby="false"
127  *             revoked="false"
128  *             last_time_connected="1634641160229"
129  *             time_approved="1634389553216"
130  *             system_data_sync_flags="0"/>
131  *
132  *         <association
133  *             id="3"
134  *             profile="android.app.role.COMPANION_DEVICE_WATCH"
135  *             package="com.sample.companion.another.app"
136  *             display_name="Jhon's Chromebook"
137  *             self_managed="true"
138  *             notify_device_nearby="false"
139  *             revoked="false"
140  *             last_time_connected="1634641160229"
141  *             time_approved="1634641160229"
142  *             system_data_sync_flags="1"/>
143  *     </associations>
144  * </state>
145  * }</pre>
146  */
147 @SuppressLint("LongLogTag")
148 public final class AssociationDiskStore {
149     private static final String TAG = "CDM_AssociationDiskStore";
150 
151     private static final int CURRENT_PERSISTENCE_VERSION = 1;
152 
153     private static final String FILE_NAME_LEGACY = "companion_device_manager_associations.xml";
154     private static final String FILE_NAME = "companion_device_manager.xml";
155 
156     private static final String XML_TAG_STATE = "state";
157     private static final String XML_TAG_ASSOCIATIONS = "associations";
158     private static final String XML_TAG_ASSOCIATION = "association";
159     private static final String XML_TAG_TAG = "tag";
160 
161     private static final String XML_ATTR_PERSISTENCE_VERSION = "persistence-version";
162     private static final String XML_ATTR_MAX_ID = "max-id";
163     private static final String XML_ATTR_ID = "id";
164     private static final String XML_ATTR_PACKAGE = "package";
165     private static final String XML_ATTR_MAC_ADDRESS = "mac_address";
166     private static final String XML_ATTR_DISPLAY_NAME = "display_name";
167     private static final String XML_ATTR_PROFILE = "profile";
168     private static final String XML_ATTR_SELF_MANAGED = "self_managed";
169     private static final String XML_ATTR_NOTIFY_DEVICE_NEARBY = "notify_device_nearby";
170     private static final String XML_ATTR_REVOKED = "revoked";
171     private static final String XML_ATTR_PENDING = "pending";
172     private static final String XML_ATTR_TIME_APPROVED = "time_approved";
173     private static final String XML_ATTR_LAST_TIME_CONNECTED = "last_time_connected";
174     private static final String XML_ATTR_SYSTEM_DATA_SYNC_FLAGS = "system_data_sync_flags";
175 
176     private static final String LEGACY_XML_ATTR_DEVICE = "device";
177 
178     private final @NonNull ConcurrentMap<Integer, AtomicFile> mUserIdToStorageFile =
179             new ConcurrentHashMap<>();
180 
181     /**
182      * Read all associations for given users
183      */
readAssociationsByUsers(@onNull List<Integer> userIds)184     public Map<Integer, Associations> readAssociationsByUsers(@NonNull List<Integer> userIds) {
185         Map<Integer, Associations> userToAssociationsMap = new HashMap<>();
186         for (int userId : userIds) {
187             userToAssociationsMap.put(userId, readAssociationsByUser(userId));
188         }
189         return userToAssociationsMap;
190     }
191 
192     /**
193      * Reads previously persisted data for the given user "into" the provided containers.
194      *
195      * Note that {@link AssociationInfo#getAssociatedDevice()} will always be {@code null} after
196      * retrieval from this datastore because it is not persisted (by design). This means that
197      * persisted data is not guaranteed to be identical to the initial data that was stored at the
198      * time of association.
199      */
200     @NonNull
readAssociationsByUser(@serIdInt int userId)201     private Associations readAssociationsByUser(@UserIdInt int userId) {
202         Slog.i(TAG, "Reading associations for user " + userId + " from disk.");
203         final AtomicFile file = getStorageFileForUser(userId);
204         Associations associations;
205 
206         // getStorageFileForUser() ALWAYS returns the SAME OBJECT, which allows us to synchronize
207         // accesses to the file on the file system using this AtomicFile object.
208         synchronized (file) {
209             File legacyBaseFile = null;
210             final AtomicFile readFrom;
211             final String rootTag;
212             if (!file.getBaseFile().exists()) {
213                 legacyBaseFile = getBaseLegacyStorageFileForUser(userId);
214                 if (!legacyBaseFile.exists()) {
215                     return new Associations();
216                 }
217 
218                 readFrom = new AtomicFile(legacyBaseFile);
219                 rootTag = XML_TAG_ASSOCIATIONS;
220             } else {
221                 readFrom = file;
222                 rootTag = XML_TAG_STATE;
223             }
224 
225             associations = readAssociationsFromFile(userId, readFrom, rootTag);
226 
227             if (legacyBaseFile != null || associations.getVersion() < CURRENT_PERSISTENCE_VERSION) {
228                 // The data is either in the legacy file or in the legacy format, or both.
229                 // Save the data to right file in using the current format.
230                 writeAssociationsToFile(file, associations);
231 
232                 if (legacyBaseFile != null) {
233                     // We saved the data to the right file, can delete the old file now.
234                     legacyBaseFile.delete();
235                 }
236             }
237         }
238         return associations;
239     }
240 
241     /**
242      * Write associations to disk for the user.
243      */
writeAssociationsForUser(@serIdInt int userId, @NonNull Associations associations)244     public void writeAssociationsForUser(@UserIdInt int userId,
245             @NonNull Associations associations) {
246         Slog.i(TAG, "Writing associations for user " + userId + " to disk");
247 
248         final AtomicFile file = getStorageFileForUser(userId);
249         // getStorageFileForUser() ALWAYS returns the SAME OBJECT, which allows us to synchronize
250         // accesses to the file on the file system using this AtomicFile object.
251         synchronized (file) {
252             writeAssociationsToFile(file, associations);
253         }
254     }
255 
256     @NonNull
readAssociationsFromFile(@serIdInt int userId, @NonNull AtomicFile file, @NonNull String rootTag)257     private static Associations readAssociationsFromFile(@UserIdInt int userId,
258             @NonNull AtomicFile file, @NonNull String rootTag) {
259         try (FileInputStream in = file.openRead()) {
260             return readAssociationsFromInputStream(userId, in, rootTag);
261         } catch (XmlPullParserException | IOException e) {
262             Slog.e(TAG, "Error while reading associations file", e);
263             return new Associations();
264         }
265     }
266 
267     @NonNull
readAssociationsFromInputStream(@serIdInt int userId, @NonNull InputStream in, @NonNull String rootTag)268     private static Associations readAssociationsFromInputStream(@UserIdInt int userId,
269             @NonNull InputStream in, @NonNull String rootTag)
270             throws XmlPullParserException, IOException {
271         final TypedXmlPullParser parser = Xml.resolvePullParser(in);
272         XmlUtils.beginDocument(parser, rootTag);
273 
274         final int version = readIntAttribute(parser, XML_ATTR_PERSISTENCE_VERSION, 0);
275         Associations associations = new Associations();
276 
277         switch (version) {
278             case 0:
279                 associations = readAssociationsV0(parser, userId);
280                 break;
281             case 1:
282                 while (true) {
283                     parser.nextTag();
284                     if (isStartOfTag(parser, XML_TAG_ASSOCIATIONS)) {
285                         associations = readAssociationsV1(parser, userId);
286                     } else if (isEndOfTag(parser, rootTag)) {
287                         break;
288                     }
289                 }
290                 break;
291         }
292         return associations;
293     }
294 
writeAssociationsToFile(@onNull AtomicFile file, @NonNull Associations associations)295     private void writeAssociationsToFile(@NonNull AtomicFile file,
296             @NonNull Associations associations) {
297         // Writing to file could fail, for example, if the user has been recently removed and so was
298         // their DE (/data/system_de/<user-id>/) directory.
299         writeToFileSafely(file, out -> {
300             final TypedXmlSerializer serializer = Xml.resolveSerializer(out);
301             serializer.setFeature("http://xmlpull.org/v1/doc/features.html#indent-output", true);
302             serializer.startDocument(null, true);
303             serializer.startTag(null, XML_TAG_STATE);
304             writeIntAttribute(serializer,
305                     XML_ATTR_PERSISTENCE_VERSION, CURRENT_PERSISTENCE_VERSION);
306             writeAssociations(serializer, associations);
307             serializer.endTag(null, XML_TAG_STATE);
308             serializer.endDocument();
309         });
310     }
311 
312     /**
313      * Creates and caches {@link AtomicFile} object that represents the back-up file for the given
314      * user.
315      *
316      * IMPORTANT: the method will ALWAYS return the same {@link AtomicFile} object, which makes it
317      * possible to synchronize reads and writes to the file using the returned object.
318      */
319     @NonNull
getStorageFileForUser(@serIdInt int userId)320     private AtomicFile getStorageFileForUser(@UserIdInt int userId) {
321         return mUserIdToStorageFile.computeIfAbsent(userId,
322                 u -> createStorageFileForUser(userId, FILE_NAME));
323     }
324 
325     /**
326      * Get associations backup payload from disk
327      */
getBackupPayload(@serIdInt int userId)328     public byte[] getBackupPayload(@UserIdInt int userId) {
329         Slog.i(TAG, "Fetching stored state data for user " + userId + " from disk");
330         final AtomicFile file = getStorageFileForUser(userId);
331 
332         synchronized (file) {
333             return fileToByteArray(file);
334         }
335     }
336 
337     /**
338      * Convert payload to a set of associations
339      */
readAssociationsFromPayload(byte[] payload, @UserIdInt int userId)340     public static Associations readAssociationsFromPayload(byte[] payload, @UserIdInt int userId) {
341         try (ByteArrayInputStream in = new ByteArrayInputStream(payload)) {
342             return readAssociationsFromInputStream(userId, in, XML_TAG_STATE);
343         } catch (XmlPullParserException | IOException e) {
344             Slog.e(TAG, "Error while reading associations file", e);
345             return new Associations();
346         }
347     }
348 
getBaseLegacyStorageFileForUser(@serIdInt int userId)349     private static @NonNull File getBaseLegacyStorageFileForUser(@UserIdInt int userId) {
350         return new File(Environment.getUserSystemDirectory(userId), FILE_NAME_LEGACY);
351     }
352 
readAssociationsV0(@onNull TypedXmlPullParser parser, @UserIdInt int userId)353     private static Associations readAssociationsV0(@NonNull TypedXmlPullParser parser,
354             @UserIdInt int userId)
355             throws XmlPullParserException, IOException {
356         requireStartOfTag(parser, XML_TAG_ASSOCIATIONS);
357 
358         // Before Android T Associations didn't have IDs, so when we are upgrading from S (reading
359         // from V0) we need to generate and assign IDs to the existing Associations.
360         // It's safe to do it here, because CDM cannot create new Associations before it reads
361         // existing ones from the backup files. And the fact that we are reading from a V0 file,
362         // means that CDM hasn't assigned any IDs yet, so we can just start from the first available
363         // id for each user (eg. 1 for user 0; 100 001 - for user 1; 200 001 - for user 2; etc).
364         int associationId = getFirstAssociationIdForUser(userId);
365         Associations associations = new Associations();
366         associations.setVersion(0);
367 
368         while (true) {
369             parser.nextTag();
370             if (isEndOfTag(parser, XML_TAG_ASSOCIATIONS)) break;
371             if (!isStartOfTag(parser, XML_TAG_ASSOCIATION)) continue;
372 
373             associations.addAssociation(readAssociationV0(parser, userId, associationId++));
374         }
375 
376         associations.setMaxId(associationId - 1);
377 
378         return associations;
379     }
380 
readAssociationV0(@onNull TypedXmlPullParser parser, @UserIdInt int userId, int associationId)381     private static AssociationInfo readAssociationV0(@NonNull TypedXmlPullParser parser,
382             @UserIdInt int userId, int associationId)
383             throws XmlPullParserException {
384         requireStartOfTag(parser, XML_TAG_ASSOCIATION);
385 
386         final String appPackage = readStringAttribute(parser, XML_ATTR_PACKAGE);
387         final String tag = readStringAttribute(parser, XML_TAG_TAG);
388         final String deviceAddress = readStringAttribute(parser, LEGACY_XML_ATTR_DEVICE);
389         final String profile = readStringAttribute(parser, XML_ATTR_PROFILE);
390         final boolean notify = readBooleanAttribute(parser, XML_ATTR_NOTIFY_DEVICE_NEARBY);
391         final long timeApproved = readLongAttribute(parser, XML_ATTR_TIME_APPROVED, 0L);
392 
393         return new AssociationInfo(associationId, userId, appPackage, tag,
394                 MacAddress.fromString(deviceAddress), null, profile, null,
395                 /* managedByCompanionApp */ false, notify, /* revoked */ false, /* pending */ false,
396                 timeApproved, Long.MAX_VALUE, /* systemDataSyncFlags */ 0);
397     }
398 
readAssociationsV1(@onNull TypedXmlPullParser parser, @UserIdInt int userId)399     private static Associations readAssociationsV1(@NonNull TypedXmlPullParser parser,
400             @UserIdInt int userId)
401             throws XmlPullParserException, IOException {
402         requireStartOfTag(parser, XML_TAG_ASSOCIATIONS);
403 
404         // For old builds that don't have max-id attr,
405         // default maxId to 0 and get the maxId out of all association ids.
406         int maxId = readIntAttribute(parser, XML_ATTR_MAX_ID, 0);
407         Associations associations = new Associations();
408         associations.setVersion(1);
409 
410         while (true) {
411             parser.nextTag();
412             if (isEndOfTag(parser, XML_TAG_ASSOCIATIONS)) break;
413             if (!isStartOfTag(parser, XML_TAG_ASSOCIATION)) continue;
414 
415             AssociationInfo association = readAssociationV1(parser, userId);
416             associations.addAssociation(association);
417 
418             maxId = Math.max(maxId, association.getId());
419         }
420 
421         associations.setMaxId(maxId);
422 
423         return associations;
424     }
425 
readAssociationV1(@onNull TypedXmlPullParser parser, @UserIdInt int userId)426     private static AssociationInfo readAssociationV1(@NonNull TypedXmlPullParser parser,
427             @UserIdInt int userId)
428             throws XmlPullParserException, IOException {
429         requireStartOfTag(parser, XML_TAG_ASSOCIATION);
430 
431         final int associationId = readIntAttribute(parser, XML_ATTR_ID);
432         final String profile = readStringAttribute(parser, XML_ATTR_PROFILE);
433         final String appPackage = readStringAttribute(parser, XML_ATTR_PACKAGE);
434         final String tag = readStringAttribute(parser, XML_TAG_TAG);
435         final MacAddress macAddress = stringToMacAddress(
436                 readStringAttribute(parser, XML_ATTR_MAC_ADDRESS));
437         final String displayName = readStringAttribute(parser, XML_ATTR_DISPLAY_NAME);
438         final boolean selfManaged = readBooleanAttribute(parser, XML_ATTR_SELF_MANAGED);
439         final boolean notify = readBooleanAttribute(parser, XML_ATTR_NOTIFY_DEVICE_NEARBY);
440         final boolean revoked = readBooleanAttribute(parser, XML_ATTR_REVOKED, false);
441         final boolean pending = readBooleanAttribute(parser, XML_ATTR_PENDING, false);
442         final long timeApproved = readLongAttribute(parser, XML_ATTR_TIME_APPROVED, 0L);
443         final long lastTimeConnected = readLongAttribute(
444                 parser, XML_ATTR_LAST_TIME_CONNECTED, Long.MAX_VALUE);
445         final int systemDataSyncFlags = readIntAttribute(parser,
446                 XML_ATTR_SYSTEM_DATA_SYNC_FLAGS, 0);
447 
448         return new AssociationInfo(associationId, userId, appPackage, tag, macAddress, displayName,
449                 profile, null, selfManaged, notify, revoked, pending, timeApproved,
450                 lastTimeConnected, systemDataSyncFlags);
451     }
452 
writeAssociations(@onNull XmlSerializer parent, @NonNull Associations associations)453     private static void writeAssociations(@NonNull XmlSerializer parent,
454             @NonNull Associations associations)
455             throws IOException {
456         final XmlSerializer serializer = parent.startTag(null, XML_TAG_ASSOCIATIONS);
457         for (AssociationInfo association : associations.getAssociations()) {
458             writeAssociation(serializer, association);
459         }
460         writeIntAttribute(serializer, XML_ATTR_MAX_ID, associations.getMaxId());
461         serializer.endTag(null, XML_TAG_ASSOCIATIONS);
462     }
463 
writeAssociation(@onNull XmlSerializer parent, @NonNull AssociationInfo a)464     private static void writeAssociation(@NonNull XmlSerializer parent, @NonNull AssociationInfo a)
465             throws IOException {
466         final XmlSerializer serializer = parent.startTag(null, XML_TAG_ASSOCIATION);
467 
468         writeIntAttribute(serializer, XML_ATTR_ID, a.getId());
469         writeStringAttribute(serializer, XML_ATTR_PROFILE, a.getDeviceProfile());
470         writeStringAttribute(serializer, XML_ATTR_PACKAGE, a.getPackageName());
471         writeStringAttribute(serializer, XML_TAG_TAG, a.getTag());
472         writeStringAttribute(serializer, XML_ATTR_MAC_ADDRESS, a.getDeviceMacAddressAsString());
473         writeStringAttribute(serializer, XML_ATTR_DISPLAY_NAME, a.getDisplayName());
474         writeBooleanAttribute(serializer, XML_ATTR_SELF_MANAGED, a.isSelfManaged());
475         writeBooleanAttribute(
476                 serializer, XML_ATTR_NOTIFY_DEVICE_NEARBY, a.isNotifyOnDeviceNearby());
477         writeBooleanAttribute(serializer, XML_ATTR_REVOKED, a.isRevoked());
478         writeBooleanAttribute(serializer, XML_ATTR_PENDING, a.isPending());
479         writeLongAttribute(serializer, XML_ATTR_TIME_APPROVED, a.getTimeApprovedMs());
480         writeLongAttribute(
481                 serializer, XML_ATTR_LAST_TIME_CONNECTED, a.getLastTimeConnectedMs());
482         writeIntAttribute(serializer, XML_ATTR_SYSTEM_DATA_SYNC_FLAGS, a.getSystemDataSyncFlags());
483 
484         serializer.endTag(null, XML_TAG_ASSOCIATION);
485     }
486 
requireStartOfTag(@onNull XmlPullParser parser, @NonNull String tag)487     private static void requireStartOfTag(@NonNull XmlPullParser parser, @NonNull String tag)
488             throws XmlPullParserException {
489         if (isStartOfTag(parser, tag)) return;
490         throw new XmlPullParserException(
491                 "Should be at the start of \"" + XML_TAG_ASSOCIATIONS + "\" tag");
492     }
493 
stringToMacAddress(@ullable String address)494     private static @Nullable MacAddress stringToMacAddress(@Nullable String address) {
495         return address != null ? MacAddress.fromString(address) : null;
496     }
497 }
498