package co.smartreceipts.android.sync.drive.services; import android.support.annotation.NonNull; import com.google.android.gms.drive.DriveId; import com.google.android.gms.drive.events.CompletionEvent; import com.google.common.base.Preconditions; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import javax.inject.Inject; import co.smartreceipts.android.analytics.Analytics; import co.smartreceipts.android.analytics.events.Events; import co.smartreceipts.android.di.scopes.ApplicationScope; import co.smartreceipts.android.utils.log.Logger; /** * Drive's APIs are not RESTful, so we can get the event resource id at any time after an upload completes. * Usually if we have network, it's pretty quick. Additionally, Google tied us to using a manifest-registered * service for this stuff, so we really have nothing in the means of dynamic controls for this :(... But the * good news is, we can use Dagger to easily inject this class and effectively remove our reliance on their * service definition. * <p> * This class services as a more "OO-friendly" wrapper that we can use for unit testing purposes * </p> */ @ApplicationScope public class DriveUploadCompleteManager { @Inject Analytics analytics; private final Object lock = new Object(); private final Map<DriveIdUploadMetadata, DriveIdUploadCompleteCallback> metadataToCallbackMap = new HashMap<>(); private final Map<DriveId, DriveIdUploadMetadata> driveIdToMetadataMap = new HashMap<>(); private final Map<String, DriveIdUploadMetadata> trackingTagToMetadataMap = new HashMap<>(); @Inject public DriveUploadCompleteManager(@NonNull Analytics analytics) { this.analytics = Preconditions.checkNotNull(analytics); } /** * Should be triggered via the {@link DriveCompletionEventService#onCompletion(CompletionEvent)} method to * allow us to perform our processing outside of our service (and hence allow for easier injection). * <p> * This has been intentionally made "package-private" in order to limit the degree of interaction * </p> * @param event a {@link DriveCompletionEventWrapper} which contains the underlying event */ void onCompletion(@NonNull DriveCompletionEventWrapper event) { final DriveIdUploadCompleteCallback callback; final DriveIdUploadMetadata foundMetadata; synchronized (lock) { final DriveIdUploadMetadata metadataFromDriveId = driveIdToMetadataMap.remove(event.getDriveId()); if (metadataFromDriveId != null) { // If we were able to find the metadata via the event's drive id, use that for the callback (and clear the rest) foundMetadata = metadataFromDriveId; callback = metadataToCallbackMap.remove(metadataFromDriveId); trackingTagToMetadataMap.remove(metadataFromDriveId.getTrackingTag()); } else { // Otherwise, let's see if we can grab it from the tracking tags (and then clear the rest) final List<String> intersection = new ArrayList<>(trackingTagToMetadataMap.keySet()); intersection.retainAll(event.getTrackingTags()); if (intersection.size() == 1) { final DriveIdUploadMetadata metadataFromTrackingTag = trackingTagToMetadataMap.remove(intersection.get(0)); foundMetadata = metadataFromTrackingTag; callback = metadataToCallbackMap.remove(metadataFromTrackingTag); driveIdToMetadataMap.remove(metadataFromTrackingTag.getDriveId()); } else { foundMetadata = null; callback = null; } } } // Note: doing this outside our lock if (callback != null) { if (event.getStatus() == CompletionEvent.STATUS_SUCCESS) { Logger.info(DriveCompletionEventService.class, "Calling back with persisted drive id {} with resource id: {}. Source: {}.", event.getDriveId(), event.getDriveId().getResourceId(), foundMetadata); analytics.record(Events.Sync.DriveCompletionEventHandledWithSuccess); callback.onSuccess(event.getDriveId()); } else { Logger.error(DriveCompletionEventService.class, "Calling back with drive resource id failure. Source: {}.", foundMetadata); analytics.record(Events.Sync.DriveCompletionEventHandledWithFailure); callback.onFailure(event.getDriveId()); } } else { Logger.warn(DriveCompletionEventService.class, "Received an event before a callback was registered. Source: {}.", foundMetadata); analytics.record(Events.Sync.DriveCompletionEventNotHandled); } event.dismiss(); } /** * Registers a callback to inform us when a specific drive id has a valid resource id, so we can persist * this as having fully completed * * @param driveIdUploadMetadata - the metadata corresponding to this upload * @param callback - the callback to inform us when this has occurred */ public void registerCallback(@NonNull DriveIdUploadMetadata driveIdUploadMetadata, @NonNull DriveIdUploadCompleteCallback callback) { Preconditions.checkNotNull(driveIdUploadMetadata); Preconditions.checkNotNull(callback); synchronized (lock) { metadataToCallbackMap.put(driveIdUploadMetadata, callback); driveIdToMetadataMap.put(driveIdUploadMetadata.getDriveId(), driveIdUploadMetadata); trackingTagToMetadataMap.put(driveIdUploadMetadata.getTrackingTag(), driveIdUploadMetadata); } Logger.info(DriveCompletionEventService.class, "Registered for completion of id: {}", driveIdUploadMetadata); } }