summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author Darshil Shah <darshilshah@google.com> 2025-02-12 09:24:25 -0800
committer Android (Google) Code Review <android-gerrit@google.com> 2025-02-12 09:24:25 -0800
commit6813f3582452cc27c70b9783db57ac5f7d3ed612 (patch)
treec3208b701f2e555edec04818c75d31ae8a8785df
parentda3fdd1076e51ab17afa4e19115e368174f4784a (diff)
parentab6f4a5085e4d1f15ac0c28e66cb0cfadeecff1c (diff)
Merge "Add configurations to support for backup and restore of MediaProvider" into main
-rw-r--r--AndroidManifest.xml10
-rw-r--r--res/xml/data_extraction_rules.xml35
-rw-r--r--src/com/android/providers/media/MediaProvider.java9
-rw-r--r--src/com/android/providers/media/backupandrestore/BackupAndRestoreUtils.java129
-rw-r--r--src/com/android/providers/media/backupandrestore/BackupExecutor.java32
-rw-r--r--src/com/android/providers/media/backupandrestore/MediaBackupAgent.java164
-rw-r--r--src/com/android/providers/media/backupandrestore/RestoreExecutor.java13
-rw-r--r--src/com/android/providers/media/scan/ModernMediaScanner.java29
-rw-r--r--tests/src/com/android/providers/media/backupandrestore/BackupAndRestoreTestUtils.java175
-rw-r--r--tests/src/com/android/providers/media/backupandrestore/BackupExecutorTest.java74
-rw-r--r--tests/src/com/android/providers/media/backupandrestore/MediaBackupAgentTest.java189
-rw-r--r--tests/src/com/android/providers/media/backupandrestore/RestoreExecutorTest.java73
12 files changed, 729 insertions, 203 deletions
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index a18581111..e4249ce1c 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -84,11 +84,17 @@
android:name="com.android.providers.media.MediaApplication"
android:label="@string/picker_app_label"
android:icon="@mipmap/picker_app_icon"
- android:allowBackup="false"
android:supportsRtl="true"
android:forceQueryable="true"
android:usesCleartextTraffic="true"
- android:crossProfile="true">
+ android:crossProfile="true"
+ android:allowBackup="true"
+ android:fullBackupOnly="true"
+ android:backupInForeground="true"
+ android:killAfterRestore="false"
+ android:restoreAnyVersion="true"
+ android:backupAgent="com.android.providers.media.backupandrestore.MediaBackupAgent"
+ android:dataExtractionRules="@xml/data_extraction_rules">
<provider
android:name="com.android.providers.media.MediaProvider"
android:authorities="media"
diff --git a/res/xml/data_extraction_rules.xml b/res/xml/data_extraction_rules.xml
new file mode 100644
index 000000000..77d698b96
--- /dev/null
+++ b/res/xml/data_extraction_rules.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2025 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.
+ -->
+
+<data-extraction-rules>
+ <!-- Disable cloud backup -->
+ <cloud-backup>
+ <exclude domain="root" path="."/>
+ <exclude domain="file" path="."/>
+ <exclude domain="database" path="."/>
+ <exclude domain="sharedpref" path="."/>
+ <exclude domain="external" path="."/>
+ <exclude domain="device_root" path="."/>
+ <exclude domain="device_file" path="."/>
+ <exclude domain="device_database" path="."/>
+ <exclude domain="device_sharedpref" path="."/>
+ </cloud-backup>
+ <!-- Only transfer files/backup folder for device transfer -->
+ <device-transfer>
+ <include domain="file" path="backup" />
+ </device-transfer>
+</data-extraction-rules>
diff --git a/src/com/android/providers/media/MediaProvider.java b/src/com/android/providers/media/MediaProvider.java
index 3985e79fa..0beb07b8d 100644
--- a/src/com/android/providers/media/MediaProvider.java
+++ b/src/com/android/providers/media/MediaProvider.java
@@ -300,6 +300,7 @@ import com.android.modules.utils.BackgroundThread;
import com.android.modules.utils.build.SdkLevel;
import com.android.providers.media.DatabaseHelper.OnFilesChangeListener;
import com.android.providers.media.DatabaseHelper.OnLegacyMigrationListener;
+import com.android.providers.media.backupandrestore.BackupAndRestoreUtils;
import com.android.providers.media.backupandrestore.BackupExecutor;
import com.android.providers.media.dao.FileRow;
import com.android.providers.media.flags.Flags;
@@ -1794,6 +1795,7 @@ public class MediaProvider extends ContentProvider {
return null;
});
}
+ BackupAndRestoreUtils.doCleanUpAfterRestoreIfRequired(getContext());
// Delete any stale thumbnails
final int staleThumbnails = mExternalDatabase.runWithTransaction((db) -> {
@@ -7376,6 +7378,13 @@ public class MediaProvider extends ContentProvider {
return null;
}
+ /**
+ * Triggers backup for MediaProvider.
+ */
+ public void triggerBackup() {
+ mExternalPrimaryBackupExecutor.doBackup(null);
+ }
+
@Nullable
private Bundle getResultForWaitForIdle() {
// TODO(b/195009139): Remove after overriding wait for idle in test to sync picker
diff --git a/src/com/android/providers/media/backupandrestore/BackupAndRestoreUtils.java b/src/com/android/providers/media/backupandrestore/BackupAndRestoreUtils.java
index 4ad086784..c162c8c06 100644
--- a/src/com/android/providers/media/backupandrestore/BackupAndRestoreUtils.java
+++ b/src/com/android/providers/media/backupandrestore/BackupAndRestoreUtils.java
@@ -16,10 +16,21 @@
package com.android.providers.media.backupandrestore;
+import static com.android.providers.media.flags.Flags.enableBackupAndRestore;
+
+import android.annotation.NonNull;
+import android.content.Context;
+import android.content.pm.PackageManager;
import android.provider.MediaStore;
+import android.util.Log;
+
+import com.android.modules.utils.build.SdkLevel;
+import com.android.providers.media.util.FileUtils;
import com.google.common.collect.HashBiMap;
+import java.io.File;
+
/**
* Class containing common constants and methods for backup and restore.
*/
@@ -56,6 +67,11 @@ public final class BackupAndRestoreUtils {
static final String RESTORE_COMPLETED = "RESTORE_COMPLETED";
/**
+ * TAG to be used for logging purposes
+ */
+ static final String TAG = BackupAndRestoreUtils.class.getSimpleName();
+
+ /**
* Array of columns backed up for restore in the future.
*/
static final String[] BACKUP_COLUMNS = new String[]{
@@ -156,4 +172,117 @@ public final class BackupAndRestoreUtils {
// Adding number gap to allow addition of new values
sIdToColumnBiMap.put("80", MediaStore.MediaColumns.XMP);
}
+
+ /**
+ * Checks whether backup and restore operations are supported and enabled on the current device.
+ *
+ * <p>This method verifies that the required backup and restore flag is enabled, SDK version is
+ * S+ and ensures the device hardware is suitable for these operations. Backup and restore are
+ * supported only on mobile phones and tablets, excluding devices like automotive systems, TVs,
+ * PCs, and smartwatches.</p>
+ *
+ * @param context the application {@link Context}, used to access system resources.
+ * @return {@code true} if backup and restore is enabled and supported on the device,
+ * {@code false} otherwise.
+ */
+ static boolean isBackupAndRestoreSupported(Context context) {
+ if (!enableBackupAndRestore() || !SdkLevel.isAtLeastS()) {
+ return false;
+ }
+
+ if (context == null || context.getPackageManager() == null) {
+ return false;
+ }
+
+ final PackageManager pm = context.getPackageManager();
+ return !pm.hasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE)
+ && !pm.hasSystemFeature(PackageManager.FEATURE_TELEVISION)
+ && !pm.hasSystemFeature(PackageManager.FEATURE_PC)
+ && !pm.hasSystemFeature(PackageManager.FEATURE_WATCH);
+ }
+
+ /**
+ * Deletes the restore directory and unsets shared preference.
+ *
+ * <p>
+ * This method is triggered during idle maintenance after a media scan.
+ * During the scan, restored values are read and metadata is updated.
+ * Once the scan is complete, the restore directory is no longer needed and is deleted.
+ * The shared preference is unset to indicate that no recent restoration has occurred.
+ * </p>
+ *
+ * @param context The context to check shared preference and delete the restore directory
+ */
+ public static void doCleanUpAfterRestoreIfRequired(Context context) {
+ if (isBackupAndRestoreSupported(context) && isRestoringFromRecentBackup(context)) {
+ try {
+ deleteRestoreDirectory(context);
+ } catch (Exception e) {
+ Log.e(TAG, "Failed to delete restore directory", e);
+ }
+ disableRestoreFromRecentBackup(context);
+ }
+ }
+
+ /**
+ * Indicates that the values should be read from the recent backup. Sets shared preference's
+ * value to true.
+ *
+ * @param context The context used to access shared preferences.
+ */
+ static void enableRestoreFromRecentBackup(@NonNull Context context) {
+ context.getSharedPreferences(SHARED_PREFERENCE_NAME,
+ Context.MODE_PRIVATE).edit().putBoolean(RESTORE_COMPLETED, true).apply();
+ }
+
+ /**
+ * Indicates that values shouldn't be read from backup. Sets shared preference value to false.
+ *
+ * @param context The context used to access shared preferences.
+ */
+ static void disableRestoreFromRecentBackup(@NonNull Context context) {
+ context.getSharedPreferences(SHARED_PREFERENCE_NAME,
+ Context.MODE_PRIVATE).edit().putBoolean(RESTORE_COMPLETED, false).apply();
+ }
+
+ /**
+ * Checks if the shared preference is set, indicating a recent restore operation.
+ *
+ * @param context The application context used to access shared preferences.
+ * @return {@code true} if a restore operation was recently completed, {@code false} otherwise.
+ */
+ static boolean isRestoringFromRecentBackup(@NonNull Context context) {
+ return context.getSharedPreferences(SHARED_PREFERENCE_NAME,
+ Context.MODE_PRIVATE).getBoolean(RESTORE_COMPLETED, false);
+ }
+
+ /**
+ * Deletes the backup directory if it exists.
+ *
+ * @param context The application context used to locate and delete the backup directory.
+ */
+ static void deleteBackupDirectory(@NonNull Context context) {
+ File filesDir = context.getFilesDir();
+ File backupDir = new File(filesDir, BACKUP_DIRECTORY_NAME);
+
+ if (backupDir.exists() && backupDir.isDirectory()) {
+ FileUtils.deleteContents(backupDir);
+ backupDir.delete();
+ }
+ }
+
+ /**
+ * Deletes the restore directory if it exists.
+ *
+ * @param context The application context used to locate and delete the restore directory.
+ */
+ static void deleteRestoreDirectory(@NonNull Context context) {
+ File filesDir = context.getFilesDir();
+ File restoreDir = new File(filesDir, RESTORE_DIRECTORY_NAME);
+
+ if (restoreDir.exists() && restoreDir.isDirectory()) {
+ FileUtils.deleteContents(restoreDir);
+ restoreDir.delete();
+ }
+ }
}
diff --git a/src/com/android/providers/media/backupandrestore/BackupExecutor.java b/src/com/android/providers/media/backupandrestore/BackupExecutor.java
index 9985477e6..118ddd88f 100644
--- a/src/com/android/providers/media/backupandrestore/BackupExecutor.java
+++ b/src/com/android/providers/media/backupandrestore/BackupExecutor.java
@@ -22,19 +22,17 @@ import static com.android.providers.media.backupandrestore.BackupAndRestoreUtils
import static com.android.providers.media.backupandrestore.BackupAndRestoreUtils.BACKUP_DIRECTORY_NAME;
import static com.android.providers.media.backupandrestore.BackupAndRestoreUtils.FIELD_SEPARATOR;
import static com.android.providers.media.backupandrestore.BackupAndRestoreUtils.KEY_VALUE_SEPARATOR;
-import static com.android.providers.media.flags.Flags.enableBackupAndRestore;
+import static com.android.providers.media.backupandrestore.BackupAndRestoreUtils.isBackupAndRestoreSupported;
import static com.android.providers.media.util.Logging.TAG;
import android.annotation.SuppressLint;
import android.content.Context;
-import android.content.pm.PackageManager;
import android.database.Cursor;
import android.os.CancellationSignal;
import android.provider.MediaStore.Files.FileColumns;
import android.provider.MediaStore.MediaColumns;
import android.util.Log;
-import com.android.modules.utils.build.SdkLevel;
import com.android.providers.media.DatabaseHelper;
import com.android.providers.media.leveldb.LevelDBEntry;
import com.android.providers.media.leveldb.LevelDBInstance;
@@ -111,34 +109,6 @@ public final class BackupExecutor {
updateLastBackedUpGenerationNumber(lastGenerationNumber);
}
- /**
- * Checks whether backup and restore operations are supported and enabled on the current device.
- *
- * <p>This method verifies that the required backup and restore flag is enabled, SDK version is
- * S+ and ensures the device hardware is suitable for these operations. Backup and restore are
- * supported only on mobile phones and tablets, excluding devices like automotive systems, TVs,
- * PCs, and smartwatches.</p>
- *
- * @param context the application {@link Context}, used to access system resources.
- * @return {@code true} if backup and restore is enabled and supported on the device,
- * {@code false} otherwise.
- */
- public static boolean isBackupAndRestoreSupported(Context context) {
- if (!enableBackupAndRestore() || !SdkLevel.isAtLeastS()) {
- return false;
- }
-
- if (context == null || context.getPackageManager() == null) {
- return false;
- }
-
- final PackageManager pm = context.getPackageManager();
- return !pm.hasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE)
- && !pm.hasSystemFeature(PackageManager.FEATURE_TELEVISION)
- && !pm.hasSystemFeature(PackageManager.FEATURE_PC)
- && !pm.hasSystemFeature(PackageManager.FEATURE_WATCH);
- }
-
private long clearBackupIfNeededAndReturnLastBackedUpNumber(long currentDbGenerationNumber,
long lastBackedUpGenerationNumber) {
if (currentDbGenerationNumber < lastBackedUpGenerationNumber) {
diff --git a/src/com/android/providers/media/backupandrestore/MediaBackupAgent.java b/src/com/android/providers/media/backupandrestore/MediaBackupAgent.java
new file mode 100644
index 000000000..6fa98220e
--- /dev/null
+++ b/src/com/android/providers/media/backupandrestore/MediaBackupAgent.java
@@ -0,0 +1,164 @@
+/*
+ * Copyright (C) 2025 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.providers.media.backupandrestore;
+
+import static com.android.providers.media.backupandrestore.BackupAndRestoreUtils.BACKUP_DIRECTORY_NAME;
+import static com.android.providers.media.backupandrestore.BackupAndRestoreUtils.RESTORE_DIRECTORY_NAME;
+import static com.android.providers.media.backupandrestore.BackupAndRestoreUtils.deleteBackupDirectory;
+import static com.android.providers.media.backupandrestore.BackupAndRestoreUtils.deleteRestoreDirectory;
+import static com.android.providers.media.backupandrestore.BackupAndRestoreUtils.enableRestoreFromRecentBackup;
+import static com.android.providers.media.backupandrestore.BackupAndRestoreUtils.isBackupAndRestoreSupported;
+
+import android.annotation.NonNull;
+import android.app.backup.BackupAgent;
+import android.app.backup.BackupDataInput;
+import android.app.backup.BackupDataOutput;
+import android.app.backup.FullBackupDataOutput;
+import android.content.ContentProviderClient;
+import android.content.Context;
+import android.os.ParcelFileDescriptor;
+import android.provider.MediaStore;
+import android.util.Log;
+
+import com.android.providers.media.MediaProvider;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.StandardCopyOption;
+import java.util.stream.Stream;
+
+/**
+ * This custom BackupAgent is used for backing up and restoring MediaProvider's metadata.
+ * <p>
+ * It implements {@link BackupAgent#onFullBackup} and {@link BackupAgent#onRestoreFinished}
+ * to handle pre-processing tasks before the backup is initiated and post-processing tasks after
+ * the restore is completed, respectively.
+ * </p>
+ */
+public final class MediaBackupAgent extends BackupAgent {
+
+ private static final String TAG = MediaBackupAgent.class.getSimpleName();
+
+ @Override
+ public void onBackup(ParcelFileDescriptor oldState, BackupDataOutput data,
+ ParcelFileDescriptor newState) throws IOException {
+ }
+
+ @Override
+ public void onRestore(BackupDataInput data, int appVersionCode, ParcelFileDescriptor newState)
+ throws IOException {
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * <p>
+ * Checks if media provider's backup and restore is supported for the device. If supported,
+ * triggers a backup
+ * </p>
+ */
+ @Override
+ public void onFullBackup(FullBackupDataOutput data) throws IOException {
+ if ((data.getTransportFlags() & FLAG_DEVICE_TO_DEVICE_TRANSFER) == 0) {
+ Log.v(TAG, "Skip cloud backup for media provider");
+ return;
+ }
+
+ Context context = getApplicationContext();
+ if (isBackupAndRestoreSupported(context)) {
+ try (ContentProviderClient cpc = context.getContentResolver()
+ .acquireContentProviderClient(MediaStore.AUTHORITY)) {
+ final MediaProvider provider = ((MediaProvider) cpc.getLocalContentProvider());
+ provider.triggerBackup();
+ } catch (Exception e) {
+ Log.e(TAG, "Failed to trigger backup", e);
+ }
+ }
+
+ super.onFullBackup(data);
+ }
+
+ /**
+ * This method is called on the target device as a part of restore process after files transfer
+ * is completed and before the first media scan is triggered by restore apk. Checks if media
+ * provider's backup and restore is supported for the device. If supported,
+ * 1. Copies over files from backup directory to restore directory & deletes backup directory
+ * 2. Sets shared preference to true
+ */
+ @Override
+ public void onRestoreFinished() {
+ super.onRestoreFinished();
+
+ Context context = getApplicationContext();
+ if (isBackupAndRestoreSupported(context)) {
+ // The backed-up data from the source device will be read from the restore directory,
+ // while the device will create its own backup directory.
+ copyContentsFromBackupToRestoreDirectory(context);
+
+ // Delete the copied over backup directory
+ deleteBackupDirectory(context);
+
+ // Indicates restore is completed and metadata can be read from restore directory
+ enableRestoreFromRecentBackup(context);
+ }
+ }
+
+ /**
+ * Copies the contents of the backup directory to the restore directory.
+ *
+ * @param context The application context, used to retrieve the files directory.
+ */
+ private static void copyContentsFromBackupToRestoreDirectory(@NonNull Context context) {
+ deleteRestoreDirectory(context);
+
+ File filesDir = context.getFilesDir();
+ File backupDir = new File(filesDir, BACKUP_DIRECTORY_NAME);
+ File restoreDir = new File(filesDir, RESTORE_DIRECTORY_NAME);
+
+ try {
+ if (backupDir.exists() && backupDir.isDirectory()) {
+ copyDirectory(backupDir.toPath(), restoreDir.toPath());
+ } else {
+ Log.e(TAG, "Backup directory does not exist.");
+ }
+ } catch (Exception ex) {
+ Log.e(TAG, "Failed to copy backup directory to restore directory", ex);
+ }
+ }
+
+ private static void copyDirectory(Path source, Path target) throws IOException {
+ try (Stream<Path> paths = Files.walk(source)) {
+ paths.forEach(path -> {
+ try {
+ Path destination = target.resolve(source.relativize(path));
+ if (Files.isDirectory(path)) {
+ if (!Files.exists(destination)) {
+ Files.createDirectories(destination);
+ }
+ } else {
+ Files.copy(path, destination, StandardCopyOption.REPLACE_EXISTING);
+ }
+ } catch (IOException e) {
+ Log.e(TAG, "Failed to copy contents of backup "
+ + "directory to restore directory", e);
+ }
+ });
+ }
+ }
+}
diff --git a/src/com/android/providers/media/backupandrestore/RestoreExecutor.java b/src/com/android/providers/media/backupandrestore/RestoreExecutor.java
index e6674d6eb..05db447ff 100644
--- a/src/com/android/providers/media/backupandrestore/RestoreExecutor.java
+++ b/src/com/android/providers/media/backupandrestore/RestoreExecutor.java
@@ -20,13 +20,12 @@ import static android.provider.MediaStore.VOLUME_EXTERNAL_PRIMARY;
import static com.android.providers.media.backupandrestore.BackupAndRestoreUtils.FIELD_SEPARATOR;
import static com.android.providers.media.backupandrestore.BackupAndRestoreUtils.KEY_VALUE_SEPARATOR;
-import static com.android.providers.media.backupandrestore.BackupAndRestoreUtils.RESTORE_COMPLETED;
import static com.android.providers.media.backupandrestore.BackupAndRestoreUtils.RESTORE_DIRECTORY_NAME;
-import static com.android.providers.media.backupandrestore.BackupExecutor.isBackupAndRestoreSupported;
+import static com.android.providers.media.backupandrestore.BackupAndRestoreUtils.isBackupAndRestoreSupported;
+import static com.android.providers.media.backupandrestore.BackupAndRestoreUtils.isRestoringFromRecentBackup;
import android.content.ContentValues;
import android.content.Context;
-import android.content.SharedPreferences;
import com.android.providers.media.leveldb.LevelDBInstance;
import com.android.providers.media.leveldb.LevelDBManager;
@@ -84,14 +83,6 @@ public final class RestoreExecutor {
return Optional.of(contentValues);
}
- private static boolean isRestoringFromRecentBackup(Context context) {
- // Shared preference with key "RESTORE_COMPLETED" should be set to true for recovery to
- // take place.
- SharedPreferences sharedPreferences = context.getSharedPreferences(
- BackupAndRestoreUtils.SHARED_PREFERENCE_NAME, Context.MODE_PRIVATE);
- return sharedPreferences.getBoolean(RESTORE_COMPLETED, false);
- }
-
private Map<String, String> deSerialiseValueString(String valueString) {
String[] values = valueString.split(FIELD_SEPARATOR);
Map<String, String> map = new HashMap<>();
diff --git a/src/com/android/providers/media/scan/ModernMediaScanner.java b/src/com/android/providers/media/scan/ModernMediaScanner.java
index d7f146f26..8563cd8a2 100644
--- a/src/com/android/providers/media/scan/ModernMediaScanner.java
+++ b/src/com/android/providers/media/scan/ModernMediaScanner.java
@@ -47,6 +47,7 @@ import static android.media.MediaMetadataRetriever.METADATA_KEY_WRITER;
import static android.media.MediaMetadataRetriever.METADATA_KEY_YEAR;
import static android.provider.MediaStore.AUTHORITY;
import static android.provider.MediaStore.UNKNOWN_STRING;
+import static android.provider.MediaStore.VOLUME_EXTERNAL;
import static android.text.format.DateUtils.HOUR_IN_MILLIS;
import static android.text.format.DateUtils.MINUTE_IN_MILLIS;
@@ -1212,21 +1213,20 @@ public class ModernMediaScanner implements MediaScanner {
}
// Recovery is performed on first scan of file in target device
- if (existingId == -1) {
- try {
- if (restoreExecutor != null) {
- Optional<ContentValues> restoredDataOptional = restoreExecutor
- .getMetadataForFileIfBackedUp(file.getAbsolutePath(), mContext);
- if (restoredDataOptional.isPresent()) {
- ContentValues valuesRestored = restoredDataOptional.get();
- if (isRestoredMetadataOfActualFile(valuesRestored, attrs)) {
- return restoreDataFromBackup(valuesRestored, file, attrs, mimeType);
- }
+ try {
+ if (restoreExecutor != null) {
+ Optional<ContentValues> restoredDataOptional = restoreExecutor
+ .getMetadataForFileIfBackedUp(file.getAbsolutePath(), mContext);
+ if (restoredDataOptional.isPresent()) {
+ ContentValues valuesRestored = restoredDataOptional.get();
+ if (isRestoredMetadataOfActualFile(valuesRestored, attrs)) {
+ return restoreDataFromBackup(valuesRestored, file, attrs, mimeType,
+ existingId);
}
}
- } catch (Exception e) {
- Log.e(TAG, "Error while attempting to restore metadata from backup", e);
}
+ } catch (Exception e) {
+ Log.e(TAG, "Error while attempting to restore metadata from backup", e);
}
switch (mediaType) {
@@ -1259,8 +1259,9 @@ public class ModernMediaScanner implements MediaScanner {
}
private ContentProviderOperation.Builder restoreDataFromBackup(
- ContentValues restoredValues, File file, BasicFileAttributes attrs, String mimeType) {
- final ContentProviderOperation.Builder op = newUpsert(MediaStore.VOLUME_EXTERNAL, -1);
+ ContentValues restoredValues, File file, BasicFileAttributes attrs, String mimeType,
+ long existingId) {
+ final ContentProviderOperation.Builder op = newUpsert(VOLUME_EXTERNAL, existingId);
withGenericValues(op, file, attrs, mimeType, /* mediaType */ null);
op.withValues(restoredValues);
return op;
diff --git a/tests/src/com/android/providers/media/backupandrestore/BackupAndRestoreTestUtils.java b/tests/src/com/android/providers/media/backupandrestore/BackupAndRestoreTestUtils.java
new file mode 100644
index 000000000..228be1b40
--- /dev/null
+++ b/tests/src/com/android/providers/media/backupandrestore/BackupAndRestoreTestUtils.java
@@ -0,0 +1,175 @@
+/*
+ * Copyright (C) 2025 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.providers.media.backupandrestore;
+
+import static com.android.providers.media.backupandrestore.BackupAndRestoreUtils.FIELD_SEPARATOR;
+import static com.android.providers.media.backupandrestore.BackupAndRestoreUtils.KEY_VALUE_SEPARATOR;
+import static com.android.providers.media.backupandrestore.BackupAndRestoreUtils.RESTORE_COMPLETED;
+import static com.android.providers.media.backupandrestore.BackupAndRestoreUtils.SHARED_PREFERENCE_NAME;
+
+import android.content.Context;
+import android.provider.MediaStore;
+
+import java.util.HashMap;
+import java.util.Map;
+
+public class BackupAndRestoreTestUtils {
+
+ /**
+ * Map used to store column name for given key id.
+ */
+ private static Map<String, String> sColumnIdToKeyMap;
+
+ /**
+ * Map used to store key id for given column name.
+ */
+ private static Map<String, String> sColumnNameToIdMap;
+
+ static void createKeyToColumnNameMap() {
+ sColumnIdToKeyMap = new HashMap<>();
+ sColumnIdToKeyMap.put("0", MediaStore.Files.FileColumns.IS_FAVORITE);
+ sColumnIdToKeyMap.put("1", MediaStore.Files.FileColumns.MEDIA_TYPE);
+ sColumnIdToKeyMap.put("2", MediaStore.Files.FileColumns.MIME_TYPE);
+ sColumnIdToKeyMap.put("3", MediaStore.Files.FileColumns._USER_ID);
+ sColumnIdToKeyMap.put("4", MediaStore.Files.FileColumns.SIZE);
+ sColumnIdToKeyMap.put("5", MediaStore.MediaColumns.DATE_TAKEN);
+ sColumnIdToKeyMap.put("6", MediaStore.MediaColumns.CD_TRACK_NUMBER);
+ sColumnIdToKeyMap.put("7", MediaStore.MediaColumns.ALBUM);
+ sColumnIdToKeyMap.put("8", MediaStore.MediaColumns.ARTIST);
+ sColumnIdToKeyMap.put("9", MediaStore.MediaColumns.AUTHOR);
+ sColumnIdToKeyMap.put("10", MediaStore.MediaColumns.COMPOSER);
+ sColumnIdToKeyMap.put("11", MediaStore.MediaColumns.GENRE);
+ sColumnIdToKeyMap.put("12", MediaStore.MediaColumns.TITLE);
+ sColumnIdToKeyMap.put("13", MediaStore.MediaColumns.YEAR);
+ sColumnIdToKeyMap.put("14", MediaStore.MediaColumns.DURATION);
+ sColumnIdToKeyMap.put("15", MediaStore.MediaColumns.NUM_TRACKS);
+ sColumnIdToKeyMap.put("16", MediaStore.MediaColumns.WRITER);
+ sColumnIdToKeyMap.put("17", MediaStore.MediaColumns.ALBUM_ARTIST);
+ sColumnIdToKeyMap.put("18", MediaStore.MediaColumns.DISC_NUMBER);
+ sColumnIdToKeyMap.put("19", MediaStore.MediaColumns.COMPILATION);
+ sColumnIdToKeyMap.put("20", MediaStore.MediaColumns.BITRATE);
+ sColumnIdToKeyMap.put("21", MediaStore.MediaColumns.CAPTURE_FRAMERATE);
+ sColumnIdToKeyMap.put("22", MediaStore.Audio.AudioColumns.TRACK);
+ sColumnIdToKeyMap.put("23", MediaStore.MediaColumns.DOCUMENT_ID);
+ sColumnIdToKeyMap.put("24", MediaStore.MediaColumns.INSTANCE_ID);
+ sColumnIdToKeyMap.put("25", MediaStore.MediaColumns.ORIGINAL_DOCUMENT_ID);
+ sColumnIdToKeyMap.put("26", MediaStore.MediaColumns.RESOLUTION);
+ sColumnIdToKeyMap.put("27", MediaStore.MediaColumns.ORIENTATION);
+ sColumnIdToKeyMap.put("28", MediaStore.Video.VideoColumns.COLOR_STANDARD);
+ sColumnIdToKeyMap.put("29", MediaStore.Video.VideoColumns.COLOR_TRANSFER);
+ sColumnIdToKeyMap.put("30", MediaStore.Video.VideoColumns.COLOR_RANGE);
+ sColumnIdToKeyMap.put("31", MediaStore.Files.FileColumns._VIDEO_CODEC_TYPE);
+ sColumnIdToKeyMap.put("32", MediaStore.MediaColumns.WIDTH);
+ sColumnIdToKeyMap.put("33", MediaStore.MediaColumns.HEIGHT);
+ sColumnIdToKeyMap.put("34", MediaStore.Images.ImageColumns.DESCRIPTION);
+ sColumnIdToKeyMap.put("35", MediaStore.Images.ImageColumns.EXPOSURE_TIME);
+ sColumnIdToKeyMap.put("36", MediaStore.Images.ImageColumns.F_NUMBER);
+ sColumnIdToKeyMap.put("37", MediaStore.Images.ImageColumns.ISO);
+ sColumnIdToKeyMap.put("38", MediaStore.Images.ImageColumns.SCENE_CAPTURE_TYPE);
+ sColumnIdToKeyMap.put("39", MediaStore.Files.FileColumns._SPECIAL_FORMAT);
+ sColumnIdToKeyMap.put("40", MediaStore.Files.FileColumns.OWNER_PACKAGE_NAME);
+ // Adding number gap to allow addition of new values
+ sColumnIdToKeyMap.put("80", MediaStore.MediaColumns.XMP);
+ }
+
+ static void createColumnNameToKeyMap() {
+ sColumnNameToIdMap = new HashMap<>();
+ sColumnNameToIdMap.put(MediaStore.Files.FileColumns.IS_FAVORITE, "0");
+ sColumnNameToIdMap.put(MediaStore.Files.FileColumns.MEDIA_TYPE, "1");
+ sColumnNameToIdMap.put(MediaStore.Files.FileColumns.MIME_TYPE, "2");
+ sColumnNameToIdMap.put(MediaStore.Files.FileColumns._USER_ID, "3");
+ sColumnNameToIdMap.put(MediaStore.Files.FileColumns.SIZE, "4");
+ sColumnNameToIdMap.put(MediaStore.MediaColumns.DATE_TAKEN, "5");
+ sColumnNameToIdMap.put(MediaStore.MediaColumns.CD_TRACK_NUMBER, "6");
+ sColumnNameToIdMap.put(MediaStore.MediaColumns.ALBUM, "7");
+ sColumnNameToIdMap.put(MediaStore.MediaColumns.ARTIST, "8");
+ sColumnNameToIdMap.put(MediaStore.MediaColumns.AUTHOR, "9");
+ sColumnNameToIdMap.put(MediaStore.MediaColumns.COMPOSER, "10");
+ sColumnNameToIdMap.put(MediaStore.MediaColumns.GENRE, "11");
+ sColumnNameToIdMap.put(MediaStore.MediaColumns.TITLE, "12");
+ sColumnNameToIdMap.put(MediaStore.MediaColumns.YEAR, "13");
+ sColumnNameToIdMap.put(MediaStore.MediaColumns.DURATION, "14");
+ sColumnNameToIdMap.put(MediaStore.MediaColumns.NUM_TRACKS, "15");
+ sColumnNameToIdMap.put(MediaStore.MediaColumns.WRITER, "16");
+ sColumnNameToIdMap.put(MediaStore.MediaColumns.ALBUM_ARTIST, "17");
+ sColumnNameToIdMap.put(MediaStore.MediaColumns.DISC_NUMBER, "18");
+ sColumnNameToIdMap.put(MediaStore.MediaColumns.COMPILATION, "19");
+ sColumnNameToIdMap.put(MediaStore.MediaColumns.BITRATE, "20");
+ sColumnNameToIdMap.put(MediaStore.MediaColumns.CAPTURE_FRAMERATE, "21");
+ sColumnNameToIdMap.put(MediaStore.Audio.AudioColumns.TRACK, "22");
+ sColumnNameToIdMap.put(MediaStore.MediaColumns.DOCUMENT_ID, "23");
+ sColumnNameToIdMap.put(MediaStore.MediaColumns.INSTANCE_ID, "24");
+ sColumnNameToIdMap.put(MediaStore.MediaColumns.ORIGINAL_DOCUMENT_ID, "25");
+ sColumnNameToIdMap.put(MediaStore.MediaColumns.RESOLUTION, "26");
+ sColumnNameToIdMap.put(MediaStore.MediaColumns.ORIENTATION, "27");
+ sColumnNameToIdMap.put(MediaStore.Video.VideoColumns.COLOR_STANDARD, "28");
+ sColumnNameToIdMap.put(MediaStore.Video.VideoColumns.COLOR_TRANSFER, "29");
+ sColumnNameToIdMap.put(MediaStore.Video.VideoColumns.COLOR_RANGE, "30");
+ sColumnNameToIdMap.put(MediaStore.Files.FileColumns._VIDEO_CODEC_TYPE, "31");
+ sColumnNameToIdMap.put(MediaStore.MediaColumns.WIDTH, "32");
+ sColumnNameToIdMap.put(MediaStore.MediaColumns.HEIGHT, "33");
+ sColumnNameToIdMap.put(MediaStore.Images.ImageColumns.DESCRIPTION, "34");
+ sColumnNameToIdMap.put(MediaStore.Images.ImageColumns.EXPOSURE_TIME, "35");
+ sColumnNameToIdMap.put(MediaStore.Images.ImageColumns.F_NUMBER, "36");
+ sColumnNameToIdMap.put(MediaStore.Images.ImageColumns.ISO, "37");
+ sColumnNameToIdMap.put(MediaStore.Images.ImageColumns.SCENE_CAPTURE_TYPE, "38");
+ sColumnNameToIdMap.put(MediaStore.Files.FileColumns._SPECIAL_FORMAT, "39");
+ sColumnNameToIdMap.put(MediaStore.Files.FileColumns.OWNER_PACKAGE_NAME, "40");
+ // Adding number gap to allow addition of new values
+ sColumnNameToIdMap.put(MediaStore.MediaColumns.XMP, "80");
+ }
+
+ static Map<String, String> deSerialiseValueString(String valueString) {
+ if (sColumnIdToKeyMap == null) {
+ createKeyToColumnNameMap();
+ }
+
+ String[] values = valueString.split(":::");
+ Map<String, String> map = new HashMap<>();
+ for (String value : values) {
+ if (value == null || value.isEmpty()) {
+ continue;
+ }
+
+ String[] keyValue = value.split("=", 2);
+ map.put(sColumnIdToKeyMap.get(keyValue[0]), keyValue[1]);
+ }
+
+ return map;
+ }
+
+ static String createSerialisedValue(Map<String, String> entries) {
+ if (sColumnNameToIdMap == null) {
+ createColumnNameToKeyMap();
+ }
+
+ StringBuilder sb = new StringBuilder();
+ for (String backupColumn : sColumnNameToIdMap.keySet()) {
+ if (entries.containsKey(backupColumn)) {
+ sb.append(sColumnNameToIdMap.get(backupColumn)).append(KEY_VALUE_SEPARATOR).append(
+ entries.get(backupColumn));
+ sb.append(FIELD_SEPARATOR);
+ }
+ }
+ return sb.toString();
+ }
+
+ static boolean getSharedPreferenceValue(Context context) {
+ return context.getSharedPreferences(SHARED_PREFERENCE_NAME,
+ Context.MODE_PRIVATE).getBoolean(RESTORE_COMPLETED, false);
+ }
+}
diff --git a/tests/src/com/android/providers/media/backupandrestore/BackupExecutorTest.java b/tests/src/com/android/providers/media/backupandrestore/BackupExecutorTest.java
index d464dffc3..70c7eb373 100644
--- a/tests/src/com/android/providers/media/backupandrestore/BackupExecutorTest.java
+++ b/tests/src/com/android/providers/media/backupandrestore/BackupExecutorTest.java
@@ -16,6 +16,7 @@
package com.android.providers.media.backupandrestore;
+import static com.android.providers.media.backupandrestore.BackupAndRestoreTestUtils.deSerialiseValueString;
import static com.android.providers.media.backupandrestore.BackupAndRestoreUtils.BACKUP_COLUMNS;
import static com.android.providers.media.scan.MediaScanner.REASON_UNKNOWN;
import static com.android.providers.media.scan.MediaScannerTest.stage;
@@ -51,7 +52,6 @@ import com.android.providers.media.util.FileUtils;
import org.junit.After;
import org.junit.Before;
-import org.junit.BeforeClass;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -75,11 +75,6 @@ public final class BackupExecutorTest {
@Rule
public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule();
- /**
- * Map used to store key id for given column and vice versa.
- */
- private static Map<String, String> sColumnIdToKeyMap;
-
private Set<File> mStagedFiles = new HashSet<>();
private Context mIsolatedContext;
@@ -90,11 +85,6 @@ public final class BackupExecutorTest {
private File mDownloadsDir;
- @BeforeClass
- public static void setupBeforeClass() {
- createColumnToKeyMap();
- }
-
private String mLevelDbPath;
@Before
@@ -216,71 +206,9 @@ public final class BackupExecutorTest {
}
}
- static Map<String, String> deSerialiseValueString(String valueString) {
- String[] values = valueString.split(":::");
- Map<String, String> map = new HashMap<>();
- for (String value : values) {
- if (value == null || value.isEmpty()) {
- continue;
- }
-
- String[] keyValue = value.split("=", 2);
- map.put(sColumnIdToKeyMap.get(keyValue[0]), keyValue[1]);
- }
-
- return map;
- }
-
private void stageNewFile(int resId, File file) throws IOException {
file.createNewFile();
mStagedFiles.add(file);
stage(resId, file);
}
-
- static void createColumnToKeyMap() {
- sColumnIdToKeyMap = new HashMap<>();
- sColumnIdToKeyMap.put("0", MediaStore.Files.FileColumns.IS_FAVORITE);
- sColumnIdToKeyMap.put("1", MediaStore.Files.FileColumns.MEDIA_TYPE);
- sColumnIdToKeyMap.put("2", MediaStore.Files.FileColumns.MIME_TYPE);
- sColumnIdToKeyMap.put("3", MediaStore.Files.FileColumns._USER_ID);
- sColumnIdToKeyMap.put("4", MediaStore.Files.FileColumns.SIZE);
- sColumnIdToKeyMap.put("5", MediaStore.MediaColumns.DATE_TAKEN);
- sColumnIdToKeyMap.put("6", MediaStore.MediaColumns.CD_TRACK_NUMBER);
- sColumnIdToKeyMap.put("7", MediaStore.MediaColumns.ALBUM);
- sColumnIdToKeyMap.put("8", MediaStore.MediaColumns.ARTIST);
- sColumnIdToKeyMap.put("9", MediaStore.MediaColumns.AUTHOR);
- sColumnIdToKeyMap.put("10", MediaStore.MediaColumns.COMPOSER);
- sColumnIdToKeyMap.put("11", MediaStore.MediaColumns.GENRE);
- sColumnIdToKeyMap.put("12", MediaStore.MediaColumns.TITLE);
- sColumnIdToKeyMap.put("13", MediaStore.MediaColumns.YEAR);
- sColumnIdToKeyMap.put("14", MediaStore.MediaColumns.DURATION);
- sColumnIdToKeyMap.put("15", MediaStore.MediaColumns.NUM_TRACKS);
- sColumnIdToKeyMap.put("16", MediaStore.MediaColumns.WRITER);
- sColumnIdToKeyMap.put("17", MediaStore.MediaColumns.ALBUM_ARTIST);
- sColumnIdToKeyMap.put("18", MediaStore.MediaColumns.DISC_NUMBER);
- sColumnIdToKeyMap.put("19", MediaStore.MediaColumns.COMPILATION);
- sColumnIdToKeyMap.put("20", MediaStore.MediaColumns.BITRATE);
- sColumnIdToKeyMap.put("21", MediaStore.MediaColumns.CAPTURE_FRAMERATE);
- sColumnIdToKeyMap.put("22", MediaStore.Audio.AudioColumns.TRACK);
- sColumnIdToKeyMap.put("23", MediaStore.MediaColumns.DOCUMENT_ID);
- sColumnIdToKeyMap.put("24", MediaStore.MediaColumns.INSTANCE_ID);
- sColumnIdToKeyMap.put("25", MediaStore.MediaColumns.ORIGINAL_DOCUMENT_ID);
- sColumnIdToKeyMap.put("26", MediaStore.MediaColumns.RESOLUTION);
- sColumnIdToKeyMap.put("27", MediaStore.MediaColumns.ORIENTATION);
- sColumnIdToKeyMap.put("28", MediaStore.Video.VideoColumns.COLOR_STANDARD);
- sColumnIdToKeyMap.put("29", MediaStore.Video.VideoColumns.COLOR_TRANSFER);
- sColumnIdToKeyMap.put("30", MediaStore.Video.VideoColumns.COLOR_RANGE);
- sColumnIdToKeyMap.put("31", MediaStore.Files.FileColumns._VIDEO_CODEC_TYPE);
- sColumnIdToKeyMap.put("32", MediaStore.MediaColumns.WIDTH);
- sColumnIdToKeyMap.put("33", MediaStore.MediaColumns.HEIGHT);
- sColumnIdToKeyMap.put("34", MediaStore.Images.ImageColumns.DESCRIPTION);
- sColumnIdToKeyMap.put("35", MediaStore.Images.ImageColumns.EXPOSURE_TIME);
- sColumnIdToKeyMap.put("36", MediaStore.Images.ImageColumns.F_NUMBER);
- sColumnIdToKeyMap.put("37", MediaStore.Images.ImageColumns.ISO);
- sColumnIdToKeyMap.put("38", MediaStore.Images.ImageColumns.SCENE_CAPTURE_TYPE);
- sColumnIdToKeyMap.put("39", MediaStore.Files.FileColumns._SPECIAL_FORMAT);
- sColumnIdToKeyMap.put("40", MediaStore.Files.FileColumns.OWNER_PACKAGE_NAME);
- // Adding number gap to allow addition of new values
- sColumnIdToKeyMap.put("80", MediaStore.MediaColumns.XMP);
- }
}
diff --git a/tests/src/com/android/providers/media/backupandrestore/MediaBackupAgentTest.java b/tests/src/com/android/providers/media/backupandrestore/MediaBackupAgentTest.java
new file mode 100644
index 000000000..5f892f126
--- /dev/null
+++ b/tests/src/com/android/providers/media/backupandrestore/MediaBackupAgentTest.java
@@ -0,0 +1,189 @@
+/*
+ * Copyright (C) 2025 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.providers.media.backupandrestore;
+
+import static com.android.providers.media.backupandrestore.BackupAndRestoreTestUtils.deSerialiseValueString;
+import static com.android.providers.media.backupandrestore.BackupAndRestoreTestUtils.getSharedPreferenceValue;
+import static com.android.providers.media.scan.MediaScanner.REASON_UNKNOWN;
+import static com.android.providers.media.scan.MediaScannerTest.stage;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+import android.Manifest;
+import android.content.ContentResolver;
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Environment;
+import android.os.SystemClock;
+import android.platform.test.annotations.EnableFlags;
+import android.platform.test.flag.junit.SetFlagsRule;
+import android.provider.MediaStore;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SdkSuppress;
+
+import com.android.providers.media.IsolatedContext;
+import com.android.providers.media.R;
+import com.android.providers.media.TestConfigStore;
+import com.android.providers.media.flags.Flags;
+import com.android.providers.media.leveldb.LevelDBInstance;
+import com.android.providers.media.leveldb.LevelDBManager;
+import com.android.providers.media.leveldb.LevelDBResult;
+import com.android.providers.media.scan.ModernMediaScanner;
+import com.android.providers.media.util.FileUtils;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.io.File;
+import java.util.Map;
+
+@RunWith(AndroidJUnit4.class)
+@EnableFlags(Flags.FLAG_ENABLE_BACKUP_AND_RESTORE)
+@SdkSuppress(minSdkVersion = Build.VERSION_CODES.S)
+public class MediaBackupAgentTest {
+ @Rule
+ public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
+
+ private Context mIsolatedContext;
+
+ private ContentResolver mIsolatedResolver;
+
+ private ModernMediaScanner mModern;
+
+ private File mDownloadsDir;
+
+ private File mRestoreDir;
+ private File mBackupDir;
+
+ private String mLevelDbPath;
+ private MediaBackupAgent mMediaBackupAgent;
+
+ @Before
+ public void setUp() {
+ final Context context = InstrumentationRegistry.getTargetContext();
+ androidx.test.platform.app.InstrumentationRegistry.getInstrumentation().getUiAutomation()
+ .adoptShellPermissionIdentity(Manifest.permission.LOG_COMPAT_CHANGE,
+ Manifest.permission.READ_COMPAT_CHANGE_CONFIG,
+ Manifest.permission.DUMP,
+ Manifest.permission.READ_DEVICE_CONFIG);
+
+ mIsolatedContext = new IsolatedContext(context, "modern", /*asFuseThread*/ false);
+ mIsolatedResolver = mIsolatedContext.getContentResolver();
+ mModern = new ModernMediaScanner(mIsolatedContext, new TestConfigStore());
+ mRestoreDir = new File(mIsolatedContext.getFilesDir(), "restore");
+ mBackupDir = new File(mIsolatedContext.getFilesDir(), "backup");
+ mDownloadsDir = new File(Environment.getExternalStorageDirectory(),
+ Environment.DIRECTORY_DOWNLOADS);
+ mLevelDbPath =
+ mIsolatedContext.getFilesDir().getAbsolutePath() + "/backup/external_primary/";
+ FileUtils.deleteContents(mDownloadsDir);
+
+ mMediaBackupAgent = new MediaBackupAgent();
+ mMediaBackupAgent.attach(mIsolatedContext);
+ }
+
+ @Test
+ public void testCompleteFlow() throws Exception {
+ //create new test file & stage it
+ File file = new File(mDownloadsDir, "testImage_"
+ + SystemClock.elapsedRealtimeNanos() + ".jpg");
+ file.createNewFile();
+ stage(R.raw.test_image, file);
+
+ try {
+ String path = file.getAbsolutePath();
+ // scan directory to have entry in files table
+ mModern.scanDirectory(mDownloadsDir, REASON_UNKNOWN);
+
+ // set is_favorite value to 1. We will check this value later after restoration.
+ updateFavoritesValue(path, 1);
+
+ // run idle maintenance, this will save file's metadata in leveldb with is_favorite = 1
+ MediaStore.runIdleMaintenance(mIsolatedResolver);
+ assertTrue(mBackupDir.exists());
+
+ assertLevelDbExistsAndHasLatestValues(path);
+
+ // run the backup agent. This will copy over backup directory to restore directory and
+ // set shared preference.
+ mMediaBackupAgent.onRestoreFinished();
+ assertTrue(getSharedPreferenceValue(mIsolatedContext));
+ assertTrue(mRestoreDir.exists());
+ assertFalse(mBackupDir.exists());
+
+ //delete existing external db database having old values
+ mIsolatedContext.deleteDatabase("external.db");
+
+ // run media scan, this will populate db and read value from backup
+ mModern.scanDirectory(mDownloadsDir, REASON_UNKNOWN);
+ assertEquals(1, queryFavoritesValue(path));
+
+ // on idle maintenance, clean up is called. It should delete restore directory and set
+ // shared preference to false
+ MediaStore.runIdleMaintenance(mIsolatedResolver);
+ assertFalse(getSharedPreferenceValue(mIsolatedContext));
+ assertFalse(mRestoreDir.exists());
+ } finally {
+ file.delete();
+ }
+ }
+
+ private void assertLevelDbExistsAndHasLatestValues(String path) {
+ // check that entry is created in level db for the file
+ LevelDBInstance levelDBInstance = LevelDBManager.getInstance(mLevelDbPath);
+ assertNotNull(levelDBInstance);
+
+ // check that entry created in level db has latest value(is_favorite = 1)
+ LevelDBResult levelDBResult = levelDBInstance.query(path);
+ assertNotNull(levelDBResult);
+ Map<String, String> actualResultMap = deSerialiseValueString(levelDBResult.getValue());
+ assertEquals(1,
+ Integer.parseInt(actualResultMap.get(MediaStore.MediaColumns.IS_FAVORITE)));
+ }
+
+ private void updateFavoritesValue(String path, int value) {
+ Uri uri = MediaStore.Files.getContentUri(MediaStore.VOLUME_EXTERNAL);
+ String selection = MediaStore.Files.FileColumns.DATA + " LIKE ?";
+ String[] selectionArgs = new String[]{path};
+
+ ContentValues values = new ContentValues();
+ values.put(MediaStore.Files.FileColumns.IS_FAVORITE, value);
+ values.put(MediaStore.MediaColumns.DATE_MODIFIED, System.currentTimeMillis());
+
+ mIsolatedResolver.update(uri, values, selection, selectionArgs);
+ }
+
+ private int queryFavoritesValue(String path) {
+ Uri uri = MediaStore.Files.getContentUri(MediaStore.VOLUME_EXTERNAL);
+ String selection = MediaStore.Files.FileColumns.DATA + " LIKE ?";
+ String[] selectionArgs = new String[]{path};
+
+ Cursor cursor = mIsolatedResolver.query(uri, null, selection, selectionArgs, null);
+ cursor.moveToFirst();
+ return cursor.getInt(cursor.getColumnIndex(MediaStore.Files.FileColumns.IS_FAVORITE));
+ }
+}
diff --git a/tests/src/com/android/providers/media/backupandrestore/RestoreExecutorTest.java b/tests/src/com/android/providers/media/backupandrestore/RestoreExecutorTest.java
index ebffecc13..9f0b41a21 100644
--- a/tests/src/com/android/providers/media/backupandrestore/RestoreExecutorTest.java
+++ b/tests/src/com/android/providers/media/backupandrestore/RestoreExecutorTest.java
@@ -16,8 +16,7 @@
package com.android.providers.media.backupandrestore;
-import static com.android.providers.media.backupandrestore.BackupAndRestoreUtils.FIELD_SEPARATOR;
-import static com.android.providers.media.backupandrestore.BackupAndRestoreUtils.KEY_VALUE_SEPARATOR;
+import static com.android.providers.media.backupandrestore.BackupAndRestoreTestUtils.createSerialisedValue;
import static com.android.providers.media.backupandrestore.BackupAndRestoreUtils.RESTORE_COMPLETED;
import static com.android.providers.media.scan.MediaScanner.REASON_UNKNOWN;
import static com.android.providers.media.scan.MediaScannerTest.stage;
@@ -53,7 +52,6 @@ import com.android.providers.media.util.FileUtils;
import org.junit.After;
import org.junit.Before;
-import org.junit.BeforeClass;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -73,11 +71,6 @@ public final class RestoreExecutorTest {
@Rule
public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule();
- /**
- * Map used to store key id for given column and vice versa.
- */
- private static Map<String, String> sColumnNameToIdMap;
-
private Context mIsolatedContext;
private ContentResolver mIsolatedResolver;
@@ -86,11 +79,6 @@ public final class RestoreExecutorTest {
private File mDownloadsDir;
- @BeforeClass
- public static void setupBeforeClass() {
- createColumnToKeyMap();
- }
-
@Before
public void setUp() {
final Context context = InstrumentationRegistry.getTargetContext();
@@ -347,63 +335,4 @@ public final class RestoreExecutorTest {
file.createNewFile();
stage(resId, file);
}
-
- private String createSerialisedValue(Map<String, String> entries) {
- StringBuilder sb = new StringBuilder();
- for (String backupColumn : sColumnNameToIdMap.keySet()) {
- if (entries.containsKey(backupColumn)) {
- sb.append(sColumnNameToIdMap.get(backupColumn)).append(KEY_VALUE_SEPARATOR).append(
- entries.get(backupColumn));
- sb.append(FIELD_SEPARATOR);
- }
- }
- return sb.toString();
- }
-
- private static void createColumnToKeyMap() {
- sColumnNameToIdMap = new HashMap<>();
- sColumnNameToIdMap.put(MediaStore.Files.FileColumns.IS_FAVORITE, "0");
- sColumnNameToIdMap.put(MediaStore.Files.FileColumns.MEDIA_TYPE, "1");
- sColumnNameToIdMap.put(MediaStore.Files.FileColumns.MIME_TYPE, "2");
- sColumnNameToIdMap.put(MediaStore.Files.FileColumns._USER_ID, "3");
- sColumnNameToIdMap.put(MediaStore.Files.FileColumns.SIZE, "4");
- sColumnNameToIdMap.put(MediaStore.MediaColumns.DATE_TAKEN, "5");
- sColumnNameToIdMap.put(MediaStore.MediaColumns.CD_TRACK_NUMBER, "6");
- sColumnNameToIdMap.put(MediaStore.MediaColumns.ALBUM, "7");
- sColumnNameToIdMap.put(MediaStore.MediaColumns.ARTIST, "8");
- sColumnNameToIdMap.put(MediaStore.MediaColumns.AUTHOR, "9");
- sColumnNameToIdMap.put(MediaStore.MediaColumns.COMPOSER, "10");
- sColumnNameToIdMap.put(MediaStore.MediaColumns.GENRE, "11");
- sColumnNameToIdMap.put(MediaStore.MediaColumns.TITLE, "12");
- sColumnNameToIdMap.put(MediaStore.MediaColumns.YEAR, "13");
- sColumnNameToIdMap.put(MediaStore.MediaColumns.DURATION, "14");
- sColumnNameToIdMap.put(MediaStore.MediaColumns.NUM_TRACKS, "15");
- sColumnNameToIdMap.put(MediaStore.MediaColumns.WRITER, "16");
- sColumnNameToIdMap.put(MediaStore.MediaColumns.ALBUM_ARTIST, "17");
- sColumnNameToIdMap.put(MediaStore.MediaColumns.DISC_NUMBER, "18");
- sColumnNameToIdMap.put(MediaStore.MediaColumns.COMPILATION, "19");
- sColumnNameToIdMap.put(MediaStore.MediaColumns.BITRATE, "20");
- sColumnNameToIdMap.put(MediaStore.MediaColumns.CAPTURE_FRAMERATE, "21");
- sColumnNameToIdMap.put(MediaStore.Audio.AudioColumns.TRACK, "22");
- sColumnNameToIdMap.put(MediaStore.MediaColumns.DOCUMENT_ID, "23");
- sColumnNameToIdMap.put(MediaStore.MediaColumns.INSTANCE_ID, "24");
- sColumnNameToIdMap.put(MediaStore.MediaColumns.ORIGINAL_DOCUMENT_ID, "25");
- sColumnNameToIdMap.put(MediaStore.MediaColumns.RESOLUTION, "26");
- sColumnNameToIdMap.put(MediaStore.MediaColumns.ORIENTATION, "27");
- sColumnNameToIdMap.put(MediaStore.Video.VideoColumns.COLOR_STANDARD, "28");
- sColumnNameToIdMap.put(MediaStore.Video.VideoColumns.COLOR_TRANSFER, "29");
- sColumnNameToIdMap.put(MediaStore.Video.VideoColumns.COLOR_RANGE, "30");
- sColumnNameToIdMap.put(MediaStore.Files.FileColumns._VIDEO_CODEC_TYPE, "31");
- sColumnNameToIdMap.put(MediaStore.MediaColumns.WIDTH, "32");
- sColumnNameToIdMap.put(MediaStore.MediaColumns.HEIGHT, "33");
- sColumnNameToIdMap.put(MediaStore.Images.ImageColumns.DESCRIPTION, "34");
- sColumnNameToIdMap.put(MediaStore.Images.ImageColumns.EXPOSURE_TIME, "35");
- sColumnNameToIdMap.put(MediaStore.Images.ImageColumns.F_NUMBER, "36");
- sColumnNameToIdMap.put(MediaStore.Images.ImageColumns.ISO, "37");
- sColumnNameToIdMap.put(MediaStore.Images.ImageColumns.SCENE_CAPTURE_TYPE, "38");
- sColumnNameToIdMap.put(MediaStore.Files.FileColumns._SPECIAL_FORMAT, "39");
- sColumnNameToIdMap.put(MediaStore.Files.FileColumns.OWNER_PACKAGE_NAME, "40");
- // Adding number gap to allow addition of new values
- sColumnNameToIdMap.put(MediaStore.MediaColumns.XMP, "80");
- }
}