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