blob: bfbbbd528625786337c06f52275c6964d368d5fc [file] [log] [blame]
// Copyright 2015 The Vanadium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package io.v.baku.toolkit;
import android.app.Activity;
import io.v.android.libs.security.BlessingsManager;
import io.v.android.v23.services.blessing.BlessingCreationException;
import io.v.v23.security.Blessings;
import io.v.v23.verror.VException;
import lombok.Getter;
import lombok.Synchronized;
import lombok.experimental.Accessors;
import lombok.extern.slf4j.Slf4j;
import rx.Observable;
import rx.subjects.PublishSubject;
@Accessors(prefix = "m")
@Slf4j
public class BlessedActivityTrait implements BlessingsProvider {
@Getter
private final Activity mActivity;
@Getter
private final ErrorReporter mErrorReporter;
/**
* An observable that, when subscribed to, refreshes the blessing. If the account manager needs
* to be invoked for the refresh, the subscription will not produce results until the invocation
* completes. Subsequently, it will receive all blessings refreshed via
* {@link #refreshBlessings()} and other subscriptions to {@link #getRxBlessings()}.
*/
@Getter
private final Observable<Blessings> mRxBlessings;
/**
* An observable for the blessings that does not refresh when subscribed to. Upon subscription,
* this will produce the last known blessing. It will subsequently receive all blessings
* refreshed via {@link #refreshBlessings()} and subscriptions to {@link #getRxBlessings()}.
*/
@Getter
private final Observable<Blessings> mPassiveRxBlessings;
private final PublishSubject<Observable<Blessings>> mPub;
private Blessings mLastBlessings;
private Observable<Blessings> mCurrentSeek;
private final Object mSeekLock = new Object();
public BlessedActivityTrait(final Activity activity, final ErrorReporter errorReporter) {
mActivity = activity;
mErrorReporter = errorReporter;
mPub = PublishSubject.create();
mPassiveRxBlessings = Observable.switchOnNext(mPub)
.distinctUntilChanged()
.replay(1).autoConnect();
/* It might make more sense for b -> mLastBlessings = b to be an onNext before the above
replay rather than a subscription (especially if we start getting
OnErrorNotImplementedException or have to include a possibly redundant error reporter).
However, replay, even with autoConnect(0), does not offer backpressure support unless it has
subscribers. We can get around this by adding .onBackpressureBuffer(1), but if this turns
out to be a better way of doing this, we should submit an issue requesting that
OperatorReplay use its buffer size for backpressure. */
mPassiveRxBlessings.subscribe(b -> mLastBlessings = b);
mRxBlessings = Observable.defer(this::refreshBlessings)
.ignoreElements()
.concatWith(mPassiveRxBlessings);
}
@Synchronized("mSeekLock")
public Observable<Blessings> refreshBlessings() {
if (mCurrentSeek != null) {
return mCurrentSeek;
}
Blessings mgrBlessings;
try {
mgrBlessings = BlessingsManager.getBlessings(mActivity.getApplicationContext());
} catch (final VException e) {
log.warn("Could not get blessings from shared preferences", e);
mgrBlessings = null;
}
final Observable<Blessings> nextBlessings;
if (mgrBlessings == null) {
mCurrentSeek = nextBlessings = seekBlessings()
.first() // Longer-lived providers should consider a more direct implementation
// of BlessingsProvider.
.onErrorResumeNext(this::handleBlessingsError)
.doOnNext(this::afterSeekBlessings)
.replay(1).autoConnect();
} else {
nextBlessings = Observable.just(mgrBlessings);
}
mPub.onNext(nextBlessings);
return nextBlessings;
}
protected Observable<Blessings> seekBlessings() {
final BlessingRequestFragment brf = new BlessingRequestFragment();
mActivity.getFragmentManager().beginTransaction()
.add(brf, null)
.commit();
return brf.getObservable();
}
protected Observable<Blessings> handleBlessingsError(final Throwable t) {
if (t instanceof BlessingCreationException) {
/* This exception can occur if a user hits "Deny" in Blessings Manager, so don't treat
it as an error if we have a fallback. */
if (mLastBlessings == null) {
mErrorReporter.onError(R.string.err_blessings_required, t);
} else {
log.warn("Could not create blessings", t);
}
} else if (t instanceof VException) {
mErrorReporter.onError(R.string.err_blessings_decode, t);
} else {
return Observable.error(t);
}
if (mLastBlessings == null) {
mActivity.finish();
/* Block while the app exits, as opposed to returning an error that would be reported
(redundantly) elsewhere. */
return Observable.never();
} else {
return Observable.just(mLastBlessings);
}
}
private void afterSeekBlessings(final Blessings b) {
try {
BlessingsManager.addBlessings(mActivity.getApplicationContext(), b);
} catch (final VException e) {
mErrorReporter.onError(R.string.err_blessings_store, e);
} finally {
synchronized (mSeekLock) {
mCurrentSeek = null;
}
}
}
}