package io.nextop.rx; import android.view.View; import com.google.common.base.Objects; import immutablecollections.ImSet; import rx.*; import rx.Observable; import rx.Observer; import rx.android.schedulers.AndroidSchedulers; import rx.functions.Action0; import rx.functions.Action2; import rx.observers.Subscribers; import rx.subscriptions.BooleanSubscription; import rx.subscriptions.CompositeSubscription; import javax.annotation.Nullable; public interface RxLifecycleBinder extends Subscription { void reset(); /** this version compares the current id to the given id. * This is useful for recycler views where tearing down and * setting up the same id is wasteful/visually jarring. * if different, does a reset and returns true. * if the same, does nothing and returns false. */ boolean reset(Object id); /** Wraps the given observable in an observable bound to the lifecyle of a container. * The wrapper allocates on subscribe and cleans up on unsubscribe. * Unsubscription of all can be forced with {@link #reset}. * * This ensures: * - the subscription is connected to the source when started * - the subscription is dropped from the source when stopped * (onComplete is not called, because the subscription will resume when restarted) */ <T> Observable<T> bind(Observable<T> source); void bind(Subscription sub); /** Binds to an internal lifecycle start/stop. */ final class Lifted implements RxLifecycleBinder { private final Scheduler scheduler; @Nullable private Object currentId = null; private ImSet<Bind<?>> binds = ImSet.empty(); private final CompositeSubscription subscriptions = new CompositeSubscription(); private boolean connected = false; @Nullable private View connectedView = null; private boolean closed = false; @Nullable private Subscription cascadeSubscription = null; @Nullable private RxDebugger debugger; public Lifted() { this(RxDebugger.get()); } public Lifted(@Nullable RxDebugger debugger) { scheduler = AndroidSchedulers.mainThread(); this.debugger = debugger; } public void setDebugger(@Nullable RxDebugger debugger) { if (null != this.debugger) { // FIXME // FIXME detach } this.debugger = debugger; if (null != debugger) { // FIXME // FIXME attach } } public boolean isClosed() { return closed; } public void connect() { connect(null); } public void connect(@Nullable View view) { if (closed) { throw new IllegalStateException(); } if (!connected) { connected = true; connectedView = view; for (Bind<?> bind : binds) { bind.connect(); } } } public void disconnect() { if (closed) { throw new IllegalStateException(); } if (connected) { connected = false; connectedView = null; for (Bind<?> bind : binds) { bind.disconnect(); } } } public void close() { if (closed) { throw new IllegalStateException(); } closed = true; removeCascadeUnsubscribe(); clear(); subscriptions.unsubscribe(); } public void removeCascadeUnsubscribe() { if (null != cascadeSubscription) { cascadeSubscription.unsubscribe(); cascadeSubscription = null; } } // replaces previous cascade parent public void cascadeUnsubscribe(@Nullable RxLifecycleBinder parent) { removeCascadeUnsubscribe(); if (null != parent) { cascadeSubscription = parent.bind(MoreObservables.hanging()).doOnCompleted(new Action0() { @Override public void call() { unsubscribe(); } }).subscribe(); } } private void clear() { ImSet<Bind<?>> _binds = binds; binds = ImSet.empty(); for (Bind bind : _binds) { bind.close(); } subscriptions.clear(); } /////// Subscription /////// @Override public void unsubscribe() { close(); } @Override public boolean isUnsubscribed() { return closed; } /////// RxLifecycleBinder /////// @Override public void reset() { if (closed) { throw new IllegalStateException(); } currentId = null; clear(); } @Override public boolean reset(Object id) { if (closed) { throw new IllegalStateException(); } if (!Objects.equal(currentId, id)) { currentId = id; clear(); return true; } else { return false; } } @Override public <T> Observable<T> bind(Observable<T> source) { if (closed) { return Observable.empty(); } Bind<T> bind = new Bind<T>(source.subscribeOn(scheduler)); binds = binds.adding(bind); if (connected) { bind.connect(); } return bind.adapter; } @Override public void bind(Subscription sub) { if (closed) { sub.unsubscribe(); } else { subscriptions.add(sub); } } private final class Bind<T> { private final Observable<T> source; final Observable<T> adapter; private boolean closed = false; private boolean connected; private ImSet<Bridge> subscribers = ImSet.empty(); Bind(Observable<T> source) { this.source = source; adapter = Observable.create(new Observable.OnSubscribe<T>() { @Override public void call(Subscriber<? super T> subscriber) { add(subscriber); } }); } final class Bridge { final Subscriber<? super T> subscriber; private @Nullable Subscription subscription = null; // DEBUG STATISTICS private int onNextCount = 0; private int onCompletedCount = 0; private int onErrorCount = 0; @Nullable private Notification mostRecentNotification = null; private int failedNotificationCount = 0; @Nullable private Notification mostRecentFailedNotification = null; @Nullable private Throwable mostRecentFailedNotificationReason = null; Bridge(Subscriber<? super T> subscriber) { this.subscriber = subscriber; } void subscribe(Subscription subscription) { unsubscribe(); if (subscriber.isUnsubscribed()) { subscription.unsubscribe(); } else { this.subscription = subscription; if (null != debugger && debugger.isEnabled()) { debugger.update(new RxDebugger.Stats(RxDebugger.Stats.F_CONNECTED, subscriber, connectedView, false, true, onNextCount, onCompletedCount, onErrorCount, mostRecentNotification, failedNotificationCount, mostRecentFailedNotification, mostRecentFailedNotificationReason)); } } } void unsubscribe() { if (null != subscription) { subscription.unsubscribe(); subscription = null; if (null != debugger && debugger.isEnabled()) { debugger.update(new RxDebugger.Stats(RxDebugger.Stats.F_DISCONNECTED, subscriber, connectedView, false, false, onNextCount, onCompletedCount, onErrorCount, mostRecentNotification, failedNotificationCount, mostRecentFailedNotification, mostRecentFailedNotificationReason)); } } } void close() { unsubscribe(); if (!subscriber.isUnsubscribed()) { subscriber.onCompleted(); subscriber.unsubscribe(); if (null != debugger && debugger.isEnabled()) { onCompletedCount += 1; debugger.update(new RxDebugger.Stats(RxDebugger.Stats.F_COMPLETED, subscriber, connectedView, true, false, onNextCount, onCompletedCount, onErrorCount, mostRecentNotification, failedNotificationCount, mostRecentFailedNotification, mostRecentFailedNotificationReason)); } } } public Subscriber inSubscriber() { // if in debug, the notification is routed through the debugger // which may step/filter notifications final Action2<Subscriber, Notification> debugDelivery = new Action2<Subscriber, Notification>() { @Override public void call(Subscriber subscriber, Notification notification) { if (!subscriber.isUnsubscribed()) { mostRecentNotification = notification; int flags = 0; try { notification.accept(subscriber); switch (notification.getKind()) { case OnNext: onNextCount += 1; flags |= RxDebugger.Stats.F_NEXT; break; case OnError: onErrorCount += 1; flags |= RxDebugger.Stats.F_ERROR; break; case OnCompleted: onCompletedCount += 1; flags |= RxDebugger.Stats.F_COMPLETED; break; default: throw new IllegalArgumentException(); } } catch (Throwable t) { failedNotificationCount += 1; mostRecentFailedNotification = notification; mostRecentFailedNotificationReason = t; flags |= RxDebugger.Stats.F_FAILED; } if (null != debugger && debugger.isEnabled()) { debugger.update(new RxDebugger.Stats(flags, subscriber, connectedView, false, !subscription.isUnsubscribed(), onNextCount, onCompletedCount, onErrorCount, mostRecentNotification, failedNotificationCount, mostRecentFailedNotification, mostRecentFailedNotificationReason)); } } } }; return Subscribers.from(new Observer<T>() { @Override public void onNext(T t) { if (null != debugger && debugger.isEnabled()) { debugger.deliver(subscriber, Notification.createOnNext(t), debugDelivery); } else if (!subscriber.isUnsubscribed()) { subscriber.onNext(t); } } @Override public void onCompleted() { if (null != debugger && debugger.isEnabled()) { debugger.deliver(subscriber, Notification.createOnCompleted(), debugDelivery); } else if (!subscriber.isUnsubscribed()) { subscriber.onCompleted(); } } @Override public void onError(Throwable e) { if (null != debugger && debugger.isEnabled()) { debugger.deliver(subscriber, Notification.createOnError(e), debugDelivery); } else if (!subscriber.isUnsubscribed()) { subscriber.onError(e); } } }); } public Subscription outSubscription() { return BooleanSubscription.create(new Action0() { @Override public void call() { unsubscribe(); subscribers = subscribers.removing(Bridge.this); } }); } }; private void add(final Subscriber<? super T> subscriber) { Bridge bridge = new Bridge(subscriber); subscribers = subscribers.adding(bridge); subscriber.add(bridge.outSubscription()); connect(bridge); } void connect() { if (closed) { throw new IllegalStateException(); } if (!connected) { connected = true; for (Bridge bridge : subscribers) { connect(bridge); } } } void connect(Bridge bridge) { if (connected && !bridge.subscriber.isUnsubscribed()) { bridge.subscribe(source.subscribe(bridge.inSubscriber())); } } void disconnect() { if (closed) { throw new IllegalStateException(); } if (connected) { connected = false; for (Bridge bridge : subscribers) { bridge.unsubscribe(); } } } // implies disconnect void close() { if (closed) { throw new IllegalStateException(); } disconnect(); if (!closed) { closed = true; for (Bridge bridge : subscribers) { bridge.close(); } } } } } }