package co.smartreceipts.android.sync.drive.managers; import android.content.Context; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; import android.support.annotation.NonNull; import android.text.TextUtils; import com.google.android.gms.drive.DriveId; import com.google.common.base.Preconditions; import com.hadisatrio.optional.Optional; import java.io.File; import java.io.IOException; import java.util.List; import co.smartreceipts.android.persistence.DatabaseHelper; import co.smartreceipts.android.persistence.database.tables.AbstractSqlTable; import co.smartreceipts.android.persistence.database.tables.ReceiptsTable; import co.smartreceipts.android.sync.drive.rx.DriveStreamsManager; import co.smartreceipts.android.sync.manual.ManualBackupTask; import co.smartreceipts.android.sync.model.RemoteBackupMetadata; import co.smartreceipts.android.sync.model.impl.Identifier; import co.smartreceipts.android.utils.log.Logger; import io.reactivex.Observable; import io.reactivex.Single; public class DriveRestoreDataManager { private final Context mContext; private final DriveStreamsManager mDriveStreamsManager; private final DriveDatabaseManager mDriveDatabaseManager; private final DatabaseHelper mDatabaseHelper; private final File mStorageDirectory; @SuppressWarnings("ConstantConditions") public DriveRestoreDataManager(@NonNull Context context, @NonNull DriveStreamsManager driveStreamsManager, @NonNull DatabaseHelper databaseHelper, @NonNull DriveDatabaseManager driveDatabaseManager) { this(context, driveStreamsManager, databaseHelper, driveDatabaseManager, context.getExternalFilesDir(null)); } public DriveRestoreDataManager(@NonNull Context context, @NonNull DriveStreamsManager driveStreamsManager, @NonNull DatabaseHelper databaseHelper, @NonNull DriveDatabaseManager driveDatabaseManager, @NonNull File storageDirectory) { mContext = Preconditions.checkNotNull(context.getApplicationContext()); mDriveStreamsManager = Preconditions.checkNotNull(driveStreamsManager); mDatabaseHelper = Preconditions.checkNotNull(databaseHelper); mDriveDatabaseManager = Preconditions.checkNotNull(driveDatabaseManager); mStorageDirectory = Preconditions.checkNotNull(storageDirectory); } @NonNull public Single<Boolean> restoreBackup(@NonNull final RemoteBackupMetadata remoteBackupMetadata, final boolean overwriteExistingData) { Logger.info(this, "Initiating the restoration of a backup file for Google Drive with ID: {}", remoteBackupMetadata.getId()); return downloadBackupMetadataImages(remoteBackupMetadata, overwriteExistingData, mStorageDirectory) .flatMap(files -> { Logger.debug(this, "Performing database merge"); final File tempDbFile = new File(mStorageDirectory, ManualBackupTask.DATABASE_EXPORT_NAME); return Single.just(mDatabaseHelper.merge(tempDbFile.getAbsolutePath(), mContext.getPackageName(), overwriteExistingData)); }) .doOnSuccess(aBoolean -> { Logger.debug(this, "Syncing database following merge operation"); mDriveDatabaseManager.syncDatabase(); }); } @NonNull public Single<List<File>> downloadAllBackupMetadataImages(@NonNull final RemoteBackupMetadata remoteBackupMetadata, @NonNull final File downloadLocation) { return downloadBackupMetadataImages(remoteBackupMetadata, true, downloadLocation); } @NonNull public Single<List<File>> downloadAllFilesInDriveFolder(@NonNull final RemoteBackupMetadata remoteBackupMetadata, @NonNull final File downloadLocation) { Preconditions.checkNotNull(remoteBackupMetadata); Preconditions.checkNotNull(downloadLocation); return mDriveStreamsManager.getDriveId(remoteBackupMetadata.getId()) .map(driveId -> { Logger.debug(DriveRestoreDataManager.this, "Converting drive id to smart receipts drive folder"); return driveId.asDriveFolder(); }) .flatMapObservable(mDriveStreamsManager::getFilesInFolder) .map(DriveId::asDriveFile) .flatMapSingle(driveFile -> mDriveStreamsManager.getMetadata(driveFile) .map(metadata -> driveFile.getDriveId().getResourceId() + "__" + metadata.getOriginalFilename()) .flatMap(filename -> mDriveStreamsManager.download(driveFile, new File(downloadLocation, filename)))) .toList(); } @NonNull private Single<List<File>> downloadBackupMetadataImages(@NonNull final RemoteBackupMetadata remoteBackupMetadata, final boolean overwriteExistingData, @NonNull final File downloadLocation) { Preconditions.checkNotNull(remoteBackupMetadata); Preconditions.checkNotNull(downloadLocation); return deletePreviousTemporaryDatabase(downloadLocation) .<Optional<DriveId>>flatMap(success -> { if (success) { Logger.debug(DriveRestoreDataManager.this, "Fetching drive id"); return mDriveStreamsManager.getDriveId(remoteBackupMetadata.getId()).map(Optional::of); } else { return Single.just(Optional.absent()); } }) .filter(Optional::isPresent) .map(Optional::get) .flatMapSingle(driveId -> { Logger.debug(DriveRestoreDataManager.this, "Converting drive id to smart receipts drive folder"); return Single.just(driveId.asDriveFolder()); }) .flatMapObservable(driveFolder -> { Logger.debug(DriveRestoreDataManager.this, "Fetching receipts database in drive for this folder"); return mDriveStreamsManager.getFilesInFolder(driveFolder, DatabaseHelper.DATABASE_NAME); }) .take(1) .flatMap(driveId -> { Logger.debug(DriveRestoreDataManager.this, "Converting database drive id to drive file"); return Observable.just(driveId.asDriveFile()); }) .flatMapSingle(driveFile -> { Logger.debug(DriveRestoreDataManager.this, "Downloading database file"); final File tempDbFile = new File(downloadLocation, ManualBackupTask.DATABASE_EXPORT_NAME); return mDriveStreamsManager.download(driveFile, tempDbFile); }) .flatMap(file -> { Logger.debug(DriveRestoreDataManager.this, "Retrieving partial receipts from our temporary drive database"); return getPartialReceipts(file); }) .flatMapSingle(partialReceipt -> { Logger.debug(DriveRestoreDataManager.this, "Creating trip folder for partial receipt: {}", partialReceipt.parentTripName); return createParentFolderIfNeeded(partialReceipt, downloadLocation); }) .filter(partialReceipt -> { if (overwriteExistingData) { return true; } else { final File receiptFile = new File(new File(downloadLocation, partialReceipt.parentTripName), partialReceipt.fileName); Logger.debug(DriveRestoreDataManager.this, "Filtering out receipt? " + !receiptFile.exists()); return !receiptFile.exists(); } }) .flatMapSingle(partialReceipt -> { Logger.debug(DriveRestoreDataManager.this, "Downloading file for partial receipt: {}", partialReceipt.driveId); return downloadFileForReceipt(partialReceipt, downloadLocation); }) .toList(); } private Single<Boolean> deletePreviousTemporaryDatabase(@NonNull final File inDirectory) { return Single.create(emitter -> { final File tempDbFile = new File(inDirectory, ManualBackupTask.DATABASE_EXPORT_NAME); if (tempDbFile.exists()) { if (tempDbFile.delete()) { emitter.onSuccess(true); } else { emitter.onError(new IOException("Failed to delete our temporary database file")); } } else { emitter.onSuccess(true); } }); } private Observable<PartialReceipt> getPartialReceipts(@NonNull final File temporaryDatabaseFile) { Preconditions.checkNotNull(temporaryDatabaseFile); return Observable.create(emitter -> { SQLiteDatabase importDb = null; Cursor cursor = null; try { importDb = SQLiteDatabase.openDatabase(temporaryDatabaseFile.getAbsolutePath(), null, SQLiteDatabase.OPEN_READONLY); final String[] selection = new String[] { AbstractSqlTable.COLUMN_DRIVE_SYNC_ID, ReceiptsTable.COLUMN_PARENT, ReceiptsTable.COLUMN_PATH }; cursor = importDb.query(ReceiptsTable.TABLE_NAME, selection, AbstractSqlTable.COLUMN_DRIVE_SYNC_ID + " IS NOT NULL AND " + ReceiptsTable.COLUMN_PATH + " IS NOT NULL AND " + AbstractSqlTable.COLUMN_DRIVE_MARKED_FOR_DELETION + " = ?", new String[] { Integer.toString(0) }, null, null, null); if (cursor != null && cursor.moveToFirst()) { final int driveIdIndex = cursor.getColumnIndex(AbstractSqlTable.COLUMN_DRIVE_SYNC_ID); final int parentIndex = cursor.getColumnIndex(ReceiptsTable.COLUMN_PARENT); final int pathIndex = cursor.getColumnIndex(ReceiptsTable.COLUMN_PATH); do { final String driveId = cursor.getString(driveIdIndex); final String parent = cursor.getString(parentIndex); final String path = cursor.getString(pathIndex); if (driveId != null && parent != null && !TextUtils.isEmpty(path) && !DatabaseHelper.NO_DATA.equals(path)) { emitter.onNext(new PartialReceipt(driveId, parent, path)); } } while (cursor.moveToNext()); } emitter.onComplete(); } finally { if (importDb != null) { importDb.close(); } if (cursor != null) { cursor.close(); } } }); } private Single<PartialReceipt> createParentFolderIfNeeded(@NonNull final PartialReceipt partialReceipt, @NonNull final File inDirectory) { return Single.create(emitter -> { final File parentTripFolder = new File(inDirectory, partialReceipt.parentTripName); if (!parentTripFolder.exists()) { if (parentTripFolder.mkdir()) { emitter.onSuccess(partialReceipt); } else { emitter.onError(new IOException("Failed to create the parent directory for this receipt")); } } else { emitter.onSuccess(partialReceipt); } }); } private Single<File> downloadFileForReceipt(@NonNull final PartialReceipt partialReceipt, @NonNull final File inDirectory) { return mDriveStreamsManager.getDriveId(partialReceipt.driveId) .flatMap(driveId -> Single.just(driveId.asDriveFile())) .flatMap(driveFile -> { final File receiptFile = new File(new File(inDirectory, partialReceipt.parentTripName), partialReceipt.fileName); return mDriveStreamsManager.download(driveFile, receiptFile); }); } /** * A subset of receipt metadata so we don't need to full new as many objects as normally required, * since this will have a lot of extra memory overhead */ private static final class PartialReceipt { private final Identifier driveId; private final String parentTripName; private final String fileName; public PartialReceipt(@NonNull String driveId, @NonNull String parentTripName, @NonNull String fileName) { this.driveId = new Identifier(driveId); this.parentTripName = Preconditions.checkNotNull(parentTripName); this.fileName = Preconditions.checkNotNull(fileName); } } }