diff options
Diffstat (limited to 'src')
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; |