• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2022 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;
18 
19 import static com.android.providers.media.MediaProviderStatsLog.MEDIA_PROVIDER_VOLUME_RECOVERY_REPORTED__VOLUME__EXTERNAL_PRIMARY;
20 import static com.android.providers.media.MediaProviderStatsLog.MEDIA_PROVIDER_VOLUME_RECOVERY_REPORTED__VOLUME__INTERNAL;
21 import static com.android.providers.media.MediaProviderStatsLog.MEDIA_PROVIDER_VOLUME_RECOVERY_REPORTED__VOLUME__PUBLIC;
22 import static com.android.providers.media.util.Logging.TAG;
23 
24 import android.content.ContentValues;
25 import android.database.Cursor;
26 import android.database.sqlite.SQLiteDatabase;
27 import android.os.CancellationSignal;
28 import android.os.ParcelFileDescriptor;
29 import android.os.SystemClock;
30 import android.os.SystemProperties;
31 import android.os.UserHandle;
32 import android.provider.MediaStore;
33 import android.system.Os;
34 import android.util.Log;
35 import android.util.Pair;
36 
37 import androidx.annotation.NonNull;
38 
39 import com.android.providers.media.dao.FileRow;
40 import com.android.providers.media.fuse.FuseDaemon;
41 import com.android.providers.media.stableuris.dao.BackupIdRow;
42 import com.android.providers.media.util.StringUtils;
43 
44 import com.google.common.base.Strings;
45 
46 import java.io.File;
47 import java.io.FileNotFoundException;
48 import java.io.IOException;
49 import java.util.Arrays;
50 import java.util.List;
51 import java.util.Locale;
52 import java.util.Map;
53 import java.util.Optional;
54 import java.util.concurrent.atomic.AtomicBoolean;
55 import java.util.concurrent.atomic.AtomicInteger;
56 
57 /**
58  * To ensure that the ids of MediaStore database uris are stable and reliable.
59  */
60 public class DatabaseBackupAndRecovery {
61 
62     private static final String RECOVERY_DIRECTORY_PATH =
63             "/storage/emulated/" + UserHandle.myUserId() + "/.transforms/recovery";
64 
65     /**
66      * Path for storing owner id to owner package identifier relation and vice versa.
67      * Lower file system path is used as upper file system does not support xattrs.
68      */
69     private static final String OWNER_RELATION_BACKUP_PATH =
70             "/data/media/" + UserHandle.myUserId() + "/.transforms/recovery/leveldb-ownership";
71 
72     /**
73      * Path which stores backup of external primary volume.
74      * Lower file system path is used as upper file system does not support xattrs.
75      */
76     private static final String EXTERNAL_PRIMARY_VOLUME_BACKUP_PATH =
77             "/data/media/" + UserHandle.myUserId()
78                     + "/.transforms/recovery/leveldb-external_primary";
79 
80     /**
81      * Frequency at which next value of owner id is backed up in the external storage.
82      */
83     private static final int NEXT_OWNER_ID_BACKUP_FREQUENCY = 50;
84 
85     /**
86      * Start value used for next owner id.
87      */
88     private static final int NEXT_OWNER_ID_DEFAULT_VALUE = 0;
89 
90     /**
91      * Key name of xattr used to set next owner id on ownership DB.
92      */
93     private static final String NEXT_OWNER_ID_XATTR_KEY = "user.nextownerid";
94 
95     /**
96      * Key name of xattr used to store last modified generation number.
97      */
98     private static final String LAST_BACKEDUP_GENERATION_XATTR_KEY = "user.lastbackedgeneration";
99 
100     /**
101      * External primary storage root path for given user.
102      */
103     private static final String EXTERNAL_PRIMARY_ROOT_PATH =
104             "/storage/emulated/" + UserHandle.myUserId();
105 
106     /**
107      * Array of columns backed up in external storage.
108      */
109     private static final String[] QUERY_COLUMNS = new String[]{
110             MediaStore.Files.FileColumns._ID,
111             MediaStore.Files.FileColumns.DATA,
112             MediaStore.Files.FileColumns.IS_FAVORITE,
113             MediaStore.Files.FileColumns.IS_PENDING,
114             MediaStore.Files.FileColumns.IS_TRASHED,
115             MediaStore.Files.FileColumns.MEDIA_TYPE,
116             MediaStore.Files.FileColumns._USER_ID,
117             MediaStore.Files.FileColumns.DATE_EXPIRES,
118             MediaStore.Files.FileColumns.OWNER_PACKAGE_NAME,
119             MediaStore.Files.FileColumns.GENERATION_MODIFIED
120     };
121 
122     /**
123      * Wait time of 5 seconds in millis.
124      */
125     private static final long WAIT_TIME_5_SECONDS_IN_MILLIS = 5000;
126 
127     /**
128      * Wait time of 10 seconds in millis.
129      */
130     private static final long WAIT_TIME_10_SECONDS_IN_MILLIS = 10000;
131 
132     /**
133      * Number of records to read from leveldb in a JNI call.
134      */
135     protected static final int LEVEL_DB_READ_LIMIT = 1000;
136 
137     /**
138      * Stores cached value of next owner id. This helps in improving performance by backing up next
139      * row id less frequently in the external storage.
140      */
141     private AtomicInteger mNextOwnerId;
142 
143     /**
144      * Stores value of next backup of owner id.
145      */
146     private AtomicInteger mNextOwnerIdBackup;
147     private final ConfigStore mConfigStore;
148     private final VolumeCache mVolumeCache;
149 
150     private AtomicBoolean mIsBackupSetupComplete = new AtomicBoolean(false);
151 
152     private Map<String, String> mOwnerIdRelationMap;
153 
DatabaseBackupAndRecovery(ConfigStore configStore, VolumeCache volumeCache)154     protected DatabaseBackupAndRecovery(ConfigStore configStore, VolumeCache volumeCache) {
155         mConfigStore = configStore;
156         mVolumeCache = volumeCache;
157     }
158 
159     /**
160      * Returns true if migration and recovery code flow for stable uris is enabled for given volume.
161      */
isStableUrisEnabled(String volumeName)162     protected boolean isStableUrisEnabled(String volumeName) {
163         switch (volumeName) {
164             case MediaStore.VOLUME_INTERNAL:
165                 return mConfigStore.isStableUrisForInternalVolumeEnabled()
166                         || SystemProperties.getBoolean("persist.sys.fuse.backup.internal_db_backup",
167                         /* defaultValue */ false);
168             case MediaStore.VOLUME_EXTERNAL_PRIMARY:
169                 return mConfigStore.isStableUrisForExternalVolumeEnabled()
170                         || SystemProperties.getBoolean(
171                         "persist.sys.fuse.backup.external_volume_backup",
172                         /* defaultValue */ false);
173             default:
174                 return false;
175         }
176     }
177 
onConfigPropertyChangeListener()178     protected void onConfigPropertyChangeListener() {
179         if ((mConfigStore.isStableUrisForInternalVolumeEnabled()
180                 || mConfigStore.isStableUrisForExternalVolumeEnabled())
181                 && mVolumeCache.getExternalVolumeNames().contains(
182                 MediaStore.VOLUME_EXTERNAL_PRIMARY)) {
183             Log.i(TAG,
184                     "On device config change, found stable uri support enabled. Attempting backup"
185                             + " and recovery setup.");
186             setupVolumeDbBackupAndRecovery(MediaStore.VOLUME_EXTERNAL_PRIMARY,
187                     new File(EXTERNAL_PRIMARY_ROOT_PATH));
188         }
189     }
190 
191     /**
192      * On device boot, leveldb setup is done as part of attachVolume call for primary external.
193      * Also, on device config flag change, we check if flag is enabled, if yes, we proceed to
194      * setup(no-op if connection already exists). So, we setup backup and recovery for internal
195      * volume on Media mount signal of EXTERNAL_PRIMARY.
196      */
setupVolumeDbBackupAndRecovery(String volumeName, File volumePath)197     protected synchronized void setupVolumeDbBackupAndRecovery(String volumeName, File volumePath) {
198         // We are setting up leveldb instance only for internal volume as of now. Since internal
199         // volume does not have any fuse daemon thread, leveldb instance is created by fuse
200         // daemon thread of EXTERNAL_PRIMARY.
201         if (!MediaStore.VOLUME_EXTERNAL_PRIMARY.equalsIgnoreCase(volumeName)) {
202             // Set backup only for external primary for now.
203             return;
204         }
205         // Do not create leveldb instance if stable uris is not enabled for internal volume.
206         if (!isStableUrisEnabled(MediaStore.VOLUME_INTERNAL)) {
207             // Return if we are not supporting backup for internal volume
208             return;
209         }
210 
211         if (mIsBackupSetupComplete.get()) {
212             // Return if setup is already done
213             return;
214         }
215 
216         try {
217             if (!new File(RECOVERY_DIRECTORY_PATH).exists()) {
218                 new File(RECOVERY_DIRECTORY_PATH).mkdirs();
219             }
220             FuseDaemon fuseDaemon = getFuseDaemonForFileWithWait(volumePath,
221                     WAIT_TIME_5_SECONDS_IN_MILLIS);
222             fuseDaemon.setupVolumeDbBackup();
223             mIsBackupSetupComplete = new AtomicBoolean(true);
224         } catch (IOException e) {
225             Log.e(TAG, "Failure in setting up backup and recovery for volume: " + volumeName, e);
226         }
227     }
228 
229     /**
230      * Backs up databases to external storage to ensure stable URIs.
231      */
backupDatabases(DatabaseHelper internalDatabaseHelper, DatabaseHelper externalDatabaseHelper, CancellationSignal signal)232     public void backupDatabases(DatabaseHelper internalDatabaseHelper,
233             DatabaseHelper externalDatabaseHelper, CancellationSignal signal) {
234         Log.i(TAG, "Triggering database backup");
235         backupInternalDatabase(internalDatabaseHelper, signal);
236         backupExternalDatabase(externalDatabaseHelper, signal);
237     }
238 
readDataFromBackup(String volumeName, String filePath)239     protected Optional<BackupIdRow> readDataFromBackup(String volumeName, String filePath) {
240         if (!isStableUrisEnabled(volumeName)) {
241             return Optional.empty();
242         }
243 
244         final String fuseDaemonFilePath = getFuseDaemonFilePath(filePath);
245         try {
246             final String data = getFuseDaemonForPath(fuseDaemonFilePath).readBackedUpData(filePath);
247             return Optional.of(BackupIdRow.deserialize(data));
248         } catch (Exception e) {
249             Log.e(TAG, "Failure in getting backed up data for filePath: " + filePath, e);
250             return Optional.empty();
251         }
252     }
253 
backupInternalDatabase(DatabaseHelper internalDbHelper, CancellationSignal signal)254     protected void backupInternalDatabase(DatabaseHelper internalDbHelper,
255             CancellationSignal signal) {
256         if (!isStableUrisEnabled(MediaStore.VOLUME_INTERNAL)
257                 || internalDbHelper.isDatabaseRecovering()) {
258             return;
259         }
260 
261         if (!mIsBackupSetupComplete.get()) {
262             setupVolumeDbBackupAndRecovery(MediaStore.VOLUME_EXTERNAL,
263                     new File(EXTERNAL_PRIMARY_ROOT_PATH));
264         }
265 
266         FuseDaemon fuseDaemon;
267         try {
268             fuseDaemon = getFuseDaemonForPath(EXTERNAL_PRIMARY_ROOT_PATH);
269         } catch (FileNotFoundException e) {
270             Log.e(TAG,
271                     "Fuse Daemon not found for primary external storage, skipping backing up of "
272                             + "internal database.",
273                     e);
274             return;
275         }
276 
277         internalDbHelper.runWithTransaction((db) -> {
278             try (Cursor c = db.query(true, "files", QUERY_COLUMNS, null, null, null, null, null,
279                     null, signal)) {
280                 while (c.moveToNext()) {
281                     backupDataValues(fuseDaemon, c);
282                 }
283                 Log.d(TAG, String.format(Locale.ROOT,
284                         "Backed up %d rows of internal database to external storage on idle "
285                                 + "maintenance.",
286                         c.getCount()));
287             } catch (Exception e) {
288                 Log.e(TAG, "Failure in backing up internal database to external storage.", e);
289             }
290             return null;
291         });
292     }
293 
backupExternalDatabase(DatabaseHelper externalDbHelper, CancellationSignal signal)294     protected void backupExternalDatabase(DatabaseHelper externalDbHelper,
295             CancellationSignal signal) {
296         if (!isStableUrisEnabled(MediaStore.VOLUME_EXTERNAL_PRIMARY)
297                 || externalDbHelper.isDatabaseRecovering()) {
298             return;
299         }
300 
301         if (!mIsBackupSetupComplete.get()) {
302             setupVolumeDbBackupAndRecovery(MediaStore.VOLUME_EXTERNAL,
303                     new File(EXTERNAL_PRIMARY_ROOT_PATH));
304         }
305 
306         FuseDaemon fuseDaemon;
307         try {
308             fuseDaemon = getFuseDaemonForFileWithWait(new File(EXTERNAL_PRIMARY_ROOT_PATH),
309                     WAIT_TIME_5_SECONDS_IN_MILLIS);
310         } catch (FileNotFoundException e) {
311             Log.e(TAG,
312                     "Fuse Daemon not found for primary external storage, skipping backing up of "
313                             + "external database.",
314                     e);
315             return;
316         }
317 
318         // Read last backed up generation number
319         Optional<Long> lastBackedUpGenNum = getXattrOfLongValue(
320                 EXTERNAL_PRIMARY_VOLUME_BACKUP_PATH, LAST_BACKEDUP_GENERATION_XATTR_KEY);
321         long lastBackedGenerationNumber = lastBackedUpGenNum.isPresent()
322                 ? lastBackedUpGenNum.get() : 0;
323         if (lastBackedGenerationNumber > 0) {
324             Log.i(TAG, "Last backed up generation number is " + lastBackedGenerationNumber);
325         }
326         final String generationClause = MediaStore.Files.FileColumns.GENERATION_MODIFIED + " > "
327                 + lastBackedGenerationNumber;
328         final String volumeClause = MediaStore.Files.FileColumns.VOLUME_NAME + " = '"
329                 + MediaStore.VOLUME_EXTERNAL_PRIMARY + "'";
330         final String selectionClause = generationClause + " AND " + volumeClause;
331 
332         externalDbHelper.runWithTransaction((db) -> {
333             long maxGeneration = lastBackedGenerationNumber;
334             try (Cursor c = db.query(true, "files", QUERY_COLUMNS, selectionClause, null, null,
335                     null, null, null, signal)) {
336                 while (c.moveToNext()) {
337                     if (signal != null && signal.isCanceled()) {
338                         break;
339                     }
340                     backupDataValues(fuseDaemon, c);
341                     maxGeneration = Math.max(maxGeneration, c.getLong(9));
342                 }
343                 setXattr(EXTERNAL_PRIMARY_VOLUME_BACKUP_PATH, LAST_BACKEDUP_GENERATION_XATTR_KEY,
344                         String.valueOf(maxGeneration));
345                 Log.d(TAG, String.format(Locale.ROOT,
346                         "Backed up %d rows of external database to external storage on idle "
347                                 + "maintenance.",
348                         c.getCount()));
349             } catch (Exception e) {
350                 Log.e(TAG, "Failure in backing up external database to external storage.", e);
351                 return null;
352             }
353             return null;
354         });
355     }
356 
backupDataValues(FuseDaemon fuseDaemon, Cursor c)357     private void backupDataValues(FuseDaemon fuseDaemon, Cursor c) throws IOException {
358         final long id = c.getLong(0);
359         final String data = c.getString(1);
360         final boolean isFavorite = c.getInt(2) != 0;
361         final boolean isPending = c.getInt(3) != 0;
362         final boolean isTrashed = c.getInt(4) != 0;
363         final int mediaType = c.getInt(5);
364         final int userId = c.getInt(6);
365         final String dateExpires = c.getString(7);
366         final String ownerPackageName = c.getString(8);
367         BackupIdRow backupIdRow = createBackupIdRow(fuseDaemon, id, mediaType,
368                 isFavorite, isPending, isTrashed, userId, dateExpires,
369                 ownerPackageName);
370         fuseDaemon.backupVolumeDbData(data, BackupIdRow.serialize(backupIdRow));
371     }
372 
deleteBackupForVolume(String volumeName)373     protected void deleteBackupForVolume(String volumeName) {
374         File dbFilePath = new File(
375                 String.format(Locale.ROOT, "%s/%s.db", RECOVERY_DIRECTORY_PATH, volumeName));
376         if (dbFilePath.exists()) {
377             dbFilePath.delete();
378         }
379     }
380 
readBackedUpFilePaths(String volumeName, String lastReadValue, int limit)381     protected String[] readBackedUpFilePaths(String volumeName, String lastReadValue, int limit) {
382         if (!isStableUrisEnabled(volumeName)) {
383             return new String[0];
384         }
385 
386         try {
387             return getFuseDaemonForPath(EXTERNAL_PRIMARY_ROOT_PATH).readBackedUpFilePaths(
388                     volumeName, lastReadValue, limit);
389         } catch (IOException e) {
390             Log.e(TAG, "Failure in reading backed up file paths for volume: " + volumeName, e);
391             return new String[0];
392         }
393     }
394 
updateNextRowIdXattr(DatabaseHelper helper, long id)395     protected void updateNextRowIdXattr(DatabaseHelper helper, long id) {
396         if (helper.isInternal()) {
397             updateNextRowIdForInternal(helper, id);
398             return;
399         }
400 
401         if (!helper.isNextRowIdBackupEnabled()) {
402             return;
403         }
404 
405         Optional<Long> nextRowIdBackupOptional = helper.getNextRowId();
406         if (!nextRowIdBackupOptional.isPresent()) {
407             throw new RuntimeException(
408                     String.format(Locale.ROOT, "Cannot find next row id xattr for %s.",
409                             helper.getDatabaseName()));
410         }
411 
412         if (id >= nextRowIdBackupOptional.get()) {
413             helper.backupNextRowId(id);
414         }
415     }
416 
417     @NonNull
getFuseDaemonForPath(@onNull String path)418     private FuseDaemon getFuseDaemonForPath(@NonNull String path)
419             throws FileNotFoundException {
420         return MediaProvider.getFuseDaemonForFile(new File(path), mVolumeCache);
421     }
422 
updateNextRowIdAndSetDirty(@onNull DatabaseHelper helper, @NonNull FileRow oldRow, @NonNull FileRow newRow)423     protected void updateNextRowIdAndSetDirty(@NonNull DatabaseHelper helper,
424             @NonNull FileRow oldRow, @NonNull FileRow newRow) {
425         updateNextRowIdXattr(helper, newRow.getId());
426         markBackupAsDirty(helper, oldRow);
427     }
428 
429     /**
430      * Backs up DB data in external storage to recover in case of DB rollback.
431      */
backupVolumeDbData(DatabaseHelper databaseHelper, FileRow insertedRow)432     protected void backupVolumeDbData(DatabaseHelper databaseHelper, FileRow insertedRow) {
433         if (!isBackupUpdateAllowed(databaseHelper, insertedRow.getVolumeName())) {
434             return;
435         }
436 
437         // For all internal file paths, redirect to external primary fuse daemon.
438         final String fuseDaemonFilePath = getFuseDaemonFilePath(insertedRow.getPath());
439         try {
440             FuseDaemon fuseDaemon = getFuseDaemonForPath(fuseDaemonFilePath);
441             final BackupIdRow value = createBackupIdRow(fuseDaemon, insertedRow);
442             fuseDaemon.backupVolumeDbData(insertedRow.getPath(), BackupIdRow.serialize(value));
443         } catch (Exception e) {
444             Log.e(TAG, "Failure in backing up data to external storage", e);
445         }
446     }
447 
getFuseDaemonFilePath(String filePath)448     private String getFuseDaemonFilePath(String filePath) {
449         return filePath.startsWith("/storage") ? filePath : EXTERNAL_PRIMARY_ROOT_PATH;
450     }
451 
createBackupIdRow(FuseDaemon fuseDaemon, FileRow insertedRow)452     private BackupIdRow createBackupIdRow(FuseDaemon fuseDaemon, FileRow insertedRow)
453             throws IOException {
454         return createBackupIdRow(fuseDaemon, insertedRow.getId(), insertedRow.getMediaType(),
455                 insertedRow.isFavorite(), insertedRow.isPending(), insertedRow.isTrashed(),
456                 insertedRow.getUserId(), insertedRow.getDateExpires(),
457                 insertedRow.getOwnerPackageName());
458     }
459 
createBackupIdRow(FuseDaemon fuseDaemon, long id, int mediaType, boolean isFavorite, boolean isPending, boolean isTrashed, int userId, String dateExpires, String ownerPackageName)460     private BackupIdRow createBackupIdRow(FuseDaemon fuseDaemon, long id, int mediaType,
461             boolean isFavorite,
462             boolean isPending, boolean isTrashed, int userId, String dateExpires,
463             String ownerPackageName) throws IOException {
464         BackupIdRow.Builder builder = BackupIdRow.newBuilder(id);
465         builder.setMediaType(mediaType);
466         builder.setIsFavorite(isFavorite ? 1 : 0);
467         builder.setIsPending(isPending ? 1 : 0);
468         builder.setIsTrashed(isTrashed ? 1 : 0);
469         builder.setUserId(userId);
470         builder.setDateExpires(dateExpires);
471         // We set owner package id instead of owner package name in the backup. When an
472         // application is uninstalled, all media rows corresponding to it will be orphaned and
473         // would have owner package name as null. This should not change if application is
474         // installed again. Therefore, we are storing owner id instead of owner package name. On
475         // package uninstallation, we delete the owner id relation from the backup. All rows
476         // recovered for orphaned owner ids will have package name as null. Since we also need to
477         // support cloned apps, we are storing a combination of owner package name and user id to
478         // uniquely identify a package.
479         builder.setOwnerPackagedId(getOwnerPackageId(fuseDaemon, ownerPackageName, userId));
480         return builder.setIsDirty(false).build();
481     }
482 
483 
getOwnerPackageId(FuseDaemon fuseDaemon, String ownerPackageName, int userId)484     private int getOwnerPackageId(FuseDaemon fuseDaemon, String ownerPackageName, int userId)
485             throws IOException {
486         if (Strings.isNullOrEmpty(ownerPackageName) || ownerPackageName.equalsIgnoreCase("null")) {
487             // We store -1 in the backup if owner package name is null.
488             return -1;
489         }
490 
491         // Create identifier of format "owner_pkg_name::user_id". Tightly coupling owner package
492         // name and user id helps in handling app cloning scenarios.
493         String ownerPackageIdentifier = createOwnerPackageIdentifier(ownerPackageName, userId);
494         // Read any existing entry for given owner package name and user id
495         String ownerId = fuseDaemon.readFromOwnershipBackup(ownerPackageIdentifier);
496         if (!ownerId.trim().isEmpty()) {
497             // Use existing owner id if found and is positive
498             int val = Integer.parseInt(ownerId);
499             if (val >= 0) {
500                 return val;
501             }
502         }
503 
504         int nextOwnerId = getAndIncrementNextOwnerId();
505         fuseDaemon.createOwnerIdRelation(String.valueOf(nextOwnerId), ownerPackageIdentifier);
506         Log.i(TAG, "Created relation b/w " + nextOwnerId + " and " + ownerPackageIdentifier);
507         return nextOwnerId;
508     }
509 
createOwnerPackageIdentifier(String ownerPackageName, int userId)510     private String createOwnerPackageIdentifier(String ownerPackageName, int userId) {
511         return ownerPackageName.trim().concat("::").concat(String.valueOf(userId));
512     }
513 
getPackageNameAndUserId(String ownerPackageIdentifier)514     private Pair<String, Integer> getPackageNameAndUserId(String ownerPackageIdentifier) {
515         if (ownerPackageIdentifier.trim().isEmpty()) {
516             return Pair.create(null, null);
517         }
518 
519         String[] arr = ownerPackageIdentifier.trim().split("::");
520         return Pair.create(arr[0], Integer.valueOf(arr[1]));
521     }
522 
getAndIncrementNextOwnerId()523     private synchronized int getAndIncrementNextOwnerId() {
524         // In synchronized block to avoid use of same owner id for multiple owner package relations
525         if (mNextOwnerId == null) {
526             Optional<Integer> nextOwnerIdOptional = getXattrOfIntegerValue(
527                     OWNER_RELATION_BACKUP_PATH,
528                     NEXT_OWNER_ID_XATTR_KEY);
529             mNextOwnerId = nextOwnerIdOptional.map(AtomicInteger::new).orElseGet(
530                     () -> new AtomicInteger(NEXT_OWNER_ID_DEFAULT_VALUE));
531             mNextOwnerIdBackup = new AtomicInteger(mNextOwnerId.get());
532         }
533         if (mNextOwnerId.get() >= mNextOwnerIdBackup.get()) {
534             int nextBackup = mNextOwnerId.get() + NEXT_OWNER_ID_BACKUP_FREQUENCY;
535             updateNextOwnerId(nextBackup);
536             mNextOwnerIdBackup = new AtomicInteger(nextBackup);
537         }
538         int returnValue = mNextOwnerId.get();
539         mNextOwnerId.set(returnValue + 1);
540         return returnValue;
541     }
542 
updateNextOwnerId(int val)543     private void updateNextOwnerId(int val) {
544         setXattr(OWNER_RELATION_BACKUP_PATH, NEXT_OWNER_ID_XATTR_KEY, String.valueOf(val));
545         Log.d(TAG, "Updated next owner id to: " + val);
546     }
547 
removeOwnerIdToPackageRelation(String packageName, int userId)548     protected void removeOwnerIdToPackageRelation(String packageName, int userId) {
549         if (Strings.isNullOrEmpty(packageName) || packageName.equalsIgnoreCase("null")
550                 || !isStableUrisEnabled(MediaStore.VOLUME_EXTERNAL_PRIMARY)
551                 || !new File(OWNER_RELATION_BACKUP_PATH).exists()) {
552             return;
553         }
554 
555         try {
556             FuseDaemon fuseDaemon = getFuseDaemonForPath(EXTERNAL_PRIMARY_ROOT_PATH);
557             String ownerPackageIdentifier = createOwnerPackageIdentifier(packageName, userId);
558             String ownerId = fuseDaemon.readFromOwnershipBackup(ownerPackageIdentifier);
559 
560             fuseDaemon.removeOwnerIdRelation(ownerId, ownerPackageIdentifier);
561         } catch (Exception e) {
562             Log.e(TAG, "Failure in removing owner id to package relation", e);
563         }
564     }
565 
566     /**
567      * Deletes backed up data(needed for recovery) from external storage.
568      */
deleteFromDbBackup(DatabaseHelper databaseHelper, FileRow deletedRow)569     protected void deleteFromDbBackup(DatabaseHelper databaseHelper, FileRow deletedRow) {
570         if (!isBackupUpdateAllowed(databaseHelper, deletedRow.getVolumeName())) {
571             return;
572         }
573 
574         String deletedFilePath = deletedRow.getPath();
575         if (deletedFilePath == null) {
576             return;
577         }
578 
579         // For all internal file paths, redirect to external primary fuse daemon.
580         String fuseDaemonFilePath = getFuseDaemonFilePath(deletedFilePath);
581         try {
582             getFuseDaemonForPath(fuseDaemonFilePath).deleteDbBackup(deletedFilePath);
583         } catch (IOException e) {
584             Log.w(TAG, "Failure in deleting backup data for key: " + deletedFilePath, e);
585         }
586     }
587 
isBackupUpdateAllowed(DatabaseHelper databaseHelper, String volumeName)588     protected boolean isBackupUpdateAllowed(DatabaseHelper databaseHelper, String volumeName) {
589         // Backup only if stable uris is enabled, db is not recovering and backup setup is complete.
590         return isStableUrisEnabled(volumeName) && !databaseHelper.isDatabaseRecovering()
591                 && mIsBackupSetupComplete.get();
592     }
593 
594 
updateNextRowIdForInternal(DatabaseHelper helper, long id)595     private void updateNextRowIdForInternal(DatabaseHelper helper, long id) {
596         if (!isStableUrisEnabled(MediaStore.VOLUME_INTERNAL)) {
597             return;
598         }
599 
600         Optional<Long> nextRowIdBackupOptional = helper.getNextRowId();
601 
602         if (!nextRowIdBackupOptional.isPresent()) {
603             return;
604         }
605 
606         if (id >= nextRowIdBackupOptional.get()) {
607             helper.backupNextRowId(id);
608         }
609     }
610 
markBackupAsDirty(DatabaseHelper databaseHelper, FileRow updatedRow)611     private void markBackupAsDirty(DatabaseHelper databaseHelper, FileRow updatedRow) {
612         if (!isBackupUpdateAllowed(databaseHelper, updatedRow.getVolumeName())) {
613             return;
614         }
615 
616         final String updatedFilePath = updatedRow.getPath();
617         // For all internal file paths, redirect to external primary fuse daemon.
618         final String fuseDaemonFilePath = getFuseDaemonFilePath(updatedFilePath);
619         try {
620             getFuseDaemonForPath(fuseDaemonFilePath).backupVolumeDbData(updatedFilePath,
621                     BackupIdRow.serialize(BackupIdRow.newBuilder(updatedRow.getId()).setIsDirty(
622                             true).build()));
623         } catch (IOException e) {
624             Log.e(TAG, "Failure in marking data as dirty to external storage for path:"
625                     + updatedFilePath, e);
626         }
627     }
628 
629     /**
630      * Reads value corresponding to given key from xattr on given path.
631      */
getXattr(String path, String key)632     public static Optional<String> getXattr(String path, String key) {
633         try {
634             return Optional.of(Arrays.toString(Os.getxattr(path, key)));
635         } catch (Exception e) {
636             Log.w(TAG, String.format(Locale.ROOT,
637                     "Exception encountered while reading xattr:%s from path:%s.", key, path));
638             return Optional.empty();
639         }
640     }
641 
642     /**
643      * Reads long value corresponding to given key from xattr on given path.
644      */
getXattrOfLongValue(String path, String key)645     public static Optional<Long> getXattrOfLongValue(String path, String key) {
646         try {
647             return Optional.of(Long.parseLong(new String(Os.getxattr(path, key))));
648         } catch (Exception e) {
649             Log.w(TAG, String.format(Locale.ROOT,
650                     "Exception encountered while reading xattr:%s from path:%s.", key, path));
651             return Optional.empty();
652         }
653     }
654 
655     /**
656      * Reads integer value corresponding to given key from xattr on given path.
657      */
getXattrOfIntegerValue(String path, String key)658     public static Optional<Integer> getXattrOfIntegerValue(String path, String key) {
659         try {
660             return Optional.of(Integer.parseInt(new String(Os.getxattr(path, key))));
661         } catch (Exception e) {
662             Log.w(TAG, String.format(Locale.ROOT,
663                     "Exception encountered while reading xattr:%s from path:%s.", key, path));
664             return Optional.empty();
665         }
666     }
667 
668     /**
669      * Sets key and value as xattr on given path.
670      */
setXattr(String path, String key, String value)671     public static boolean setXattr(String path, String key, String value) {
672         try (ParcelFileDescriptor pfd = ParcelFileDescriptor.open(new File(path),
673                 ParcelFileDescriptor.MODE_READ_ONLY)) {
674             // Map id value to xattr key
675             Os.setxattr(path, key, value.getBytes(), 0);
676             Os.fsync(pfd.getFileDescriptor());
677             Log.d(TAG, String.format("xattr set to %s for key:%s on path: %s.", value, key, path));
678             return true;
679         } catch (Exception e) {
680             Log.e(TAG, String.format(Locale.ROOT, "Failed to set xattr:%s to %s for path: %s.", key,
681                     value, path), e);
682             return false;
683         }
684     }
685 
insertDataInDatabase(SQLiteDatabase db, BackupIdRow row, String filePath, String volumeName)686     protected void insertDataInDatabase(SQLiteDatabase db, BackupIdRow row, String filePath,
687             String volumeName) {
688         final ContentValues values = createValuesFromFileRow(row, filePath, volumeName);
689         if (db.insert("files", null, values) == -1) {
690             Log.e(TAG, "Failed to insert " + values + "; continuing");
691         }
692     }
693 
createValuesFromFileRow(BackupIdRow row, String filePath, String volumeName)694     private ContentValues createValuesFromFileRow(BackupIdRow row, String filePath,
695             String volumeName) {
696         ContentValues values = new ContentValues();
697         values.put(MediaStore.Files.FileColumns._ID, row.getId());
698         values.put(MediaStore.Files.FileColumns.IS_FAVORITE, row.getIsFavorite());
699         values.put(MediaStore.Files.FileColumns.IS_PENDING, row.getIsPending());
700         values.put(MediaStore.Files.FileColumns.IS_TRASHED, row.getIsTrashed());
701         values.put(MediaStore.Files.FileColumns.DATA, filePath);
702         values.put(MediaStore.Files.FileColumns.VOLUME_NAME, volumeName);
703         values.put(MediaStore.Files.FileColumns._USER_ID, row.getUserId());
704         values.put(MediaStore.Files.FileColumns.MEDIA_TYPE, row.getMediaType());
705         if (!StringUtils.isNullOrEmpty(row.getDateExpires())) {
706             values.put(MediaStore.Files.FileColumns.DATE_EXPIRES,
707                     Long.valueOf(row.getDateExpires()));
708         }
709         if (row.getOwnerPackageId() >= 0) {
710             Pair<String, Integer> ownerPackageNameAndUidPair = getOwnerPackageNameAndUidPair(
711                     row.getOwnerPackageId());
712             if (ownerPackageNameAndUidPair.first != null) {
713                 values.put(MediaStore.Files.FileColumns.OWNER_PACKAGE_NAME,
714                         ownerPackageNameAndUidPair.first);
715             }
716             if (ownerPackageNameAndUidPair.second != null) {
717                 values.put(MediaStore.Files.FileColumns._USER_ID,
718                         ownerPackageNameAndUidPair.second);
719             }
720         }
721 
722         return values;
723     }
724 
getOwnerPackageNameAndUidPair(int ownerPackageId)725     private Pair<String, Integer> getOwnerPackageNameAndUidPair(int ownerPackageId) {
726         if (mOwnerIdRelationMap == null) {
727             try {
728                 mOwnerIdRelationMap = getFuseDaemonForPath(
729                         EXTERNAL_PRIMARY_ROOT_PATH).readOwnerIdRelations();
730                 Log.i(TAG, "Cached owner id map");
731             } catch (IOException e) {
732                 Log.e(TAG, "Failure in reading owner details for owner id:" + ownerPackageId, e);
733                 return Pair.create(null, null);
734             }
735         }
736 
737         if (mOwnerIdRelationMap.containsKey(String.valueOf(ownerPackageId))) {
738             return getPackageNameAndUserId(mOwnerIdRelationMap.get(String.valueOf(ownerPackageId)));
739         }
740         return Pair.create(null, null);
741     }
742 
recoverData(SQLiteDatabase db, String volumeName)743     protected void recoverData(SQLiteDatabase db, String volumeName) {
744         if (!isBackupPresent()) {
745             return;
746         }
747 
748         final long startTime = SystemClock.elapsedRealtime();
749         final String fuseFilePath = getFuseFilePathFromVolumeName(volumeName);
750         // Wait for external primary to be attached as we use same thread for internal volume.
751         // Maximum wait for 10s
752         try {
753             getFuseDaemonForFileWithWait(new File(fuseFilePath), WAIT_TIME_10_SECONDS_IN_MILLIS);
754         } catch (FileNotFoundException e) {
755             Log.e(TAG, "Could not recover data as fuse daemon could not serve requests.", e);
756             return;
757         }
758 
759         setupVolumeDbBackupAndRecovery(volumeName, new File(EXTERNAL_PRIMARY_ROOT_PATH));
760         long rowsRecovered = 0;
761         long dirtyRowsCount = 0;
762         String[] backedUpFilePaths;
763         String lastReadValue = "";
764 
765         while (true) {
766             backedUpFilePaths = readBackedUpFilePaths(volumeName, lastReadValue,
767                     LEVEL_DB_READ_LIMIT);
768             if (backedUpFilePaths.length <= 0) {
769                 break;
770             }
771 
772             for (String filePath : backedUpFilePaths) {
773                 Optional<BackupIdRow> fileRow = readDataFromBackup(volumeName, filePath);
774                 if (fileRow.isPresent()) {
775                     if (fileRow.get().getIsDirty()) {
776                         dirtyRowsCount++;
777                         continue;
778                     }
779 
780                     insertDataInDatabase(db, fileRow.get(), filePath, volumeName);
781                     rowsRecovered++;
782                 }
783             }
784 
785             // Read less rows than expected
786             if (backedUpFilePaths.length < LEVEL_DB_READ_LIMIT) {
787                 break;
788             }
789             lastReadValue = backedUpFilePaths[backedUpFilePaths.length - 1];
790         }
791         long recoveryTime = SystemClock.elapsedRealtime() - startTime;
792         MediaProviderStatsLog.write(MediaProviderStatsLog.MEDIA_PROVIDER_VOLUME_RECOVERY_REPORTED,
793                 getVolumeNameForStatsLog(volumeName), recoveryTime, rowsRecovered, dirtyRowsCount);
794         Log.i(TAG, String.format(Locale.ROOT, "%d rows recovered for volume:%s.", rowsRecovered,
795                 volumeName));
796         if (MediaStore.VOLUME_EXTERNAL_PRIMARY.equalsIgnoreCase(volumeName)) {
797             // Resetting generation number
798             setXattr(EXTERNAL_PRIMARY_VOLUME_BACKUP_PATH, LAST_BACKEDUP_GENERATION_XATTR_KEY,
799                     String.valueOf(0));
800         }
801         Log.i(TAG, String.format(Locale.ROOT, "Recovery time: %d ms", recoveryTime));
802     }
803 
isBackupPresent()804     protected boolean isBackupPresent() {
805         return new File(RECOVERY_DIRECTORY_PATH).exists();
806     }
807 
getFuseDaemonForFileWithWait(File fuseFilePath, long waitTime)808     protected FuseDaemon getFuseDaemonForFileWithWait(File fuseFilePath, long waitTime)
809             throws FileNotFoundException {
810         return MediaProvider.getFuseDaemonForFileWithWait(fuseFilePath, mVolumeCache, waitTime);
811     }
812 
getVolumeNameForStatsLog(String volumeName)813     private int getVolumeNameForStatsLog(String volumeName) {
814         if (volumeName.equalsIgnoreCase(MediaStore.VOLUME_INTERNAL)) {
815             return MEDIA_PROVIDER_VOLUME_RECOVERY_REPORTED__VOLUME__INTERNAL;
816         } else if (volumeName.equalsIgnoreCase(MediaStore.VOLUME_EXTERNAL_PRIMARY)) {
817             return MEDIA_PROVIDER_VOLUME_RECOVERY_REPORTED__VOLUME__EXTERNAL_PRIMARY;
818         }
819 
820         return MEDIA_PROVIDER_VOLUME_RECOVERY_REPORTED__VOLUME__PUBLIC;
821     }
822 
getFuseFilePathFromVolumeName(String volumeName)823     private static String getFuseFilePathFromVolumeName(String volumeName) {
824         switch (volumeName) {
825             case MediaStore.VOLUME_INTERNAL:
826             case MediaStore.VOLUME_EXTERNAL_PRIMARY:
827                 return EXTERNAL_PRIMARY_ROOT_PATH;
828             default:
829                 return "/storage/" + volumeName;
830         }
831     }
832 
833     /**
834      * Returns list of backed up files from external storage.
835      */
getBackupFiles()836     protected List<File> getBackupFiles() {
837         return Arrays.asList(new File(RECOVERY_DIRECTORY_PATH).listFiles());
838     }
839 
840     /**
841      * Updates backup in external storage to the latest values. Deletes backup of old file path if
842      * file path has changed.
843      */
updateBackup(DatabaseHelper helper, FileRow oldRow, FileRow newRow)844     public void updateBackup(DatabaseHelper helper, FileRow oldRow, FileRow newRow) {
845         if (!isBackupUpdateAllowed(helper, newRow.getVolumeName())) {
846             return;
847         }
848 
849         FuseDaemon fuseDaemon;
850         try {
851             fuseDaemon = getFuseDaemonForPath(EXTERNAL_PRIMARY_ROOT_PATH);
852         } catch (FileNotFoundException e) {
853             Log.e(TAG,
854                     "Fuse Daemon not found for primary external storage, skipping update of "
855                             + "backup.",
856                     e);
857             return;
858         }
859 
860         helper.runWithTransaction((db) -> {
861             try (Cursor c = db.query(true, "files", QUERY_COLUMNS, "_id=?",
862                     new String[]{String.valueOf(newRow.getId())}, null, null, null,
863                     null, null)) {
864                 if (c.moveToFirst()) {
865                     backupDataValues(fuseDaemon, c);
866                     Log.v(TAG, "Updated backed up row in leveldb");
867                     String newPath = c.getString(1);
868                     if (oldRow.getPath() != null && !oldRow.getPath().equalsIgnoreCase(newPath)) {
869                         // If file path has changed, update leveldb backup to delete old path.
870                         deleteFromDbBackup(helper, oldRow);
871                         Log.v(TAG, "Deleted backup of old file path.");
872                     }
873                 }
874             } catch (Exception e) {
875                 Log.e(TAG, "Failure in updating row in external storage backup.", e);
876             }
877             return null;
878         });
879     }
880 }
881