1 /* 2 * Copyright (C) 2024 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.providers.media.backupandrestore; 18 19 import static android.provider.MediaStore.VOLUME_EXTERNAL_PRIMARY; 20 21 import static com.android.providers.media.backupandrestore.BackupAndRestoreUtils.BACKUP_COLUMNS; 22 import static com.android.providers.media.backupandrestore.BackupAndRestoreUtils.BACKUP_DIRECTORY_NAME; 23 import static com.android.providers.media.backupandrestore.BackupAndRestoreUtils.FIELD_SEPARATOR; 24 import static com.android.providers.media.backupandrestore.BackupAndRestoreUtils.KEY_VALUE_SEPARATOR; 25 import static com.android.providers.media.backupandrestore.BackupAndRestoreUtils.isBackupAndRestoreSupported; 26 import static com.android.providers.media.util.Logging.TAG; 27 28 import android.annotation.SuppressLint; 29 import android.content.Context; 30 import android.database.Cursor; 31 import android.os.CancellationSignal; 32 import android.provider.MediaStore.Files.FileColumns; 33 import android.provider.MediaStore.MediaColumns; 34 import android.util.Log; 35 36 import com.android.providers.media.DatabaseHelper; 37 import com.android.providers.media.leveldb.LevelDBEntry; 38 import com.android.providers.media.leveldb.LevelDBInstance; 39 import com.android.providers.media.leveldb.LevelDBManager; 40 import com.android.providers.media.leveldb.LevelDBResult; 41 42 import com.google.common.collect.BiMap; 43 44 import java.io.File; 45 import java.util.ArrayList; 46 import java.util.Arrays; 47 import java.util.List; 48 import java.util.Optional; 49 50 /** 51 * Class containing implementation details for backing up files table data to leveldb. 52 */ 53 public final class BackupExecutor { 54 55 private static final String EXTERNAL_PRIMARY_VOLUME_CLAUSE = 56 FileColumns.VOLUME_NAME + " = '" + VOLUME_EXTERNAL_PRIMARY + "'"; 57 58 private static final String SCANNER_AS_MODIFIER_CLAUSE = FileColumns._MODIFIER + " = 3"; 59 60 private static final String FILE_NOT_PENDING_CLAUSE = FileColumns.IS_PENDING + " = 0"; 61 62 private static final String MIME_TYPE_CLAUSE = FileColumns.MIME_TYPE + " IS NOT NULL"; 63 64 private static final String AND_CONNECTOR = " AND "; 65 66 /** 67 * Key corresponding to which last backed up generation number is stored. 68 */ 69 private static final String LAST_BACKED_GENERATION_NUMBER_KEY = "LAST_BACKED_GENERATION_NUMBER"; 70 71 /** 72 * Name of files table in MediaProvider database. 73 */ 74 private static final String FILES_TABLE_NAME = "files"; 75 76 private final Context mContext; 77 78 private final DatabaseHelper mExternalDatabaseHelper; 79 80 private LevelDBInstance mLevelDBInstance; 81 BackupExecutor(Context context, DatabaseHelper databaseHelper)82 public BackupExecutor(Context context, DatabaseHelper databaseHelper) { 83 mContext = context; 84 mExternalDatabaseHelper = databaseHelper; 85 } 86 87 /** 88 * Addresses the following:- 89 * 1. Gets last backed generation number from leveldb 90 * 2. Backs up data for rows greater than last backed generation number 91 * 3. Updates the new backed up generation number 92 */ doBackup(CancellationSignal signal)93 public void doBackup(CancellationSignal signal) { 94 if (!isBackupAndRestoreSupported(mContext)) { 95 return; 96 } 97 Log.v(TAG, "Backup is enabled"); 98 99 if (mLevelDBInstance == null) { 100 mLevelDBInstance = LevelDBManager.getInstance(getBackupFilePath()); 101 } 102 final long lastBackedUpGenerationNumberFromLevelDb = getLastBackedUpGenerationNumber(); 103 final long currentDbGenerationNumber = mExternalDatabaseHelper.runWithoutTransaction( 104 DatabaseHelper::getGeneration); 105 final long lastBackedUpGenerationNumber = clearBackupIfNeededAndReturnLastBackedUpNumber( 106 currentDbGenerationNumber, lastBackedUpGenerationNumberFromLevelDb); 107 Log.v(TAG, "Last backed up generation number: " + lastBackedUpGenerationNumber); 108 long lastGenerationNumber = backupData(lastBackedUpGenerationNumber, signal); 109 updateLastBackedUpGenerationNumber(lastGenerationNumber); 110 } 111 clearBackupIfNeededAndReturnLastBackedUpNumber(long currentDbGenerationNumber, long lastBackedUpGenerationNumber)112 private long clearBackupIfNeededAndReturnLastBackedUpNumber(long currentDbGenerationNumber, 113 long lastBackedUpGenerationNumber) { 114 if (currentDbGenerationNumber < lastBackedUpGenerationNumber) { 115 // If DB generation number is lesser than last backed, we would have to re-sync 116 // everything 117 mLevelDBInstance = LevelDBManager.recreate(getBackupFilePath()); 118 return 0; 119 } 120 121 return lastBackedUpGenerationNumber; 122 } 123 124 @SuppressLint("Range") backupData(long lastBackedUpGenerationNumber, CancellationSignal signal)125 private long backupData(long lastBackedUpGenerationNumber, CancellationSignal signal) { 126 List<String> queryColumns = new ArrayList<>(Arrays.asList(BACKUP_COLUMNS)); 127 queryColumns.addAll(Arrays.asList(FileColumns.DATA, FileColumns.GENERATION_MODIFIED)); 128 final String selectionClause = prepareSelectionClause(lastBackedUpGenerationNumber); 129 return mExternalDatabaseHelper.runWithTransaction((db) -> { 130 long maxGeneration = lastBackedUpGenerationNumber; 131 try (Cursor c = db.query(true, FILES_TABLE_NAME, 132 queryColumns.stream().toArray(String[]::new), 133 selectionClause, null, null, null, MediaColumns.GENERATION_MODIFIED + " ASC", 134 null, signal)) { 135 while (c.moveToNext()) { 136 if (signal != null && signal.isCanceled()) { 137 Log.i(TAG, "Received a cancellation signal during the backup process"); 138 break; 139 } 140 141 backupDataValues(c); 142 maxGeneration = Math.max(maxGeneration, 143 c.getLong(c.getColumnIndex(FileColumns.GENERATION_MODIFIED))); 144 } 145 } catch (Exception e) { 146 Log.e(TAG, "Failure in backing up for B&R ", e); 147 } 148 return maxGeneration; 149 }); 150 } 151 152 @SuppressLint("Range") backupDataValues(Cursor c)153 private void backupDataValues(Cursor c) { 154 String data = c.getString(c.getColumnIndex(FileColumns.DATA)); 155 // Skip backing up directories 156 if (new File(data).isDirectory()) { 157 return; 158 } 159 160 mLevelDBInstance.insert(new LevelDBEntry(data, serialiseValueString(c))); 161 } 162 serialiseValueString(Cursor c)163 private static String serialiseValueString(Cursor c) { 164 StringBuilder sb = new StringBuilder(); 165 BiMap<String, String> columnToIdBiMap = BackupAndRestoreUtils.sIdToColumnBiMap.inverse(); 166 for (String backupColumn : BACKUP_COLUMNS) { 167 Optional<String> optionalValue = extractValue(c, backupColumn); 168 if (!optionalValue.isPresent()) { 169 continue; 170 } 171 172 sb.append(columnToIdBiMap.get(backupColumn)).append(KEY_VALUE_SEPARATOR).append( 173 optionalValue.get()); 174 sb.append(FIELD_SEPARATOR); 175 } 176 return sb.toString(); 177 } 178 179 @SuppressLint("Range") extractValue(Cursor c, String col)180 static Optional<String> extractValue(Cursor c, String col) { 181 int columnIndex = c.getColumnIndex(col); 182 int fieldType = c.getType(columnIndex); 183 switch (fieldType) { 184 case Cursor.FIELD_TYPE_STRING -> { 185 String stringValue = c.getString(columnIndex); 186 if (stringValue == null || stringValue.isEmpty()) { 187 return Optional.empty(); 188 } 189 return Optional.of(stringValue); 190 } 191 case Cursor.FIELD_TYPE_INTEGER -> { 192 long longValue = c.getLong(columnIndex); 193 return Optional.of(String.valueOf(longValue)); 194 } 195 case Cursor.FIELD_TYPE_FLOAT -> { 196 float floatValue = c.getFloat(columnIndex); 197 return Optional.of(String.valueOf(floatValue)); 198 } 199 case Cursor.FIELD_TYPE_BLOB -> { 200 byte[] bytes = c.getBlob(columnIndex); 201 if (bytes == null || bytes.length == 0) { 202 return Optional.empty(); 203 } 204 return Optional.of(new String(bytes)); 205 } 206 case Cursor.FIELD_TYPE_NULL -> { 207 return Optional.empty(); 208 } 209 default -> { 210 Log.e(TAG, "Column type not supported for backup: " + col); 211 return Optional.empty(); 212 } 213 } 214 } 215 prepareSelectionClause(long lastBackedUpGenerationNumber)216 private String prepareSelectionClause(long lastBackedUpGenerationNumber) { 217 // Last scan might have not finished for last gen number if cancellation signal is triggered 218 final String generationClause = FileColumns.GENERATION_MODIFIED + " >= " 219 + lastBackedUpGenerationNumber; 220 // Only scanned files are expected to have corresponding metadata in DB, hence this check. 221 return generationClause 222 + AND_CONNECTOR 223 + EXTERNAL_PRIMARY_VOLUME_CLAUSE 224 + AND_CONNECTOR 225 + FILE_NOT_PENDING_CLAUSE 226 + AND_CONNECTOR 227 + MIME_TYPE_CLAUSE 228 + AND_CONNECTOR 229 + SCANNER_AS_MODIFIER_CLAUSE; 230 } 231 getLastBackedUpGenerationNumber()232 private long getLastBackedUpGenerationNumber() { 233 LevelDBResult levelDBResult = mLevelDBInstance.query(LAST_BACKED_GENERATION_NUMBER_KEY); 234 if (!levelDBResult.isSuccess() && !levelDBResult.isNotFound()) { 235 throw new IllegalStateException("Error in fetching last backed up generation number : " 236 + levelDBResult.getErrorMessage()); 237 } 238 239 String value = levelDBResult.getValue(); 240 if (levelDBResult.isNotFound() || value == null || value.isEmpty()) { 241 return 0L; 242 } 243 244 return Long.parseLong(value); 245 } 246 updateLastBackedUpGenerationNumber(long lastGenerationNumber)247 private void updateLastBackedUpGenerationNumber(long lastGenerationNumber) { 248 LevelDBResult levelDBResult = mLevelDBInstance.insert( 249 new LevelDBEntry(LAST_BACKED_GENERATION_NUMBER_KEY, 250 String.valueOf(lastGenerationNumber))); 251 if (!levelDBResult.isSuccess()) { 252 throw new IllegalStateException("Error in inserting last backed up generation number : " 253 + levelDBResult.getErrorMessage()); 254 } 255 } 256 257 /** 258 * Returns backup file path based on the volume name. 259 */ getBackupFilePath()260 private String getBackupFilePath() { 261 String backupDirectory = 262 mContext.getFilesDir().getAbsolutePath() + "/" + BACKUP_DIRECTORY_NAME + "/"; 263 File backupDir = new File(backupDirectory + VOLUME_EXTERNAL_PRIMARY + "/"); 264 if (!backupDir.exists()) { 265 backupDir.mkdirs(); 266 } 267 268 return backupDir.getAbsolutePath(); 269 } 270 271 /** 272 * Removes entry for given file path from Backup. 273 */ deleteBackupForPath(String path)274 public void deleteBackupForPath(String path) { 275 if (isBackupAndRestoreSupported(mContext) && path != null && mLevelDBInstance != null) { 276 mLevelDBInstance.delete(path); 277 } 278 } 279 } 280