package co.smartreceipts.android.ocr; import android.content.Context; import android.support.annotation.NonNull; import android.support.annotation.VisibleForTesting; import com.google.common.base.Preconditions; import java.io.File; import javax.inject.Inject; import co.smartreceipts.android.analytics.Analytics; import co.smartreceipts.android.analytics.events.ErrorEvent; import co.smartreceipts.android.analytics.events.Events; import co.smartreceipts.android.apis.ApiValidationException; import co.smartreceipts.android.apis.hosts.ServiceManager; import co.smartreceipts.android.aws.s3.S3Manager; import co.smartreceipts.android.di.scopes.ApplicationScope; import co.smartreceipts.android.identity.IdentityManager; import co.smartreceipts.android.ocr.apis.OcrService; import co.smartreceipts.android.ocr.apis.model.OcrResponse; import co.smartreceipts.android.ocr.apis.model.RecongitionRequest; import co.smartreceipts.android.ocr.purchases.OcrPurchaseTracker; import co.smartreceipts.android.ocr.push.OcrPushMessageReceiver; import co.smartreceipts.android.ocr.push.OcrPushMessageReceiverFactory; import co.smartreceipts.android.ocr.widget.alert.OcrProcessingStatus; import co.smartreceipts.android.push.PushManager; import co.smartreceipts.android.settings.UserPreferenceManager; import co.smartreceipts.android.settings.catalog.UserPreference; import co.smartreceipts.android.utils.Feature; import co.smartreceipts.android.utils.FeatureFlags; import co.smartreceipts.android.utils.log.Logger; import io.reactivex.Observable; import io.reactivex.schedulers.Schedulers; import io.reactivex.subjects.BehaviorSubject; @ApplicationScope public class OcrManager { private static final String OCR_FOLDER = "ocr/"; private final Context context; private final S3Manager s3Manager; private final IdentityManager identityManager; private final ServiceManager ocrServiceManager; private final PushManager pushManager; private final UserPreferenceManager userPreferenceManager; private final Analytics analytics; private final OcrPurchaseTracker ocrPurchaseTracker; private final OcrPushMessageReceiverFactory pushMessageReceiverFactory; private final Feature ocrFeature; private final BehaviorSubject<OcrProcessingStatus> ocrProcessingStatusSubject = BehaviorSubject.createDefault(OcrProcessingStatus.Idle); @Inject public OcrManager(@NonNull Context context, @NonNull S3Manager s3Manager, @NonNull IdentityManager identityManager, @NonNull ServiceManager serviceManager, @NonNull PushManager pushManager, @NonNull OcrPurchaseTracker ocrPurchaseTracker, @NonNull UserPreferenceManager userPreferenceManager, @NonNull Analytics analytics) { this(context, s3Manager, identityManager, serviceManager, pushManager, ocrPurchaseTracker, userPreferenceManager, analytics, new OcrPushMessageReceiverFactory(), FeatureFlags.Ocr); } @VisibleForTesting OcrManager(@NonNull Context context, @NonNull S3Manager s3Manager, @NonNull IdentityManager identityManager, @NonNull ServiceManager serviceManager, @NonNull PushManager pushManager, @NonNull OcrPurchaseTracker ocrPurchaseTracker, @NonNull UserPreferenceManager userPreferenceManager, @NonNull Analytics analytics, @NonNull OcrPushMessageReceiverFactory pushMessageReceiverFactory, @NonNull Feature ocrFeature) { this.context = Preconditions.checkNotNull(context.getApplicationContext()); this.s3Manager = Preconditions.checkNotNull(s3Manager); this.identityManager = Preconditions.checkNotNull(identityManager); this.ocrServiceManager = Preconditions.checkNotNull(serviceManager); this.pushManager = Preconditions.checkNotNull(pushManager); this.ocrPurchaseTracker = Preconditions.checkNotNull(ocrPurchaseTracker); this.userPreferenceManager = Preconditions.checkNotNull(userPreferenceManager); this.analytics = Preconditions.checkNotNull(analytics); this.pushMessageReceiverFactory = Preconditions.checkNotNull(pushMessageReceiverFactory); this.ocrFeature = Preconditions.checkNotNull(ocrFeature); } public void initialize() { ocrPurchaseTracker.initialize(); } @NonNull public Observable<OcrResponse> scan(@NonNull File file) { Preconditions.checkNotNull(file); ocrProcessingStatusSubject.onNext(OcrProcessingStatus.Idle); if (ocrFeature.isEnabled() && identityManager.isLoggedIn() && ocrPurchaseTracker.hasAvailableScans() && userPreferenceManager.get(UserPreference.Misc.OcrIsEnabled)) { Logger.info(OcrManager.this, "Initiating scan of {}.", file); final OcrPushMessageReceiver ocrPushMessageReceiver = pushMessageReceiverFactory.get(); ocrProcessingStatusSubject.onNext(OcrProcessingStatus.UploadingImage); return s3Manager.upload(file, OCR_FOLDER) .doOnSubscribe(disposable -> { pushManager.registerReceiver(ocrPushMessageReceiver); analytics.record(Events.Ocr.OcrRequestStarted); }) .subscribeOn(Schedulers.io()) .flatMap(s3Url -> { Logger.debug(OcrManager.this, "S3 upload completed. Preparing url for delivery to our APIs."); if (s3Url != null && s3Url.indexOf(OCR_FOLDER) > 0) { return Observable.just(s3Url.substring(s3Url.indexOf(OCR_FOLDER))); } else { return Observable.error(new ApiValidationException("Failed to receive a valid url: " + s3Url)); } }) .flatMap(s3Url -> { Logger.debug(OcrManager.this, "Uploading OCR request for processing"); ocrProcessingStatusSubject.onNext(OcrProcessingStatus.PerformingScan); final boolean incognito = userPreferenceManager.get(UserPreference.Misc.OcrIncognitoMode); return ocrServiceManager.getService(OcrService.class).scanReceipt(new RecongitionRequest(s3Url, incognito)); }) .flatMap(recognitionResponse -> { if (recognitionResponse != null && recognitionResponse.getRecognition() != null && recognitionResponse.getRecognition().getId() != null) { return Observable.just(recognitionResponse.getRecognition().getId()); } else { return Observable.error(new ApiValidationException("Failed to receive a valid recognition response.")); } }) .flatMap(recognitionId -> { Logger.debug(OcrManager.this, "Awaiting completion of recognition request {}.", recognitionId); return ocrPushMessageReceiver.getOcrPushResponse() .doOnNext(ignore -> analytics.record(Events.Ocr.OcrPushMessageReceived)) .doOnError(ignore -> analytics.record(Events.Ocr.OcrPushMessageTimeOut)) .onErrorReturn(throwable -> { Logger.warn(OcrManager.this, "Ocr request timed out. Attempting to get response as is"); return new Object(); }) .map(o -> recognitionId); }) .flatMap(recognitionId -> { Logger.debug(OcrManager.this, "Scan completed. Fetching results for {}.", recognitionId); ocrProcessingStatusSubject.onNext(OcrProcessingStatus.RetrievingResults); return ocrServiceManager.getService(OcrService.class).getRecognitionResult(recognitionId); }) .flatMap(recognitionResponse -> { Logger.debug(OcrManager.this, "Parsing OCR Response"); if (recognitionResponse != null && recognitionResponse.getRecognition() != null && recognitionResponse.getRecognition().getData() != null && recognitionResponse.getRecognition().getData().getRecognitionData() != null) { return Observable.just(recognitionResponse.getRecognition().getData().getRecognitionData()); } else { return Observable.error(new ApiValidationException("Failed to receive a valid recognition response.")); } }) .doOnNext(ignore -> { ocrPurchaseTracker.decrementRemainingScans(); analytics.record(Events.Ocr.OcrRequestSucceeded); }) .doOnError(throwable -> { analytics.record(Events.Ocr.OcrRequestFailed); analytics.record(new ErrorEvent(OcrManager.this, throwable)); }) .onErrorReturnItem(new OcrResponse()) .doOnTerminate(() -> { ocrProcessingStatusSubject.onNext(OcrProcessingStatus.Idle); pushManager.unregisterReceiver(ocrPushMessageReceiver); }); } else { Logger.debug(OcrManager.this, "Ignoring ocr scan of as: isFeatureEnabled = {}, isLoggedIn = {}, hasAvailableScans = {}.", ocrFeature.isEnabled(), identityManager.isLoggedIn(), ocrPurchaseTracker.hasAvailableScans()); return Observable.just(new OcrResponse()); } } @NonNull public Observable<OcrProcessingStatus> getOcrProcessingStatus() { return ocrProcessingStatusSubject .subscribeOn(Schedulers.computation()); } }