summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-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
6 files changed, 320 insertions, 56 deletions
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;