All Downloads are FREE. Search and download functionalities are using the official Maven repository.

src.com.android.server.companion.PersistentDataStore Maven / Gradle / Ivy

/*
 * Copyright (C) 2021 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.server.companion;

import static com.android.internal.util.CollectionUtils.forEach;
import static com.android.internal.util.XmlUtils.readBooleanAttribute;
import static com.android.internal.util.XmlUtils.readIntAttribute;
import static com.android.internal.util.XmlUtils.readLongAttribute;
import static com.android.internal.util.XmlUtils.readStringAttribute;
import static com.android.internal.util.XmlUtils.writeBooleanAttribute;
import static com.android.internal.util.XmlUtils.writeIntAttribute;
import static com.android.internal.util.XmlUtils.writeLongAttribute;
import static com.android.internal.util.XmlUtils.writeStringAttribute;
import static com.android.server.companion.CompanionDeviceManagerService.getFirstAssociationIdForUser;
import static com.android.server.companion.CompanionDeviceManagerService.getLastAssociationIdForUser;
import static com.android.server.companion.DataStoreUtils.createStorageFileForUser;
import static com.android.server.companion.DataStoreUtils.isEndOfTag;
import static com.android.server.companion.DataStoreUtils.isStartOfTag;
import static com.android.server.companion.DataStoreUtils.writeToFileSafely;

import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.SuppressLint;
import android.annotation.UserIdInt;
import android.companion.AssociationInfo;
import android.content.pm.UserInfo;
import android.net.MacAddress;
import android.os.Environment;
import android.util.ArrayMap;
import android.util.AtomicFile;
import android.util.Log;
import android.util.Slog;
import android.util.SparseArray;
import android.util.TypedXmlPullParser;
import android.util.TypedXmlSerializer;
import android.util.Xml;

import com.android.internal.util.XmlUtils;

import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import org.xmlpull.v1.XmlSerializer;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;

/**
 * The class responsible for persisting Association records and other related information (such as
 * previously used IDs) to a disk, and reading the data back from the disk.
 *
 * 

* Before Android T the data was stored in "companion_device_manager_associations.xml" file in * {@link Environment#getUserSystemDirectory(int) /data/system/user/}. * * See {@link #getBaseLegacyStorageFileForUser(int) getBaseLegacyStorageFileForUser()}. * *

* Before Android T the data was stored using the v0 schema. See: *

    *
  • {@link #readAssociationsV0(TypedXmlPullParser, int, Collection) readAssociationsV0()}. *
  • {@link #readAssociationV0(TypedXmlPullParser, int, int, Collection) readAssociationV0()}. *
* * The following snippet is a sample of a file that is using v0 schema. *
{@code
 * 
 *   
 *   
 * 
 * }
* *

* Since Android T the data is stored to "companion_device_manager.xml" file in * {@link Environment#getDataSystemDeDirectory(int) /data/system_de/}. * * See {@link #getBaseStorageFileForUser(int) getBaseStorageFileForUser()} * *

* Since Android T the data is stored using the v1 schema. * * In the v1 schema, a list of the previously used IDs is stored along with the association * records. * * V1 schema adds a new optional "display_name" attribute, and makes the "mac_address" attribute * optional. *

    *
  • {@link #CURRENT_PERSISTENCE_VERSION} *
  • {@link #readAssociationsV1(TypedXmlPullParser, int, Collection) readAssociationsV1()} *
  • {@link #readAssociationV1(TypedXmlPullParser, int, Collection) readAssociationV1()} *
  • {@link #readPreviouslyUsedIdsV1(TypedXmlPullParser, Map) readPreviouslyUsedIdsV1()} *
* * The following snippet is a sample of a file that is using v0 schema. *
{@code
 * 
 *     
 *         
 *
 *         
 *     
 *
 *     
 *         
 *             2
 *         
 *     
 * 
 * }
*/ @SuppressLint("LongLogTag") final class PersistentDataStore { private static final String TAG = "CompanionDevice_PersistentDataStore"; private static final boolean DEBUG = CompanionDeviceManagerService.DEBUG; private static final int CURRENT_PERSISTENCE_VERSION = 1; private static final String FILE_NAME_LEGACY = "companion_device_manager_associations.xml"; private static final String FILE_NAME = "companion_device_manager.xml"; private static final String XML_TAG_STATE = "state"; private static final String XML_TAG_ASSOCIATIONS = "associations"; private static final String XML_TAG_ASSOCIATION = "association"; private static final String XML_TAG_PREVIOUSLY_USED_IDS = "previously-used-ids"; private static final String XML_TAG_PACKAGE = "package"; private static final String XML_TAG_ID = "id"; private static final String XML_ATTR_PERSISTENCE_VERSION = "persistence-version"; private static final String XML_ATTR_ID = "id"; // Used in elements, nested within elements. private static final String XML_ATTR_PACKAGE_NAME = "package_name"; // Used in elements, nested within elements. private static final String XML_ATTR_PACKAGE = "package"; private static final String XML_ATTR_MAC_ADDRESS = "mac_address"; private static final String XML_ATTR_DISPLAY_NAME = "display_name"; private static final String XML_ATTR_PROFILE = "profile"; private static final String XML_ATTR_SELF_MANAGED = "self_managed"; private static final String XML_ATTR_NOTIFY_DEVICE_NEARBY = "notify_device_nearby"; private static final String XML_ATTR_TIME_APPROVED = "time_approved"; private static final String XML_ATTR_LAST_TIME_CONNECTED = "last_time_connected"; private static final String LEGACY_XML_ATTR_DEVICE = "device"; private final @NonNull ConcurrentMap mUserIdToStorageFile = new ConcurrentHashMap<>(); void readStateForUsers(@NonNull List users, @NonNull Set allAssociationsOut, @NonNull SparseArray>> previouslyUsedIdsPerUserOut) { for (UserInfo user : users) { final int userId = user.id; // Previously used IDs are stored in the "out" collection per-user. final Map> previouslyUsedIds = new ArrayMap<>(); // Associations for all users are stored in a single "flat" set: so we read directly // into it. final Set associationsForUser = new HashSet<>(); readStateForUser(userId, associationsForUser, previouslyUsedIds); // Go through all the associations for the user and check if their IDs are within // the allowed range (for the user). final int firstAllowedId = getFirstAssociationIdForUser(userId); final int lastAllowedId = getLastAssociationIdForUser(userId); for (AssociationInfo association : associationsForUser) { final int id = association.getId(); if (id < firstAllowedId || id > lastAllowedId) { Slog.e(TAG, "Wrong association ID assignment: " + id + ". " + "Association belongs to u" + userId + " and thus its ID should be " + "within [" + firstAllowedId + ", " + lastAllowedId + "] range."); // TODO(b/224736262): try fixing (re-assigning) the ID? } } // Add user's association to the "output" set. allAssociationsOut.addAll(associationsForUser); // Save previously used IDs for this user into the "out" structure. previouslyUsedIdsPerUserOut.append(userId, previouslyUsedIds); } } /** * Reads previously persisted data for the given user "into" the provided containers. * * @param userId Android UserID * @param associationsOut a container to read the {@link AssociationInfo}s "into". * @param previouslyUsedIdsPerPackageOut a container to read the used IDs "into". */ void readStateForUser(@UserIdInt int userId, @NonNull Collection associationsOut, @NonNull Map> previouslyUsedIdsPerPackageOut) { Slog.i(TAG, "Reading associations for user " + userId + " from disk"); final AtomicFile file = getStorageFileForUser(userId); if (DEBUG) Log.d(TAG, " > File=" + file.getBaseFile().getPath()); // getStorageFileForUser() ALWAYS returns the SAME OBJECT, which allows us to synchronize // accesses to the file on the file system using this AtomicFile object. synchronized (file) { File legacyBaseFile = null; final AtomicFile readFrom; final String rootTag; if (!file.getBaseFile().exists()) { if (DEBUG) Log.d(TAG, " > File does not exist -> Try to read legacy file"); legacyBaseFile = getBaseLegacyStorageFileForUser(userId); if (DEBUG) Log.d(TAG, " > Legacy file=" + legacyBaseFile.getPath()); if (!legacyBaseFile.exists()) { if (DEBUG) Log.d(TAG, " > Legacy file does not exist -> Abort"); return; } readFrom = new AtomicFile(legacyBaseFile); rootTag = XML_TAG_ASSOCIATIONS; } else { readFrom = file; rootTag = XML_TAG_STATE; } if (DEBUG) Log.d(TAG, " > Reading associations..."); final int version = readStateFromFileLocked(userId, readFrom, rootTag, associationsOut, previouslyUsedIdsPerPackageOut); if (DEBUG) { Log.d(TAG, " > Done reading: " + associationsOut); if (version < CURRENT_PERSISTENCE_VERSION) { Log.d(TAG, " > File used old format: v." + version + " -> Re-write"); } } if (legacyBaseFile != null || version < CURRENT_PERSISTENCE_VERSION) { // The data is either in the legacy file or in the legacy format, or both. // Save the data to right file in using the current format. if (DEBUG) { Log.d(TAG, " > Writing the data to " + file.getBaseFile().getPath()); } persistStateToFileLocked(file, associationsOut, previouslyUsedIdsPerPackageOut); if (legacyBaseFile != null) { // We saved the data to the right file, can delete the old file now. if (DEBUG) Log.d(TAG, " > Deleting legacy file"); legacyBaseFile.delete(); } } } } /** * Persisted data to the disk. * * @param userId Android UserID * @param associations a set of user's associations. * @param previouslyUsedIdsPerPackage a set previously used Association IDs for the user. */ void persistStateForUser(@UserIdInt int userId, @NonNull Collection associations, @NonNull Map> previouslyUsedIdsPerPackage) { Slog.i(TAG, "Writing associations for user " + userId + " to disk"); if (DEBUG) Slog.d(TAG, " > " + associations); final AtomicFile file = getStorageFileForUser(userId); if (DEBUG) Log.d(TAG, " > File=" + file.getBaseFile().getPath()); // getStorageFileForUser() ALWAYS returns the SAME OBJECT, which allows us to synchronize // accesses to the file on the file system using this AtomicFile object. synchronized (file) { persistStateToFileLocked(file, associations, previouslyUsedIdsPerPackage); } } private int readStateFromFileLocked(@UserIdInt int userId, @NonNull AtomicFile file, @NonNull String rootTag, @Nullable Collection associationsOut, @NonNull Map> previouslyUsedIdsPerPackageOut) { try (FileInputStream in = file.openRead()) { final TypedXmlPullParser parser = Xml.resolvePullParser(in); XmlUtils.beginDocument(parser, rootTag); final int version = readIntAttribute(parser, XML_ATTR_PERSISTENCE_VERSION, 0); switch (version) { case 0: readAssociationsV0(parser, userId, associationsOut); break; case 1: while (true) { parser.nextTag(); if (isStartOfTag(parser, XML_TAG_ASSOCIATIONS)) { readAssociationsV1(parser, userId, associationsOut); } else if (isStartOfTag(parser, XML_TAG_PREVIOUSLY_USED_IDS)) { readPreviouslyUsedIdsV1(parser, previouslyUsedIdsPerPackageOut); } else if (isEndOfTag(parser, rootTag)) { break; } } break; } return version; } catch (XmlPullParserException | IOException e) { Slog.e(TAG, "Error while reading associations file", e); return -1; } } private void persistStateToFileLocked(@NonNull AtomicFile file, @Nullable Collection associations, @NonNull Map> previouslyUsedIdsPerPackage) { // Writing to file could fail, for example, if the user has been recently removed and so was // their DE (/data/system_de//) directory. writeToFileSafely(file, out -> { final TypedXmlSerializer serializer = Xml.resolveSerializer(out); serializer.setFeature( "http://xmlpull.org/v1/doc/features.html#indent-output", true); serializer.startDocument(null, true); serializer.startTag(null, XML_TAG_STATE); writeIntAttribute(serializer, XML_ATTR_PERSISTENCE_VERSION, CURRENT_PERSISTENCE_VERSION); writeAssociations(serializer, associations); writePreviouslyUsedIds(serializer, previouslyUsedIdsPerPackage); serializer.endTag(null, XML_TAG_STATE); serializer.endDocument(); }); } /** * Creates and caches {@link AtomicFile} object that represents the back-up file for the given * user. * * IMPORTANT: the method will ALWAYS return the same {@link AtomicFile} object, which makes it * possible to synchronize reads and writes to the file using the returned object. */ private @NonNull AtomicFile getStorageFileForUser(@UserIdInt int userId) { return mUserIdToStorageFile.computeIfAbsent(userId, u -> createStorageFileForUser(userId, FILE_NAME)); } private static @NonNull File getBaseLegacyStorageFileForUser(@UserIdInt int userId) { return new File(Environment.getUserSystemDirectory(userId), FILE_NAME_LEGACY); } private static void readAssociationsV0(@NonNull TypedXmlPullParser parser, @UserIdInt int userId, @NonNull Collection out) throws XmlPullParserException, IOException { requireStartOfTag(parser, XML_TAG_ASSOCIATIONS); // Before Android T Associations didn't have IDs, so when we are upgrading from S (reading // from V0) we need to generate and assign IDs to the existing Associations. // It's safe to do it here, because CDM cannot create new Associations before it reads // existing ones from the backup files. And the fact that we are reading from a V0 file, // means that CDM hasn't assigned any IDs yet, so we can just start from the first available // id for each user (eg. 1 for user 0; 100 001 - for user 1; 200 001 - for user 2; etc). int associationId = getFirstAssociationIdForUser(userId); while (true) { parser.nextTag(); if (isEndOfTag(parser, XML_TAG_ASSOCIATIONS)) break; if (!isStartOfTag(parser, XML_TAG_ASSOCIATION)) continue; readAssociationV0(parser, userId, associationId++, out); } } private static void readAssociationV0(@NonNull TypedXmlPullParser parser, @UserIdInt int userId, int associationId, @NonNull Collection out) throws XmlPullParserException { requireStartOfTag(parser, XML_TAG_ASSOCIATION); final String appPackage = readStringAttribute(parser, XML_ATTR_PACKAGE); final String deviceAddress = readStringAttribute(parser, LEGACY_XML_ATTR_DEVICE); if (appPackage == null || deviceAddress == null) return; final String profile = readStringAttribute(parser, XML_ATTR_PROFILE); final boolean notify = readBooleanAttribute(parser, XML_ATTR_NOTIFY_DEVICE_NEARBY); final long timeApproved = readLongAttribute(parser, XML_ATTR_TIME_APPROVED, 0L); out.add(new AssociationInfo(associationId, userId, appPackage, MacAddress.fromString(deviceAddress), null, profile, /* managedByCompanionApp */false, notify, timeApproved, Long.MAX_VALUE)); } private static void readAssociationsV1(@NonNull TypedXmlPullParser parser, @UserIdInt int userId, @NonNull Collection out) throws XmlPullParserException, IOException { requireStartOfTag(parser, XML_TAG_ASSOCIATIONS); while (true) { parser.nextTag(); if (isEndOfTag(parser, XML_TAG_ASSOCIATIONS)) break; if (!isStartOfTag(parser, XML_TAG_ASSOCIATION)) continue; readAssociationV1(parser, userId, out); } } private static void readAssociationV1(@NonNull TypedXmlPullParser parser, @UserIdInt int userId, @NonNull Collection out) throws XmlPullParserException, IOException { requireStartOfTag(parser, XML_TAG_ASSOCIATION); final int associationId = readIntAttribute(parser, XML_ATTR_ID); final String profile = readStringAttribute(parser, XML_ATTR_PROFILE); final String appPackage = readStringAttribute(parser, XML_ATTR_PACKAGE); final MacAddress macAddress = stringToMacAddress( readStringAttribute(parser, XML_ATTR_MAC_ADDRESS)); final String displayName = readStringAttribute(parser, XML_ATTR_DISPLAY_NAME); final boolean selfManaged = readBooleanAttribute(parser, XML_ATTR_SELF_MANAGED); final boolean notify = readBooleanAttribute(parser, XML_ATTR_NOTIFY_DEVICE_NEARBY); final long timeApproved = readLongAttribute(parser, XML_ATTR_TIME_APPROVED, 0L); final long lastTimeConnected = readLongAttribute( parser, XML_ATTR_LAST_TIME_CONNECTED, Long.MAX_VALUE); final AssociationInfo associationInfo = createAssociationInfoNoThrow(associationId, userId, appPackage, macAddress, displayName, profile, selfManaged, notify, timeApproved, lastTimeConnected); if (associationInfo != null) { out.add(associationInfo); } } private static void readPreviouslyUsedIdsV1(@NonNull TypedXmlPullParser parser, @NonNull Map> out) throws XmlPullParserException, IOException { requireStartOfTag(parser, XML_TAG_PREVIOUSLY_USED_IDS); while (true) { parser.nextTag(); if (isEndOfTag(parser, XML_TAG_PREVIOUSLY_USED_IDS)) break; if (!isStartOfTag(parser, XML_TAG_PACKAGE)) continue; final String packageName = readStringAttribute(parser, XML_ATTR_PACKAGE_NAME); final Set usedIds = new HashSet<>(); while (true) { parser.nextTag(); if (isEndOfTag(parser, XML_TAG_PACKAGE)) break; if (!isStartOfTag(parser, XML_TAG_ID)) continue; parser.nextToken(); final int id = Integer.parseInt(parser.getText()); usedIds.add(id); } out.put(packageName, usedIds); } } private static void writeAssociations(@NonNull XmlSerializer parent, @Nullable Collection associations) throws IOException { final XmlSerializer serializer = parent.startTag(null, XML_TAG_ASSOCIATIONS); for (AssociationInfo association : associations) { writeAssociation(serializer, association); } serializer.endTag(null, XML_TAG_ASSOCIATIONS); } private static void writeAssociation(@NonNull XmlSerializer parent, @NonNull AssociationInfo a) throws IOException { final XmlSerializer serializer = parent.startTag(null, XML_TAG_ASSOCIATION); writeIntAttribute(serializer, XML_ATTR_ID, a.getId()); writeStringAttribute(serializer, XML_ATTR_PROFILE, a.getDeviceProfile()); writeStringAttribute(serializer, XML_ATTR_PACKAGE, a.getPackageName()); writeStringAttribute(serializer, XML_ATTR_MAC_ADDRESS, a.getDeviceMacAddressAsString()); writeStringAttribute(serializer, XML_ATTR_DISPLAY_NAME, a.getDisplayName()); writeBooleanAttribute(serializer, XML_ATTR_SELF_MANAGED, a.isSelfManaged()); writeBooleanAttribute( serializer, XML_ATTR_NOTIFY_DEVICE_NEARBY, a.isNotifyOnDeviceNearby()); writeLongAttribute(serializer, XML_ATTR_TIME_APPROVED, a.getTimeApprovedMs()); writeLongAttribute( serializer, XML_ATTR_LAST_TIME_CONNECTED, a.getLastTimeConnectedMs()); serializer.endTag(null, XML_TAG_ASSOCIATION); } private static void writePreviouslyUsedIds(@NonNull XmlSerializer parent, @NonNull Map> previouslyUsedIdsPerPackage) throws IOException { final XmlSerializer serializer = parent.startTag(null, XML_TAG_PREVIOUSLY_USED_IDS); for (Map.Entry> entry : previouslyUsedIdsPerPackage.entrySet()) { writePreviouslyUsedIdsForPackage(serializer, entry.getKey(), entry.getValue()); } serializer.endTag(null, XML_TAG_PREVIOUSLY_USED_IDS); } private static void writePreviouslyUsedIdsForPackage(@NonNull XmlSerializer parent, @NonNull String packageName, @NonNull Set previouslyUsedIds) throws IOException { final XmlSerializer serializer = parent.startTag(null, XML_TAG_PACKAGE); writeStringAttribute(serializer, XML_ATTR_PACKAGE_NAME, packageName); forEach(previouslyUsedIds, id -> serializer.startTag(null, XML_TAG_ID) .text(Integer.toString(id)) .endTag(null, XML_TAG_ID)); serializer.endTag(null, XML_TAG_PACKAGE); } private static void requireStartOfTag(@NonNull XmlPullParser parser, @NonNull String tag) throws XmlPullParserException { if (isStartOfTag(parser, tag)) return; throw new XmlPullParserException( "Should be at the start of \"" + XML_TAG_ASSOCIATIONS + "\" tag"); } private static @Nullable MacAddress stringToMacAddress(@Nullable String address) { return address != null ? MacAddress.fromString(address) : null; } private static AssociationInfo createAssociationInfoNoThrow(int associationId, @UserIdInt int userId, @NonNull String appPackage, @Nullable MacAddress macAddress, @Nullable CharSequence displayName, @Nullable String profile, boolean selfManaged, boolean notify, long timeApproved, long lastTimeConnected) { AssociationInfo associationInfo = null; try { associationInfo = new AssociationInfo(associationId, userId, appPackage, macAddress, displayName, profile, selfManaged, notify, timeApproved, lastTimeConnected); } catch (Exception e) { if (DEBUG) Log.w(TAG, "Could not create AssociationInfo", e); } return associationInfo; } }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy