• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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