package co.smartreceipts.android.ocr.purchases;
import android.content.Context;
import android.support.annotation.NonNull;
import android.support.annotation.VisibleForTesting;
import com.google.common.base.Preconditions;
import org.reactivestreams.Subscriber;
import javax.inject.Inject;
import co.smartreceipts.android.apis.ApiValidationException;
import co.smartreceipts.android.apis.hosts.ServiceManager;
import co.smartreceipts.android.di.scopes.ApplicationScope;
import co.smartreceipts.android.identity.IdentityManager;
import co.smartreceipts.android.purchases.PurchaseEventsListener;
import co.smartreceipts.android.purchases.PurchaseManager;
import co.smartreceipts.android.purchases.apis.MobileAppPurchasesService;
import co.smartreceipts.android.purchases.apis.PurchaseRequest;
import co.smartreceipts.android.purchases.model.ConsumablePurchase;
import co.smartreceipts.android.purchases.model.InAppPurchase;
import co.smartreceipts.android.purchases.model.ManagedProduct;
import co.smartreceipts.android.purchases.model.PurchaseFamily;
import co.smartreceipts.android.purchases.source.PurchaseSource;
import co.smartreceipts.android.purchases.wallet.PurchaseWallet;
import co.smartreceipts.android.utils.log.Logger;
import io.reactivex.Observable;
import io.reactivex.Scheduler;
import io.reactivex.schedulers.Schedulers;
@ApplicationScope
public class OcrPurchaseTracker implements PurchaseEventsListener {
private static final String GOAL = "Recognition";
private final IdentityManager identityManager;
private final ServiceManager serviceManager;
private final PurchaseManager purchaseManager;
private final PurchaseWallet purchaseWallet;
private final LocalOcrScansTracker localOcrScansTracker;
private final Scheduler subscribeOnScheduler;
@Inject
public OcrPurchaseTracker(Context context, IdentityManager identityManager, ServiceManager serviceManager,
PurchaseManager purchaseManager, PurchaseWallet purchaseWallet) {
this(identityManager, serviceManager, purchaseManager, purchaseWallet, new LocalOcrScansTracker(context), Schedulers.io());
}
@VisibleForTesting
OcrPurchaseTracker(@NonNull IdentityManager identityManager, @NonNull ServiceManager serviceManager,
@NonNull PurchaseManager purchaseManager, @NonNull PurchaseWallet purchaseWallet,
@NonNull LocalOcrScansTracker localOcrScansTracker, @NonNull Scheduler subscribeOnScheduler) {
this.identityManager = Preconditions.checkNotNull(identityManager);
this.serviceManager = Preconditions.checkNotNull(serviceManager);
this.purchaseManager = Preconditions.checkNotNull(purchaseManager);
this.purchaseWallet = Preconditions.checkNotNull(purchaseWallet);
this.localOcrScansTracker = Preconditions.checkNotNull(localOcrScansTracker);
this.subscribeOnScheduler = Preconditions.checkNotNull(subscribeOnScheduler);
}
public void initialize() {
Logger.info(this, "Initializing...");
this.purchaseManager.addEventListener(this);
this.identityManager.isLoggedInStream()
.subscribeOn(subscribeOnScheduler)
.filter(isLoggedIn -> isLoggedIn)
.flatMap(aBoolean -> {
// Attempt to update our latest scan count
return fetchAndPersistAvailableRecognitions();
})
.flatMap(integer -> purchaseManager.getAllOwnedPurchases())
.flatMap(managedProducts -> {
for (final ManagedProduct managedProduct : managedProducts) {
if (PurchaseFamily.Ocr.equals(managedProduct.getInAppPurchase().getPurchaseFamily())) {
if (managedProduct instanceof ConsumablePurchase) {
return uploadOcrPurchase((ConsumablePurchase) managedProduct);
}
}
}
return Observable.empty();
})
.subscribe(o -> Logger.info(OcrPurchaseTracker.this, "Successfully initialized"),
throwable -> Logger.error(OcrPurchaseTracker.this, "Failed to initialize.", throwable));
}
@Override
public void onPurchaseSuccess(@NonNull InAppPurchase inAppPurchase, @NonNull PurchaseSource purchaseSource) {
if (PurchaseFamily.Ocr.equals(inAppPurchase.getPurchaseFamily())) {
final ManagedProduct managedProduct = purchaseWallet.getManagedProduct(inAppPurchase);
if (managedProduct instanceof ConsumablePurchase) {
final ConsumablePurchase consumablePurchase = (ConsumablePurchase) managedProduct;
this.identityManager.isLoggedInStream()
.subscribeOn(subscribeOnScheduler)
.filter(isLoggedIn -> isLoggedIn)
.flatMap(aBoolean -> uploadOcrPurchase(consumablePurchase))
.subscribe(o -> { /*onNext*/ },
throwable -> Logger.error(OcrPurchaseTracker.this, "Failed to upload purchase of " + consumablePurchase.getInAppPurchase(), throwable),
() -> Logger.info(OcrPurchaseTracker.this, "Successfully uploaded and consumed purchase of {}.", consumablePurchase.getInAppPurchase())
);
}
}
}
@Override
public void onPurchaseFailed(@NonNull PurchaseSource purchaseSource) {
}
/**
* @return the remaining Ocr scan count that is allowed for this user. Please note that is
* this not the authority for this (ie it's not the server), this may not be fully accurate, so we
* may still get a remote error after a scan
*/
public int getRemainingScans() {
return localOcrScansTracker.getRemainingScans();
}
/**
* @return the remaining Ocr scan count that is allowed for this user. Please note that is
* this not the authority for this (ie it's not the server), this may not be fully accurate, so we
* may still get a remote error after a scan. Additionally, please note that this {@link Observable}
* will only call {@link Subscriber#onNext(Object)} with the latest value (and never onComplete or
* onError) to allow us to continually get the updated value
*/
public Observable<Integer> getRemainingScansStream() {
return localOcrScansTracker.getRemainingScansStream();
}
/**
* @return {@code true} if we have OCR scans remaining. {@code false} otherwise. Please note that is
* this not the authority for this (ie it's not the server), this may not be fully accurate, so we
* may still get a remote error after a scan
*/
public boolean hasAvailableScans() {
return localOcrScansTracker.getRemainingScans() > 0;
}
/**
* Decrements the remaining scan count by 1, to indicate that we've successfully used one of our scans
*/
public void decrementRemainingScans() {
localOcrScansTracker.decrementRemainingScans();
}
@NonNull
private Observable<Object> uploadOcrPurchase(@NonNull final ConsumablePurchase consumablePurchase) {
if (consumablePurchase.getInAppPurchase().getPurchaseFamily() != PurchaseFamily.Ocr) {
throw new IllegalArgumentException("Unsupported purchase type: " + consumablePurchase.getInAppPurchase());
}
Logger.info(this, "Uploading purchase: {}", consumablePurchase.getInAppPurchase());
return serviceManager.getService(MobileAppPurchasesService.class).addPurchase(new PurchaseRequest(consumablePurchase, GOAL))
.flatMap(purchaseResponse -> {
Logger.debug(OcrPurchaseTracker.this, "Received purchase response of {}", purchaseResponse);
return purchaseManager.consumePurchase(consumablePurchase).andThen(Observable.just(new Object()));
})
.flatMap(o -> fetchAndPersistAvailableRecognitions());
}
@NonNull
private Observable<Integer> fetchAndPersistAvailableRecognitions() {
return this.identityManager.getMe()
.subscribeOn(subscribeOnScheduler)
.flatMap(meResponse -> {
if (meResponse != null && meResponse.getUser() != null) {
return Observable.just(meResponse.getUser().getRecognitionsAvailable());
} else {
return Observable.error(new ApiValidationException("Failed to get a user response back"));
}
})
.doOnNext(recognitionsAvailable -> {
if (recognitionsAvailable != null) {
localOcrScansTracker.setRemainingScans(recognitionsAvailable);
}
})
.doOnError(throwable -> Logger.error(OcrPurchaseTracker.this, "Failed to get the available OCR scans", throwable))
.onErrorReturn(throwable -> {
return 0; // ignore errors and keep moving to get the owned purchases
});
}
}