package co.smartreceipts.android.sync.drive.rx; import android.content.Context; import android.os.Bundle; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.annotation.VisibleForTesting; import com.google.android.gms.common.api.GoogleApiClient; import com.google.android.gms.drive.DriveFile; import com.google.android.gms.drive.DriveFolder; import com.google.android.gms.drive.DriveId; import com.google.android.gms.drive.Metadata; import com.google.common.base.Preconditions; import com.hadisatrio.optional.Optional; import java.io.File; import java.util.List; import java.util.concurrent.CountDownLatch; import java.util.concurrent.atomic.AtomicReference; import co.smartreceipts.android.sync.drive.device.GoogleDriveSyncMetadata; import co.smartreceipts.android.sync.drive.error.DriveThrowableToSyncErrorTranslator; import co.smartreceipts.android.sync.drive.services.DriveUploadCompleteManager; import co.smartreceipts.android.sync.model.RemoteBackupMetadata; import co.smartreceipts.android.sync.model.SyncState; import co.smartreceipts.android.sync.model.impl.Identifier; import co.smartreceipts.android.sync.provider.SyncProvider; import co.smartreceipts.android.utils.log.Logger; import io.reactivex.Completable; import io.reactivex.Observable; import io.reactivex.Single; import io.reactivex.subjects.Subject; public class DriveStreamsManager implements GoogleApiClient.ConnectionCallbacks { private final DriveDataStreams driveDataStreams; private final DriveStreamMappings driveStreamMappings; private final Subject<Optional<Throwable>> driveErrorStream; private final DriveThrowableToSyncErrorTranslator syncErrorTranslator; private final AtomicReference<CountDownLatch> latchReference; public DriveStreamsManager(@NonNull Context context, @NonNull GoogleApiClient googleApiClient, @NonNull GoogleDriveSyncMetadata googleDriveSyncMetadata, @NonNull Subject<Optional<Throwable>> driveErrorStream, @NonNull DriveUploadCompleteManager driveUploadCompleteManager) { this(new DriveDataStreams(context, googleApiClient, googleDriveSyncMetadata, driveUploadCompleteManager), new DriveStreamMappings(), driveErrorStream, new DriveThrowableToSyncErrorTranslator()); } @VisibleForTesting DriveStreamsManager(@NonNull DriveDataStreams driveDataStreams, @NonNull DriveStreamMappings driveStreamMappings, @NonNull Subject<Optional<Throwable>> driveErrorStream, @NonNull DriveThrowableToSyncErrorTranslator syncErrorTranslator) { this.driveDataStreams = Preconditions.checkNotNull(driveDataStreams); this.driveStreamMappings = Preconditions.checkNotNull(driveStreamMappings); this.driveErrorStream = Preconditions.checkNotNull(driveErrorStream); this.syncErrorTranslator = Preconditions.checkNotNull(syncErrorTranslator); this.latchReference = new AtomicReference<>(new CountDownLatch(1)); } @Override public void onConnected(@Nullable Bundle bundle) { Logger.info(this, "GoogleApiClient connection succeeded."); latchReference.get().countDown(); } @Override public void onConnectionSuspended(int cause) { Logger.info(this, "GoogleApiClient connection suspended with cause {}", cause); latchReference.set(new CountDownLatch(1)); } @NonNull public Single<List<RemoteBackupMetadata>> getRemoteBackups() { return newBlockUntilConnectedCompletable() .andThen(driveDataStreams.getSmartReceiptsFolders()) .doOnError(throwable -> driveErrorStream.onNext(Optional.of(syncErrorTranslator.get(throwable)))); } @NonNull public synchronized Single<DriveId> getDriveId(@NonNull final Identifier identifier) { Preconditions.checkNotNull(identifier); return newBlockUntilConnectedCompletable() .andThen(driveDataStreams.getDriveId(identifier)) .doOnError(throwable -> driveErrorStream.onNext(Optional.of(syncErrorTranslator.get(throwable)))); } @NonNull public synchronized Observable<DriveId> getFilesInFolder(@NonNull final DriveFolder driveFolder) { Preconditions.checkNotNull(driveFolder); return newBlockUntilConnectedCompletable() .andThen(driveDataStreams.getFilesInFolder(driveFolder)) .doOnError(throwable -> driveErrorStream.onNext(Optional.of(syncErrorTranslator.get(throwable)))); } @NonNull public synchronized Observable<DriveId> getFilesInFolder(@NonNull final DriveFolder driveFolder, @NonNull final String fileName) { Preconditions.checkNotNull(driveFolder); Preconditions.checkNotNull(fileName); return newBlockUntilConnectedCompletable() .andThen(driveDataStreams.getFilesInFolder(driveFolder, fileName)) .doOnError(throwable -> driveErrorStream.onNext(Optional.of(syncErrorTranslator.get(throwable)))); } @NonNull public synchronized Single<Metadata> getMetadata(@NonNull final DriveFile driveFile) { Preconditions.checkNotNull(driveFile); return newBlockUntilConnectedCompletable() .andThen(driveDataStreams.getMetadata(driveFile)) .doOnError(throwable -> driveErrorStream.onNext(Optional.of(syncErrorTranslator.get(throwable)))); } @NonNull public Single<SyncState> uploadFileToDrive(@NonNull final SyncState currentSyncState, @NonNull final File file) { Preconditions.checkNotNull(currentSyncState); Preconditions.checkNotNull(file); return newBlockUntilConnectedCompletable() .andThen(driveDataStreams.getSmartReceiptsFolder()) .firstOrError() // hack. because getSmartReceiptsFolder emits just once .flatMap(driveFolder -> driveDataStreams.createFileInFolder(driveFolder, file)) .flatMap(driveFile -> Single.just(driveStreamMappings.postInsertSyncState(currentSyncState, driveFile))) .doOnError(throwable -> driveErrorStream.onNext(Optional.of(syncErrorTranslator.get(throwable)))); } @NonNull public Single<Identifier> uploadFileToDrive(@NonNull final File file) { Preconditions.checkNotNull(file); return newBlockUntilConnectedCompletable() .andThen(driveDataStreams.getSmartReceiptsFolder()) .firstOrError() // hack. because getSmartReceiptsFolder emits just once .flatMap(driveFolder -> driveDataStreams.createFileInFolder(driveFolder, file)) .flatMap(driveFile -> Single.just(new Identifier(driveFile.getDriveId().getResourceId()))) .doOnError(throwable -> driveErrorStream.onNext(Optional.of(syncErrorTranslator.get(throwable)))); } @NonNull public Single<SyncState> updateDriveFile(@NonNull final SyncState currentSyncState, @NonNull final File file) { Preconditions.checkNotNull(currentSyncState); Preconditions.checkNotNull(file); return newBlockUntilConnectedCompletable() .andThen(updateDrive(currentSyncState, file)) .flatMap(driveFile -> Single.just(driveStreamMappings.postUpdateSyncState(currentSyncState, driveFile))) .doOnError(throwable -> driveErrorStream.onNext(Optional.of(syncErrorTranslator.get(throwable)))); } @NonNull public Single<Identifier> updateDriveFile(@NonNull final Identifier currentIdentifier, @NonNull final File file) { Preconditions.checkNotNull(currentIdentifier); Preconditions.checkNotNull(file); return newBlockUntilConnectedCompletable() .andThen(driveDataStreams.updateFile(currentIdentifier, file)) .flatMap(driveFile -> Single.just(new Identifier(driveFile.getDriveId().getResourceId()))) .doOnError(throwable -> driveErrorStream.onNext(Optional.of(syncErrorTranslator.get(throwable)))); } @NonNull public Single<SyncState> deleteDriveFile(@NonNull final SyncState currentSyncState, final boolean isFullDelete) { Preconditions.checkNotNull(currentSyncState); return newBlockUntilConnectedCompletable() .andThen(deleteDrive(currentSyncState)) .flatMap(success -> { if(success) { return Single.just(driveStreamMappings.postDeleteSyncState(currentSyncState, isFullDelete)); } else { return Single.just(currentSyncState); } }) .doOnError(throwable -> driveErrorStream.onNext(Optional.of(syncErrorTranslator.get(throwable)))); } private Single<Boolean> deleteDrive(@NonNull SyncState currentSyncState) { final Identifier driveIdentifier = currentSyncState.getSyncId(SyncProvider.GoogleDrive); if (driveIdentifier != null) { return driveDataStreams.delete(driveIdentifier); } else { return Single.just(true); } } private Single<DriveFile> updateDrive(@NonNull final SyncState currentSyncState, @NonNull final File file) { final Identifier driveIdentifier = currentSyncState.getSyncId(SyncProvider.GoogleDrive); if (driveIdentifier != null) { return driveDataStreams.updateFile(driveIdentifier, file); } else { return Single.error(new Exception("This sync state doesn't include a valid Drive Identifier")); } } @NonNull public Single<Boolean> delete(@NonNull final Identifier identifier) { Preconditions.checkNotNull(identifier); return newBlockUntilConnectedCompletable() .andThen(driveDataStreams.delete(identifier)) .doOnError(throwable -> driveErrorStream.onNext(Optional.of(syncErrorTranslator.get(throwable)))); } public void clearCachedData() { driveDataStreams.clear(); } @NonNull public Single<File> download(@NonNull final DriveFile driveFile, @NonNull final File downloadLocationFile) { Preconditions.checkNotNull(driveFile); Preconditions.checkNotNull(downloadLocationFile); return newBlockUntilConnectedCompletable() .andThen(driveDataStreams.download(driveFile, downloadLocationFile)) .doOnError(throwable -> driveErrorStream.onNext(Optional.of(syncErrorTranslator.get(throwable)))); } @NonNull private Completable newBlockUntilConnectedCompletable() { return Completable.fromAction(() -> { final CountDownLatch countDownLatch = latchReference.get(); try { countDownLatch.await(); } catch (InterruptedException e) { throw new Exception("newBlockUntilConnectedCompletable failed"); } }); } }