diff options
9 files changed, 457 insertions, 28 deletions
diff --git a/core/java/android/companion/AssociationInfo.java b/core/java/android/companion/AssociationInfo.java index 161fa799f2f5..cdb92acc5256 100644 --- a/core/java/android/companion/AssociationInfo.java +++ b/core/java/android/companion/AssociationInfo.java @@ -458,6 +458,29 @@ public final class AssociationInfo implements Parcelable { mSystemDataSyncFlags = info.mSystemDataSyncFlags; } + /** + * This builder is used specifically to create a new association to be restored to a device + * that is potentially using a different user ID from the backed-up device. + * + * @hide + */ + public Builder(int id, int userId, @NonNull String packageName, AssociationInfo info) { + mId = id; + mUserId = userId; + mPackageName = packageName; + mTag = info.mTag; + mDeviceMacAddress = info.mDeviceMacAddress; + mDisplayName = info.mDisplayName; + mDeviceProfile = info.mDeviceProfile; + mAssociatedDevice = info.mAssociatedDevice; + mSelfManaged = info.mSelfManaged; + mNotifyOnDeviceNearby = info.mNotifyOnDeviceNearby; + mRevoked = info.mRevoked; + mTimeApprovedMs = info.mTimeApprovedMs; + mLastTimeConnectedMs = info.mLastTimeConnectedMs; + mSystemDataSyncFlags = info.mSystemDataSyncFlags; + } + /** @hide */ @FlaggedApi(Flags.FLAG_ASSOCIATION_TAG) @TestApi diff --git a/core/java/android/companion/datatransfer/PermissionSyncRequest.java b/core/java/android/companion/datatransfer/PermissionSyncRequest.java index 973fca30c765..34865f0f9afd 100644 --- a/core/java/android/companion/datatransfer/PermissionSyncRequest.java +++ b/core/java/android/companion/datatransfer/PermissionSyncRequest.java @@ -48,6 +48,15 @@ public class PermissionSyncRequest extends SystemDataTransferRequest implements } /** @hide */ + @Override + public PermissionSyncRequest copyWithNewId(int associationId) { + PermissionSyncRequest newRequest = new PermissionSyncRequest(associationId); + newRequest.mUserId = this.mUserId; + newRequest.mUserConsented = this.mUserConsented; + return newRequest; + } + + /** @hide */ @NonNull public static final Creator<PermissionSyncRequest> CREATOR = new Creator<PermissionSyncRequest>() { diff --git a/core/java/android/companion/datatransfer/SystemDataTransferRequest.java b/core/java/android/companion/datatransfer/SystemDataTransferRequest.java index 38a553d8a725..c3a2aa4742c3 100644 --- a/core/java/android/companion/datatransfer/SystemDataTransferRequest.java +++ b/core/java/android/companion/datatransfer/SystemDataTransferRequest.java @@ -103,4 +103,15 @@ public abstract class SystemDataTransferRequest { public int describeContents() { return 0; } + + /** + * Creates a copy of itself with new association ID. + * + * This method must be implemented to ensure that backup-and-restore can correctly re-map + * the restored requests to the restored associations that can potentially have different + * IDs than what was originally backed up. + * + * @hide + */ + public abstract SystemDataTransferRequest copyWithNewId(int associationId); } diff --git a/services/companion/java/com/android/server/companion/BackupRestoreProcessor.java b/services/companion/java/com/android/server/companion/BackupRestoreProcessor.java new file mode 100644 index 000000000000..a7dbd1c15aec --- /dev/null +++ b/services/companion/java/com/android/server/companion/BackupRestoreProcessor.java @@ -0,0 +1,241 @@ +/* + * Copyright (C) 2024 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 android.os.UserHandle.getCallingUserId; + +import android.annotation.NonNull; +import android.annotation.SuppressLint; +import android.annotation.UserIdInt; +import android.companion.AssociationInfo; +import android.companion.Flags; +import android.companion.datatransfer.SystemDataTransferRequest; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManagerInternal; +import android.util.Log; +import android.util.Slog; + +import com.android.internal.util.CollectionUtils; +import com.android.server.companion.datatransfer.SystemDataTransferRequestStore; + +import java.nio.ByteBuffer; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Objects; +import java.util.Set; +import java.util.function.Predicate; + +@SuppressLint("LongLogTag") +class BackupRestoreProcessor { + static final String TAG = "CDM_BackupRestoreProcessor"; + private static final int BACKUP_AND_RESTORE_VERSION = 0; + + @NonNull + private final CompanionDeviceManagerService mService; + @NonNull + private final PackageManagerInternal mPackageManager; + @NonNull + private final AssociationStoreImpl mAssociationStore; + @NonNull + private final PersistentDataStore mPersistentStore; + @NonNull + private final SystemDataTransferRequestStore mSystemDataTransferRequestStore; + @NonNull + private final AssociationRequestsProcessor mAssociationRequestsProcessor; + + BackupRestoreProcessor(@NonNull CompanionDeviceManagerService service, + @NonNull AssociationStoreImpl associationStore, + @NonNull PersistentDataStore persistentStore, + @NonNull SystemDataTransferRequestStore systemDataTransferRequestStore, + @NonNull AssociationRequestsProcessor associationRequestsProcessor) { + mService = service; + mPackageManager = service.mPackageManagerInternal; + mAssociationStore = associationStore; + mPersistentStore = persistentStore; + mSystemDataTransferRequestStore = systemDataTransferRequestStore; + mAssociationRequestsProcessor = associationRequestsProcessor; + } + + /** + * Generate CDM state payload to be backed up. + * Backup payload is formatted as following: + * | (4) payload version | (4) AssociationInfo length | AssociationInfo XML + * | (4) SystemDataTransferRequest length | SystemDataTransferRequest XML (without userId)| + */ + byte[] getBackupPayload(int userId) { + // Persist state first to generate an up-to-date XML file + mService.persistStateForUser(userId); + byte[] associationsPayload = mPersistentStore.getBackupPayload(userId); + int associationsPayloadLength = associationsPayload.length; + + // System data transfer requests are persisted up-to-date already + byte[] requestsPayload = mSystemDataTransferRequestStore.getBackupPayload(userId); + int requestsPayloadLength = requestsPayload.length; + + int payloadSize = /* 3 integers */ 12 + + associationsPayloadLength + + requestsPayloadLength; + + return ByteBuffer.allocate(payloadSize) + .putInt(BACKUP_AND_RESTORE_VERSION) + .putInt(associationsPayloadLength) + .put(associationsPayload) + .putInt(requestsPayloadLength) + .put(requestsPayload) + .array(); + } + + /** + * Create new associations and system data transfer request consents using backed up payload. + */ + void applyRestoredPayload(byte[] payload, int userId) { + ByteBuffer buffer = ByteBuffer.wrap(payload); + + // Make sure that payload version matches current version to ensure proper deserialization + int version = buffer.getInt(); + if (version != BACKUP_AND_RESTORE_VERSION) { + Slog.e(TAG, "Unsupported backup payload version"); + return; + } + + // Read the bytes containing backed-up associations + byte[] associationsPayload = new byte[buffer.getInt()]; + buffer.get(associationsPayload); + final Set<AssociationInfo> restoredAssociations = new HashSet<>(); + mPersistentStore.readStateFromPayload(associationsPayload, userId, + restoredAssociations, new HashMap<>()); + + // Read the bytes containing backed-up system data transfer requests user consent + byte[] requestsPayload = new byte[buffer.getInt()]; + buffer.get(requestsPayload); + List<SystemDataTransferRequest> restoredRequestsForUser = + mSystemDataTransferRequestStore.readRequestsFromPayload(requestsPayload); + + // Get a list of installed packages ahead of time. + List<ApplicationInfo> installedApps = mPackageManager.getInstalledApplications( + 0, userId, getCallingUserId()); + + // Restored device may have a different user ID than the backed-up user's user-ID. Since + // association ID is dependent on the user ID, restored associations must account for + // this potential difference on their association IDs. + for (AssociationInfo restored : restoredAssociations) { + // Don't restore a revoked association. Since they weren't added to the device being + // restored in the first place, there is no need to worry about revoking a role that + // was never granted either. + if (restored.isRevoked()) { + continue; + } + + // Filter restored requests for those that belong to the restored association. + List<SystemDataTransferRequest> restoredRequests = CollectionUtils.filter( + restoredRequestsForUser, it -> it.getAssociationId() == restored.getId()); + + // Handle collision: If a local association belonging to the same package already exists + // and their tags match, then keep the local one in favor of creating a new association. + if (handleCollision(userId, restored, restoredRequests)) { + continue; + } + + // Create a new association reassigned to this user and a valid association ID + final String packageName = restored.getPackageName(); + final int newId = mService.getNewAssociationIdForPackage(userId, packageName); + AssociationInfo newAssociation = + new AssociationInfo.Builder(newId, userId, packageName, restored) + .build(); + + // Check if the companion app for this association is already installed, then do one + // of the following: + // (1) If the app is already installed, then go ahead and add this association and grant + // the role attached to this association to the app. + // (2) If the app isn't yet installed, then add this association to the list of pending + // associations to be added when the package is installed in the future. + boolean isPackageInstalled = installedApps.stream() + .anyMatch(app -> packageName.equals(app.packageName)); + if (isPackageInstalled) { + mAssociationRequestsProcessor.maybeGrantRoleAndStoreAssociation(newAssociation, + null, null); + } else { + // TODO(b/314992577): Check if package is installed before granting + } + + // Re-map restored system data transfer requests to newly created associations + for (SystemDataTransferRequest restoredRequest : restoredRequests) { + SystemDataTransferRequest newRequest = restoredRequest.copyWithNewId(newId); + newRequest.setUserId(userId); + mSystemDataTransferRequestStore.writeRequest(userId, newRequest); + } + } + + // Persist restored state. + mService.persistStateForUser(userId); + } + + /** + * Detects and handles collision between restored association and local association. Returns + * true if there has been a collision and false otherwise. + */ + private boolean handleCollision(@UserIdInt int userId, + AssociationInfo restored, + List<SystemDataTransferRequest> restoredRequests) { + List<AssociationInfo> localAssociations = mAssociationStore.getAssociationsForPackage( + restored.getUserId(), restored.getPackageName()); + Predicate<AssociationInfo> isSameDevice = associationInfo -> { + boolean matchesMacAddress = Objects.equals( + associationInfo.getDeviceMacAddress(), + restored.getDeviceMacAddress()); + boolean matchesTag = !Flags.associationTag() + || Objects.equals(associationInfo.getTag(), restored.getTag()); + return matchesMacAddress && matchesTag; + }; + AssociationInfo local = CollectionUtils.find(localAssociations, isSameDevice); + + // No collision detected + if (local == null) { + return false; + } + + Log.d(TAG, "Conflict detected with association id=" + local.getId() + + " while restoring CDM backup. Keeping local association."); + + List<SystemDataTransferRequest> localRequests = mSystemDataTransferRequestStore + .readRequestsByAssociationId(userId, local.getId()); + + // If local association doesn't have any existing system data transfer request of same type + // attached, then restore corresponding request onto the local association. Otherwise, keep + // the locally stored request. + for (SystemDataTransferRequest restoredRequest : restoredRequests) { + boolean requestTypeExists = CollectionUtils.any(localRequests, request -> + request.getDataType() == restoredRequest.getDataType()); + + // This type of request consent already exists for the association. + if (requestTypeExists) { + continue; + } + + Log.d(TAG, "Restoring " + restoredRequest.getClass().getSimpleName() + + " to an existing association id=" + local.getId() + "."); + + SystemDataTransferRequest newRequest = + restoredRequest.copyWithNewId(local.getId()); + newRequest.setUserId(userId); + mSystemDataTransferRequestStore.writeRequest(userId, newRequest); + } + + return true; + } +} diff --git a/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java b/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java index 487b66cb4e32..858887ae20c6 100644 --- a/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java +++ b/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java @@ -164,6 +164,7 @@ public class CompanionDeviceManagerService extends SystemService { private final SystemDataTransferRequestStore mSystemDataTransferRequestStore; private AssociationRequestsProcessor mAssociationRequestsProcessor; private SystemDataTransferProcessor mSystemDataTransferProcessor; + private BackupRestoreProcessor mBackupRestoreProcessor; private CompanionDevicePresenceMonitor mDevicePresenceMonitor; private CompanionApplicationController mCompanionAppController; private CompanionTransportManager mTransportManager; @@ -256,6 +257,9 @@ public class CompanionDeviceManagerService extends SystemService { mSystemDataTransferProcessor = new SystemDataTransferProcessor(this, mPackageManagerInternal, mAssociationStore, mSystemDataTransferRequestStore, mTransportManager); + mBackupRestoreProcessor = new BackupRestoreProcessor( + /* cdmService */ this, mAssociationStore, mPersistentStore, + mSystemDataTransferRequestStore, mAssociationRequestsProcessor); // TODO(b/279663946): move context sync to a dedicated system service mCrossDeviceSyncController = new CrossDeviceSyncController(getContext(), mTransportManager); @@ -501,7 +505,7 @@ public class CompanionDeviceManagerService extends SystemService { updateAtm(userId, updatedAssociations); } - private void persistStateForUser(@UserIdInt int userId) { + void persistStateForUser(@UserIdInt int userId) { // We want to store both active associations and the revoked (removed) association that we // are keeping around for the final clean-up (delayed role holder removal). final List<AssociationInfo> allAssociations; @@ -577,6 +581,11 @@ public class CompanionDeviceManagerService extends SystemService { mCompanionAppController.onPackagesChanged(userId); } + private void onPackageAddedInternal(@UserIdInt int userId, @NonNull String packageName) { + if (DEBUG) Log.i(TAG, "onPackageAddedInternal() u" + userId + "/" + packageName); + // TODO(b/314992577): Retroactively grant roles for restored associations + } + // Revoke associations if the selfManaged companion device does not connect for 3 months. void removeInactiveSelfManagedAssociations() { final long currentTime = System.currentTimeMillis(); @@ -1052,13 +1061,14 @@ public class CompanionDeviceManagerService extends SystemService { @Override public byte[] getBackupPayload(int userId) { - // TODO(b/286124853): back up CDM data - return new byte[0]; + Log.i(TAG, "getBackupPayload() userId=" + userId); + return mBackupRestoreProcessor.getBackupPayload(userId); } @Override public void applyRestoredPayload(byte[] payload, int userId) { - // TODO(b/286124853): restore CDM data + Log.i(TAG, "applyRestoredPayload() userId=" + userId); + mBackupRestoreProcessor.applyRestoredPayload(payload, userId); } @Override @@ -1067,7 +1077,8 @@ public class CompanionDeviceManagerService extends SystemService { @NonNull String[] args) { return new CompanionDeviceShellCommand(CompanionDeviceManagerService.this, mAssociationStore, mDevicePresenceMonitor, mTransportManager, - mSystemDataTransferProcessor, mAssociationRequestsProcessor) + mSystemDataTransferProcessor, mAssociationRequestsProcessor, + mBackupRestoreProcessor) .exec(this, in.getFileDescriptor(), out.getFileDescriptor(), err.getFileDescriptor(), args); } @@ -1499,6 +1510,11 @@ public class CompanionDeviceManagerService extends SystemService { public void onPackageModified(String packageName) { onPackageModifiedInternal(getChangingUserId(), packageName); } + + @Override + public void onPackageAdded(String packageName, int uid) { + onPackageAddedInternal(getChangingUserId(), packageName); + } }; static int getFirstAssociationIdForUser(@UserIdInt int userId) { diff --git a/services/companion/java/com/android/server/companion/CompanionDeviceShellCommand.java b/services/companion/java/com/android/server/companion/CompanionDeviceShellCommand.java index 9fdf5c2d0fc1..53c0184c6e29 100644 --- a/services/companion/java/com/android/server/companion/CompanionDeviceShellCommand.java +++ b/services/companion/java/com/android/server/companion/CompanionDeviceShellCommand.java @@ -18,6 +18,8 @@ package com.android.server.companion; import static android.companion.CompanionDeviceManager.MESSAGE_REQUEST_CONTEXT_SYNC; +import static com.android.server.companion.PermissionsUtils.sanitizeWithCallerChecks; + import android.companion.AssociationInfo; import android.companion.ContextSyncMessage; import android.companion.Flags; @@ -26,6 +28,7 @@ import android.companion.datatransfer.PermissionSyncRequest; import android.net.MacAddress; import android.os.Binder; import android.os.ShellCommand; +import android.util.Base64; import android.util.proto.ProtoOutputStream; import com.android.server.companion.datatransfer.SystemDataTransferProcessor; @@ -47,19 +50,22 @@ class CompanionDeviceShellCommand extends ShellCommand { private final SystemDataTransferProcessor mSystemDataTransferProcessor; private final AssociationRequestsProcessor mAssociationRequestsProcessor; + private final BackupRestoreProcessor mBackupRestoreProcessor; CompanionDeviceShellCommand(CompanionDeviceManagerService service, AssociationStoreImpl associationStore, CompanionDevicePresenceMonitor devicePresenceMonitor, CompanionTransportManager transportManager, SystemDataTransferProcessor systemDataTransferProcessor, - AssociationRequestsProcessor associationRequestsProcessor) { + AssociationRequestsProcessor associationRequestsProcessor, + BackupRestoreProcessor backupRestoreProcessor) { mService = service; mAssociationStore = associationStore; mDevicePresenceMonitor = devicePresenceMonitor; mTransportManager = transportManager; mSystemDataTransferProcessor = systemDataTransferProcessor; mAssociationRequestsProcessor = associationRequestsProcessor; + mBackupRestoreProcessor = backupRestoreProcessor; } @Override @@ -111,6 +117,19 @@ class CompanionDeviceShellCommand extends ShellCommand { } break; + case "disassociate-all": { + final int userId = getNextIntArgRequired(); + final String packageName = getNextArgRequired(); + final List<AssociationInfo> userAssociations = + mAssociationStore.getAssociationsForPackage(userId, packageName); + for (AssociationInfo association : userAssociations) { + if (sanitizeWithCallerChecks(mService.getContext(), association) != null) { + mService.disassociateInternal(association.getId()); + } + } + } + break; + case "clear-association-memory-cache": mService.persistState(); mService.loadAssociationsFromDisk(); @@ -126,6 +145,20 @@ class CompanionDeviceShellCommand extends ShellCommand { mDevicePresenceMonitor.simulateDeviceEvent(associationId, /* event */ 1); break; + case "get-backup-payload": { + final int userId = getNextIntArgRequired(); + byte[] payload = mBackupRestoreProcessor.getBackupPayload(userId); + out.println(Base64.encodeToString(payload, Base64.NO_WRAP)); + } + break; + + case "apply-restored-payload": { + final int userId = getNextIntArgRequired(); + byte[] payload = Base64.decode(getNextArgRequired(), Base64.NO_WRAP); + mBackupRestoreProcessor.applyRestoredPayload(payload, userId); + } + break; + case "remove-inactive-associations": { // This command should trigger the same "clean-up" job as performed by the // InactiveAssociationsRemovalService JobService. However, since the @@ -355,6 +388,8 @@ class CompanionDeviceShellCommand extends ShellCommand { pw.println(" Create a new Association."); pw.println(" disassociate USER_ID PACKAGE MAC_ADDRESS"); pw.println(" Remove an existing Association."); + pw.println(" disassociate-all USER_ID"); + pw.println(" Remove all Associations for a user."); pw.println(" clear-association-memory-cache"); pw.println(" Clear the in-memory association cache and reload all association "); pw.println(" information from persistent storage. USE FOR DEBUGGING PURPOSES ONLY."); @@ -378,6 +413,14 @@ class CompanionDeviceShellCommand extends ShellCommand { pw.println(" invoked for the same device (same ASSOCIATION_ID) no longer than"); pw.println(" 60 seconds ago."); + pw.println(" get-backup-payload USER_ID"); + pw.println(" Generate backup payload for the given user and print its content"); + pw.println(" encoded to a Base64 string."); + pw.println(" USE FOR DEBUGGING AND/OR TESTING PURPOSES ONLY."); + pw.println(" apply-restored-payload USER_ID PAYLOAD"); + pw.println(" Apply restored backup payload for the given user."); + pw.println(" USE FOR DEBUGGING AND/OR TESTING PURPOSES ONLY."); + if (Flags.devicePresence()) { pw.println(" simulate-device-event ASSOCIATION_ID EVENT"); pw.println(" Simulate the companion device event changes:"); diff --git a/services/companion/java/com/android/server/companion/DataStoreUtils.java b/services/companion/java/com/android/server/companion/DataStoreUtils.java index c1825296ca69..04ce1f673124 100644 --- a/services/companion/java/com/android/server/companion/DataStoreUtils.java +++ b/services/companion/java/com/android/server/companion/DataStoreUtils.java @@ -30,8 +30,11 @@ import com.android.internal.util.FunctionalUtils.ThrowingConsumer; import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; +import java.io.ByteArrayOutputStream; import java.io.File; +import java.io.FileInputStream; import java.io.FileOutputStream; +import java.io.IOException; /** * Util class for CDM data stores @@ -88,6 +91,29 @@ public final class DataStoreUtils { } } + /** + * Read a file and return the byte array containing the bytes of the file. + */ + @NonNull + public static byte[] fileToByteArray(@NonNull AtomicFile file) { + if (!file.getBaseFile().exists()) { + Slog.d(TAG, "File does not exist"); + return new byte[0]; + } + try (FileInputStream in = file.openRead()) { + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + byte[] buffer = new byte[1024]; + int read; + while ((read = in.read(buffer)) != -1) { + bytes.write(buffer, 0, read); + } + return bytes.toByteArray(); + } catch (IOException e) { + Slog.e(TAG, "Error while reading requests file", e); + } + return new byte[0]; + } + 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 b4b93793d549..dbaf7e85b7fa 100644 --- a/services/companion/java/com/android/server/companion/PersistentDataStore.java +++ b/services/companion/java/com/android/server/companion/PersistentDataStore.java @@ -28,6 +28,7 @@ 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.fileToByteArray; import static com.android.server.companion.DataStoreUtils.isEndOfTag; import static com.android.server.companion.DataStoreUtils.isStartOfTag; import static com.android.server.companion.DataStoreUtils.writeToFileSafely; @@ -55,9 +56,11 @@ import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; import org.xmlpull.v1.XmlSerializer; +import java.io.ByteArrayInputStream; import java.io.File; import java.io.FileInputStream; import java.io.IOException; +import java.io.InputStream; import java.util.Collection; import java.util.HashSet; import java.util.List; @@ -328,34 +331,42 @@ final class PersistentDataStore { @NonNull String rootTag, @Nullable Collection<AssociationInfo> associationsOut, @NonNull Map<String, Set<Integer>> 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; + return readStateFromInputStream(userId, in, rootTag, associationsOut, + previouslyUsedIdsPerPackageOut); } catch (XmlPullParserException | IOException e) { Slog.e(TAG, "Error while reading associations file", e); return -1; } } + private int readStateFromInputStream(@UserIdInt int userId, @NonNull InputStream in, + @NonNull String rootTag, @Nullable Collection<AssociationInfo> associationsOut, + @NonNull Map<String, Set<Integer>> previouslyUsedIdsPerPackageOut) + throws XmlPullParserException, IOException { + 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; + } + private void persistStateToFileLocked(@NonNull AtomicFile file, @Nullable Collection<AssociationInfo> associations, @NonNull Map<String, Set<Integer>> previouslyUsedIdsPerPackage) { @@ -391,6 +402,26 @@ final class PersistentDataStore { u -> createStorageFileForUser(userId, FILE_NAME)); } + byte[] getBackupPayload(@UserIdInt int userId) { + Slog.i(TAG, "Fetching stored state data for user " + userId + " from disk"); + final AtomicFile file = getStorageFileForUser(userId); + + synchronized (file) { + return fileToByteArray(file); + } + } + + void readStateFromPayload(byte[] payload, @UserIdInt int userId, + @NonNull Set<AssociationInfo> associationsOut, + @NonNull Map<String, Set<Integer>> previouslyUsedIdsPerPackageOut) { + try (ByteArrayInputStream in = new ByteArrayInputStream(payload)) { + readStateFromInputStream(userId, in, XML_TAG_STATE, associationsOut, + previouslyUsedIdsPerPackageOut); + } catch (XmlPullParserException | IOException e) { + Slog.e(TAG, "Error while reading associations file", e); + } + } + private static @NonNull File getBaseLegacyStorageFileForUser(@UserIdInt int userId) { return new File(Environment.getUserSystemDirectory(userId), FILE_NAME_LEGACY); } diff --git a/services/companion/java/com/android/server/companion/datatransfer/SystemDataTransferRequestStore.java b/services/companion/java/com/android/server/companion/datatransfer/SystemDataTransferRequestStore.java index 9f489e8d613a..8fe04547a9ec 100644 --- a/services/companion/java/com/android/server/companion/datatransfer/SystemDataTransferRequestStore.java +++ b/services/companion/java/com/android/server/companion/datatransfer/SystemDataTransferRequestStore.java @@ -23,6 +23,7 @@ import static com.android.internal.util.XmlUtils.readIntAttribute; import static com.android.internal.util.XmlUtils.writeBooleanAttribute; import static com.android.internal.util.XmlUtils.writeIntAttribute; import static com.android.server.companion.DataStoreUtils.createStorageFileForUser; +import static com.android.server.companion.DataStoreUtils.fileToByteArray; import static com.android.server.companion.DataStoreUtils.isEndOfTag; import static com.android.server.companion.DataStoreUtils.isStartOfTag; import static com.android.server.companion.DataStoreUtils.writeToFileSafely; @@ -44,6 +45,7 @@ import com.android.modules.utils.TypedXmlSerializer; import org.xmlpull.v1.XmlPullParserException; +import java.io.ByteArrayInputStream; import java.io.FileInputStream; import java.io.IOException; import java.util.ArrayList; @@ -152,6 +154,33 @@ public class SystemDataTransferRequestStore { mExecutor.execute(() -> writeRequestsToStore(userId, cachedRequests)); } + /** + * Return the byte contents of the XML file storing current system data transfer requests. + */ + public byte[] getBackupPayload(@UserIdInt int userId) { + final AtomicFile file = getStorageFileForUser(userId); + + synchronized (file) { + return fileToByteArray(file); + } + } + + /** + * Parse the byte array containing XML information of system data transfer requests into + * an array list of requests. + */ + public List<SystemDataTransferRequest> readRequestsFromPayload(byte[] payload) { + try (ByteArrayInputStream in = new ByteArrayInputStream(payload)) { + final TypedXmlPullParser parser = Xml.resolvePullParser(in); + XmlUtils.beginDocument(parser, XML_TAG_REQUESTS); + + return readRequestsFromXml(parser); + } catch (XmlPullParserException | IOException e) { + Slog.e(LOG_TAG, "Error while reading requests file", e); + return new ArrayList<>(); + } + } + @GuardedBy("mLock") private ArrayList<SystemDataTransferRequest> readRequestsFromCache(@UserIdInt int userId) { ArrayList<SystemDataTransferRequest> cachedRequests = mCachedPerUser.get(userId); |