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