package co.smartreceipts.android.sync.drive; import android.app.Activity; import android.content.Context; import android.content.Intent; import android.content.IntentSender; import android.os.Bundle; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.v4.app.FragmentActivity; import com.google.android.gms.common.ConnectionResult; import com.google.android.gms.common.GoogleApiAvailability; import com.google.android.gms.common.api.GoogleApiClient; import com.google.android.gms.drive.Drive; import com.google.common.base.Preconditions; import com.hadisatrio.optional.Optional; import java.io.File; import java.lang.ref.WeakReference; import java.sql.Date; import java.util.List; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; import javax.inject.Inject; import co.smartreceipts.android.analytics.Analytics; import co.smartreceipts.android.persistence.DatabaseHelper; import co.smartreceipts.android.persistence.database.controllers.impl.ReceiptTableController; import co.smartreceipts.android.sync.BackupProvider; import co.smartreceipts.android.sync.drive.device.GoogleDriveSyncMetadata; import co.smartreceipts.android.sync.drive.managers.DriveDatabaseManager; import co.smartreceipts.android.sync.drive.managers.DriveReceiptsManager; import co.smartreceipts.android.sync.drive.managers.DriveRestoreDataManager; import co.smartreceipts.android.sync.drive.managers.GoogleDriveTableManager; import co.smartreceipts.android.sync.drive.rx.DriveStreamsManager; import co.smartreceipts.android.sync.drive.services.DriveUploadCompleteManager; import co.smartreceipts.android.sync.errors.CriticalSyncError; import co.smartreceipts.android.sync.errors.SyncErrorType; import co.smartreceipts.android.sync.model.RemoteBackupMetadata; import co.smartreceipts.android.sync.model.impl.Identifier; import co.smartreceipts.android.sync.network.NetworkManager; import co.smartreceipts.android.sync.network.NetworkStateChangeListener; import co.smartreceipts.android.utils.log.Logger; import io.reactivex.Observable; import io.reactivex.Single; import io.reactivex.subjects.BehaviorSubject; public class GoogleDriveBackupManager implements BackupProvider, GoogleApiClient.ConnectionCallbacks, GoogleApiClient.OnConnectionFailedListener, NetworkStateChangeListener { /** * Request code for auto Google Play Services error resolution. */ private static final int REQUEST_CODE_RESOLUTION = 1; private final GoogleDriveTableManager mGoogleDriveTableManager; private final GoogleApiClient mGoogleApiClient; private final DriveStreamsManager mDriveTaskManager; private final AtomicReference<WeakReference<FragmentActivity>> mActivityReference; private final NetworkManager mNetworkManager; private final GoogleDriveSyncMetadata mGoogleDriveSyncMetadata; private final DriveReceiptsManager mDriveReceiptsManager; private final DriveRestoreDataManager mDriveRestoreDataManager; private final BehaviorSubject<Optional<Throwable>> mSyncErrorStream; @Inject public GoogleDriveBackupManager(@NonNull Context context, @NonNull DatabaseHelper databaseHelper, @NonNull GoogleDriveTableManager googleDriveTableManager, @NonNull NetworkManager networkManager, @NonNull Analytics analytics, @NonNull ReceiptTableController receiptTableController, @NonNull DriveUploadCompleteManager driveUploadCompleteManager) { mGoogleApiClient = new GoogleApiClient.Builder(context.getApplicationContext()) .addConnectionCallbacks(this) .addOnConnectionFailedListener(this) .addApi(Drive.API) .addScope(Drive.SCOPE_APPFOLDER) .useDefaultAccount() .build(); mGoogleDriveSyncMetadata = new GoogleDriveSyncMetadata(context); mNetworkManager = networkManager; mSyncErrorStream = BehaviorSubject.create(); mDriveTaskManager = new DriveStreamsManager(context, mGoogleApiClient, mGoogleDriveSyncMetadata, mSyncErrorStream, driveUploadCompleteManager); mActivityReference = new AtomicReference<>(new WeakReference<FragmentActivity>(null)); final DriveDatabaseManager driveDatabaseManager = new DriveDatabaseManager(context, mDriveTaskManager, mGoogleDriveSyncMetadata, mNetworkManager, analytics); mDriveReceiptsManager = new DriveReceiptsManager(receiptTableController, databaseHelper.getReceiptsTable(), mDriveTaskManager, driveDatabaseManager, mNetworkManager, analytics); mDriveRestoreDataManager = new DriveRestoreDataManager(context, mDriveTaskManager, databaseHelper, driveDatabaseManager); mGoogleDriveTableManager = googleDriveTableManager; mGoogleDriveTableManager.initBackupListeners(driveDatabaseManager, mDriveReceiptsManager); } @Override public void initialize(@Nullable FragmentActivity activity) { Preconditions.checkNotNull(activity, "Google Drive requires a valid activity to be provided"); final FragmentActivity existingActivity = mActivityReference.get().get(); if (!activity.equals(existingActivity)) { mActivityReference.set(new WeakReference<>(activity)); } if (!isConnectedOrConnecting()) { mGoogleApiClient.connect(); } mNetworkManager.registerListener(this); } @Override public void deinitialize() { mNetworkManager.unregisterListener(this); if (isConnectedOrConnecting()) { mGoogleApiClient.disconnect(); } } @Override public boolean onActivityResult(int requestCode, int resultCode, @Nullable Intent data) { if (requestCode == REQUEST_CODE_RESOLUTION && resultCode == Activity.RESULT_OK) { if (!isConnectedOrConnecting()) { mGoogleApiClient.connect(); } return true; } else { return false; } } @NonNull @Override public Single<List<RemoteBackupMetadata>> getRemoteBackups() { return mDriveTaskManager.getRemoteBackups(); } @Nullable @Override public Identifier getDeviceSyncId() { return mGoogleDriveSyncMetadata.getDeviceIdentifier(); } @NonNull @Override public Date getLastDatabaseSyncTime() { return mGoogleDriveSyncMetadata.getLastDatabaseSyncTime(); } @NonNull @Override public Single<Boolean> restoreBackup(@NonNull RemoteBackupMetadata remoteBackupMetadata, boolean overwriteExistingData) { return mDriveRestoreDataManager.restoreBackup(remoteBackupMetadata, overwriteExistingData); } @NonNull @Override public Single<Boolean> deleteBackup(@NonNull RemoteBackupMetadata remoteBackupMetadata) { Preconditions.checkNotNull(remoteBackupMetadata); if (remoteBackupMetadata.getSyncDeviceId().equals(mGoogleDriveSyncMetadata.getDeviceIdentifier())) { mGoogleDriveSyncMetadata.clear(); mDriveReceiptsManager.disable(); } return mDriveTaskManager.delete(remoteBackupMetadata.getId()) .doOnSuccess(success -> { mDriveReceiptsManager.enable(); if (success) { mDriveReceiptsManager.initialize(); } }); } @Override public Single<Boolean> clearCurrentBackupConfiguration() { mDriveReceiptsManager.disable(); mGoogleDriveSyncMetadata.clear(); mDriveTaskManager.clearCachedData(); // Note: We added a stupid delay hack here to allow things to clear out of their buffers return Single.just(true) .delay(500, TimeUnit.MILLISECONDS) .doOnSuccess(success -> { mDriveReceiptsManager.enable(); if (success) { mDriveReceiptsManager.initialize(); } }); } @NonNull @Override public Single<List<File>> downloadAllData(@NonNull RemoteBackupMetadata remoteBackupMetadata, @NonNull File downloadLocation) { return mDriveRestoreDataManager.downloadAllBackupMetadataImages(remoteBackupMetadata, downloadLocation); } @NonNull @Override public Single<List<File>> debugDownloadAllData(@NonNull RemoteBackupMetadata remoteBackupMetadata, @NonNull File downloadLocation) { return mDriveRestoreDataManager.downloadAllFilesInDriveFolder(remoteBackupMetadata, downloadLocation); } @NonNull @Override public Observable<CriticalSyncError> getCriticalSyncErrorStream() { return mSyncErrorStream.filter(Optional::isPresent) .map(Optional::get) .<Optional<CriticalSyncError>>map(throwable -> { if (throwable instanceof CriticalSyncError) { return Optional.of((CriticalSyncError) throwable); } else { return Optional.absent(); } }) .filter(Optional::isPresent) .map(Optional::get); } @Override public void markErrorResolved(@NonNull SyncErrorType syncErrorType) { mSyncErrorStream.onNext(Optional.absent()); } @Override public void onConnectionFailed(@NonNull ConnectionResult result) { Logger.warn(this, "GoogleApiClient connection failed: {}", result); final FragmentActivity activity = mActivityReference.get().get(); if (activity == null) { Logger.error(this, "The parent activity was destroyed. Unable to resolve GoogleApiClient connection failure."); return; } try { if (!result.hasResolution()) { GoogleApiAvailability.getInstance().getErrorDialog(activity, result.getErrorCode(), 0).show(); return; } try { result.startResolutionForResult(activity, REQUEST_CODE_RESOLUTION); } catch (IntentSender.SendIntentException e) { Logger.error(this, "Exception while starting resolution activity", e); } } catch (IllegalStateException e) { Logger.warn(this, "The parent activity is in a bad state.. Unable to resolve GoogleApiClient connection failure."); } } @Override public void onConnected(@Nullable Bundle bundle) { mDriveTaskManager.onConnected(bundle); mDriveReceiptsManager.initialize(); mGoogleDriveTableManager.onConnected(); } @Override public void onConnectionSuspended(int cause) { mGoogleDriveTableManager.onConnectionSuspended(); mDriveTaskManager.onConnectionSuspended(cause); } @Override public void onNetworkConnectivityLost() { } @Override public void onNetworkConnectivityGained() { Logger.info(this, "Handling a NetworkConnectivityGained event for drive"); if (!isConnectedOrConnecting()) { final FragmentActivity existingActivity = mActivityReference.get().get(); if (existingActivity != null) { initialize(existingActivity); } } else { mDriveReceiptsManager.initialize(); } } private boolean isConnectedOrConnecting() { return mGoogleApiClient.isConnected() || mGoogleApiClient.isConnecting(); } }