package co.smartreceipts.android.aws.cognito; import android.content.Context; import android.support.annotation.NonNull; import android.support.annotation.VisibleForTesting; import com.amazonaws.auth.CognitoCachingCredentialsProvider; import com.amazonaws.regions.Regions; import com.google.common.base.Preconditions; import com.hadisatrio.optional.Optional; import javax.inject.Inject; import co.smartreceipts.android.di.scopes.ApplicationScope; import co.smartreceipts.android.identity.IdentityManager; import io.reactivex.BackpressureStrategy; import io.reactivex.Observable; import io.reactivex.Scheduler; import io.reactivex.disposables.Disposable; import io.reactivex.schedulers.Schedulers; import io.reactivex.subjects.PublishSubject; import io.reactivex.subjects.ReplaySubject; @ApplicationScope public class CognitoManager { private final Context context; private final IdentityManager identityManager; private final CognitoIdentityProvider cognitoIdentityProvider; private final Scheduler subscribeOnScheduler; private final PublishSubject<Object> retryErrorsOnSubscribePredicate = PublishSubject.create(); private final ReplaySubject<Optional<CognitoCachingCredentialsProvider>> cachingCredentialsProviderReplaySubject = ReplaySubject.createWithSize(1); private Disposable cachingCredentialsProviderDisposable; @Inject public CognitoManager(Context context, IdentityManager identityManager) { this(context, identityManager, new CognitoIdentityProvider(identityManager, context), Schedulers.io()); } @VisibleForTesting CognitoManager(@NonNull Context context, @NonNull IdentityManager identityManager, @NonNull CognitoIdentityProvider cognitoIdentityProvider, @NonNull Scheduler subscribeOnScheduler) { this.context = Preconditions.checkNotNull(context); this.identityManager = Preconditions.checkNotNull(identityManager); this.cognitoIdentityProvider = Preconditions.checkNotNull(cognitoIdentityProvider); this.subscribeOnScheduler = Preconditions.checkNotNull(subscribeOnScheduler); } public void initialize() { cachingCredentialsProviderDisposable = identityManager.isLoggedInStream() .subscribeOn(subscribeOnScheduler) .flatMap(isLoggedIn -> { if (isLoggedIn) { return cognitoIdentityProvider.prefetchCognitoTokenIfNeeded() .retryWhen(throwableFlowable -> throwableFlowable .flatMap(throwable -> retryErrorsOnSubscribePredicate.toFlowable( BackpressureStrategy.MISSING))) .map(cognitoOptional -> { final SmartReceiptsAuthenticationProvider authenticationProvider = new SmartReceiptsAuthenticationProvider(cognitoIdentityProvider, getRegions()); return Optional.of(new CognitoCachingCredentialsProvider(context, authenticationProvider, getRegions())); }) .onErrorReturn(throwable -> Optional.absent()) .toObservable(); } else { return Observable.just(Optional.<CognitoCachingCredentialsProvider>absent()); } }) .subscribe(cognitoCachingCredentialsProviderOptional -> { cachingCredentialsProviderReplaySubject.onNext(cognitoCachingCredentialsProviderOptional); if (cognitoCachingCredentialsProviderOptional.isPresent()) { cachingCredentialsProviderReplaySubject.onComplete(); if (cachingCredentialsProviderDisposable != null) { cachingCredentialsProviderDisposable.dispose(); } } }, cachingCredentialsProviderReplaySubject::onError, cachingCredentialsProviderReplaySubject::onComplete); } /** * @return an {@link Optional} instance of the {@link CognitoCachingCredentialsProvider}. Once we * fetch a valid entry, this should be treated as a singleton for the lifetime of the parent * {@link CognitoManager} object, since we use a replay subject */ @NonNull public Observable<Optional<CognitoCachingCredentialsProvider>> getCognitoCachingCredentialsProvider() { return cachingCredentialsProviderReplaySubject .doOnSubscribe(disposable -> { // Any time we subscribe, let's see if we can resolve any latent errors retryErrorsOnSubscribePredicate.onNext(new Object()); }); } @NonNull public Regions getRegions() { return Regions.US_EAST_1; } }