diff --git a/services/companion/java/com/android/server/companion/DataStoreUtils.java b/services/companion/java/com/android/server/companion/DataStoreUtils.java new file mode 100644 index 0000000000000..6055a81c7ca70 --- /dev/null +++ b/services/companion/java/com/android/server/companion/DataStoreUtils.java @@ -0,0 +1,85 @@ +/* + * Copyright (C) 2022 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 org.xmlpull.v1.XmlPullParser.END_TAG; +import static org.xmlpull.v1.XmlPullParser.START_TAG; + +import android.annotation.NonNull; +import android.annotation.UserIdInt; +import android.os.Environment; +import android.util.AtomicFile; +import android.util.Slog; + +import com.android.internal.util.FunctionalUtils; + +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; + +import java.io.File; +import java.io.FileOutputStream; + +final class DataStoreUtils { + + private static final String LOG_TAG = DataStoreUtils.class.getSimpleName(); + + static boolean isStartOfTag(@NonNull XmlPullParser parser, @NonNull String tag) + throws XmlPullParserException { + return parser.getEventType() == START_TAG && tag.equals(parser.getName()); + } + + static boolean isEndOfTag(@NonNull XmlPullParser parser, @NonNull String tag) + throws XmlPullParserException { + return parser.getEventType() == END_TAG && tag.equals(parser.getName()); + } + + /** + * Creates {@link AtomicFile} object that represents the back-up 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. + * + * @param userId the userId to retrieve the storage file + * @param fileName the storage file name + * @return an AtomicFile for the user + */ + @NonNull + static AtomicFile createStorageFileForUser(@UserIdInt int userId, String fileName) { + return new AtomicFile(getBaseStorageFileForUser(userId, fileName)); + } + + @NonNull + private static File getBaseStorageFileForUser(@UserIdInt int userId, String fileName) { + return new File(Environment.getDataSystemDeDirectory(userId), fileName); + } + + /** + * Writing to file could fail, for example, if the user has been recently removed and so was + * their DE (/data/system_de//) directory. + */ + static void writeToFileSafely(@NonNull AtomicFile file, + @NonNull FunctionalUtils.ThrowingConsumer consumer) { + try { + file.write(consumer); + } catch (Exception e) { + Slog.e(LOG_TAG, "Error while writing to file " + file, e); + } + } + + private DataStoreUtils() { + } +} diff --git a/services/companion/java/com/android/server/companion/PersistentDataStore.java b/services/companion/java/com/android/server/companion/PersistentDataStore.java index 3c8c3cb8f8420..da33b44468404 100644 --- a/services/companion/java/com/android/server/companion/PersistentDataStore.java +++ b/services/companion/java/com/android/server/companion/PersistentDataStore.java @@ -25,9 +25,10 @@ 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 org.xmlpull.v1.XmlPullParser.END_TAG; -import static org.xmlpull.v1.XmlPullParser.START_TAG; +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; @@ -44,7 +45,6 @@ import android.util.TypedXmlPullParser; import android.util.TypedXmlSerializer; import android.util.Xml; -import com.android.internal.util.FunctionalUtils.ThrowingConsumer; import com.android.internal.util.XmlUtils; import org.xmlpull.v1.XmlPullParser; @@ -53,7 +53,6 @@ import org.xmlpull.v1.XmlSerializer; import java.io.File; import java.io.FileInputStream; -import java.io.FileOutputStream; import java.io.IOException; import java.util.Collection; import java.util.HashSet; @@ -210,6 +209,7 @@ final class PersistentDataStore { @NonNull Collection associationsOut, @NonNull Map> previouslyUsedIdsPerPackageOut) { Slog.i(LOG_TAG, "Reading associations for user " + userId + " from disk"); + final AtomicFile file = getStorageFileForUser(userId); if (DEBUG) Slog.d(LOG_TAG, " > File=" + file.getBaseFile().getPath()); @@ -349,11 +349,7 @@ final class PersistentDataStore { */ private @NonNull AtomicFile getStorageFileForUser(@UserIdInt int userId) { return mUserIdToStorageFile.computeIfAbsent(userId, - u -> new AtomicFile(getBaseStorageFileForUser(userId))); - } - - private static @NonNull File getBaseStorageFileForUser(@UserIdInt int userId) { - return new File(Environment.getDataSystemDeDirectory(userId), FILE_NAME); + u -> createStorageFileForUser(userId, FILE_NAME)); } private static @NonNull File getBaseLegacyStorageFileForUser(@UserIdInt int userId) { @@ -512,16 +508,6 @@ final class PersistentDataStore { serializer.endTag(null, XML_TAG_PACKAGE); } - private static boolean isStartOfTag(@NonNull XmlPullParser parser, @NonNull String tag) - throws XmlPullParserException { - return parser.getEventType() == START_TAG && tag.equals(parser.getName()); - } - - private static boolean isEndOfTag(@NonNull XmlPullParser parser, @NonNull String tag) - throws XmlPullParserException { - return parser.getEventType() == END_TAG && tag.equals(parser.getName()); - } - private static void requireStartOfTag(@NonNull XmlPullParser parser, @NonNull String tag) throws XmlPullParserException { if (isStartOfTag(parser, tag)) return; @@ -546,13 +532,4 @@ final class PersistentDataStore { } return associationInfo; } - - private static void writeToFileSafely(@NonNull AtomicFile file, - @NonNull ThrowingConsumer consumer) { - try { - file.write(consumer); - } catch (Exception e) { - Slog.e(LOG_TAG, "Error while writing to file " + file, e); - } - } } diff --git a/services/companion/java/com/android/server/companion/SystemDataTransferRequestDataStore.java b/services/companion/java/com/android/server/companion/SystemDataTransferRequestDataStore.java new file mode 100644 index 0000000000000..38e5d16842b93 --- /dev/null +++ b/services/companion/java/com/android/server/companion/SystemDataTransferRequestDataStore.java @@ -0,0 +1,226 @@ +/* + * Copyright (C) 2022 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.XmlUtils.readBooleanAttribute; +import static com.android.internal.util.XmlUtils.readIntAttribute; +import static com.android.internal.util.XmlUtils.readThisListXml; +import static com.android.internal.util.XmlUtils.writeBooleanAttribute; +import static com.android.internal.util.XmlUtils.writeIntAttribute; +import static com.android.internal.util.XmlUtils.writeListXml; +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.UserIdInt; +import android.companion.SystemDataTransferRequest; +import android.util.AtomicFile; +import android.util.Slog; +import android.util.TypedXmlPullParser; +import android.util.TypedXmlSerializer; +import android.util.Xml; + +import com.android.internal.util.XmlUtils; + +import org.xmlpull.v1.XmlPullParserException; + +import java.io.FileInputStream; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +/** + * The class is responsible for reading/writing SystemDataTransferRequest records from/to the disk. + * + * The following snippet is a sample XML file stored in the disk. + *
{@code
+ * 
+ *   
+ *     
+ *       com.sample.app1
+ *       com.sample.app2
+ *     
+ *   
+ * 
+ * }
+ */ +public class SystemDataTransferRequestDataStore { + + private static final String LOG_TAG = SystemDataTransferRequestDataStore.class.getSimpleName(); + + private static final String FILE_NAME = "companion_device_system_data_transfer_requests.xml"; + + private static final String XML_TAG_REQUESTS = "requests"; + private static final String XML_TAG_REQUEST = "request"; + private static final String XML_TAG_LIST = "list"; + + private static final String XML_ATTR_ASSOCIATION_ID = "association_id"; + private static final String XML_ATTR_IS_PERMISSION_SYNC_ALL_PACKAGES = + "is_permission_sync_all_packages"; + private static final String XML_ATTR_PERMISSION_SYNC_PACKAGES = "permission_sync_packages"; + + private final ConcurrentMap mUserIdToStorageFile = + new ConcurrentHashMap<>(); + + /** + * Reads previously persisted data for the given user + * + * @param userId Android UserID + * @return a list of SystemDataTransferRequest + */ + @NonNull + List readRequestsForUser(@UserIdInt int userId) { + final AtomicFile file = getStorageFileForUser(userId); + Slog.i(LOG_TAG, "Reading SystemDataTransferRequests for user " + userId + " from " + + "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) { + if (!file.getBaseFile().exists()) { + Slog.d(LOG_TAG, "File does not exist -> Abort"); + return Collections.emptyList(); + } + try (FileInputStream in = file.openRead()) { + final TypedXmlPullParser parser = Xml.resolvePullParser(in); + XmlUtils.beginDocument(parser, XML_TAG_REQUESTS); + + return readRequests(parser); + } catch (XmlPullParserException | IOException e) { + Slog.e(LOG_TAG, "Error while reading requests file", e); + return Collections.emptyList(); + } + } + } + + @NonNull + private List readRequests(@NonNull TypedXmlPullParser parser) + throws XmlPullParserException, IOException { + if (!isStartOfTag(parser, XML_TAG_REQUESTS)) { + throw new XmlPullParserException("The XML doesn't have start tag: " + XML_TAG_REQUESTS); + } + + List requests = new ArrayList<>(); + + while (true) { + parser.nextTag(); + if (isEndOfTag(parser, XML_TAG_REQUESTS)) break; + if (isStartOfTag(parser, XML_TAG_REQUEST)) { + requests.add(readRequest(parser)); + } + } + + return requests; + } + + private SystemDataTransferRequest readRequest(@NonNull TypedXmlPullParser parser) + throws XmlPullParserException, IOException { + if (!isStartOfTag(parser, XML_TAG_REQUEST)) { + throw new XmlPullParserException("XML doesn't have start tag: " + XML_TAG_REQUEST); + } + + final int associationId = readIntAttribute(parser, XML_ATTR_ASSOCIATION_ID); + final boolean isPermissionSyncAllPackages = readBooleanAttribute(parser, + XML_ATTR_IS_PERMISSION_SYNC_ALL_PACKAGES); + parser.nextTag(); + List permissionSyncPackages = new ArrayList<>(); + if (isStartOfTag(parser, XML_TAG_LIST)) { + parser.nextTag(); + permissionSyncPackages = readThisListXml(parser, XML_TAG_LIST, + new String[1]); + } + + return new SystemDataTransferRequest(associationId, isPermissionSyncAllPackages, + permissionSyncPackages); + } + + /** + * Persisted user's SystemDataTransferRequest data to the disk. + * + * @param userId Android UserID + * @param requests a list of user's SystemDataTransferRequest. + */ + void writeRequestsForUser(@UserIdInt int userId, + @NonNull List requests) { + final AtomicFile file = getStorageFileForUser(userId); + Slog.i(LOG_TAG, "Writing SystemDataTransferRequests for user " + userId + " to 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) { + 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); + writeRequests(serializer, requests); + serializer.endDocument(); + }); + } + } + + private void writeRequests(@NonNull TypedXmlSerializer serializer, + @Nullable Collection requests) throws IOException { + serializer.startTag(null, XML_TAG_REQUESTS); + + for (SystemDataTransferRequest request : requests) { + writeRequest(serializer, request); + } + + serializer.endTag(null, XML_TAG_REQUESTS); + } + + private void writeRequest(@NonNull TypedXmlSerializer serializer, + @NonNull SystemDataTransferRequest request) throws IOException { + serializer.startTag(null, XML_TAG_REQUEST); + + writeIntAttribute(serializer, XML_ATTR_ASSOCIATION_ID, request.getAssociationId()); + writeBooleanAttribute(serializer, XML_ATTR_IS_PERMISSION_SYNC_ALL_PACKAGES, + request.isPermissionSyncAllPackages()); + try { + writeListXml(request.getPermissionSyncPackages(), XML_ATTR_PERMISSION_SYNC_PACKAGES, + serializer); + } catch (XmlPullParserException e) { + Slog.e(LOG_TAG, "Error writing permission sync packages into XML. " + + request.getPermissionSyncPackages().toString()); + } + + serializer.endTag(null, XML_TAG_REQUEST); + } + + /** + * 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)); + } +}