/* * Copyright (c) 2015 OpenSilk Productions LLC * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package org.opensilk.common.ui.mortar; import android.annotation.SuppressLint; import android.content.Context; import android.view.View; import com.jakewharton.rxbinding.view.RxView; import com.jakewharton.rxbinding.view.ViewAttachEvent; import mortar.MortarScope; import rx.Observable; import rx.exceptions.Exceptions; import rx.functions.Func1; import rx.functions.Func2; import static java.lang.String.format; /** * Simplified version of trellos RxLifecycle (https://github.com/trello/RxLifecycle) * Allows presenters to bind observables to parent lifecycle without knowing who * controls the lifecycle * * Created by drew on 10/12/15. */ public class LifecycleService { public static final String LIFECYCLE_SERVICE = LifecycleService.class.getName(); /** * @throws IllegalArgumentException if there is no LifecycleService attached to this scope * @return The Lifecycle Observable associated with this context */ @SuppressWarnings("unchecked") @SuppressLint("WrongConstant") public static Observable<Lifecycle> getLifecycle(Context context) { //noinspection ResourceType return (Observable<Lifecycle>) context.getSystemService(LIFECYCLE_SERVICE); } /** * @throws IllegalArgumentException if there is no LifecycleService attached to this scope * @return The Lifecycle Observable associated with this scope */ @SuppressWarnings("unchecked") // public static Observable<Lifecycle> getLifecycle(MortarScope scope) { if (scope.hasService(LIFECYCLE_SERVICE)) { return (Observable<Lifecycle>) scope.getService(LIFECYCLE_SERVICE); } throw new IllegalArgumentException(format("No lifecycle service found in scope %s", scope.getName())); } /** * Binds the given source to a Lifecycle. * <p> * When the lifecycle event occurs, the source will cease to emit any notifications. * <p> * Use with {@link Observable#compose(Observable.Transformer)}: * {@code source.compose(RxLifecycle.bindUntilEvent(lifecycle, FragmentEvent.STOP)).subscribe()} * * @param lifecycle the Fragment lifecycle sequence * @param event the event which should conclude notifications from the source * @return a reusable {@link Observable.Transformer} that unsubscribes the source at the specified event */ public static <T> Observable.Transformer<? super T, ? extends T> bindUntilLifecycleEvent( final Observable<Lifecycle> lifecycle, final Lifecycle event) { return bindUntilEvent(lifecycle, event); } public static <T> Observable.Transformer<? super T, ? extends T> bindUntilLifecycleEvent( final MortarScope scope, final Lifecycle event) { return bindUntilEvent(getLifecycle(scope), event); } public static <T> Observable.Transformer<? super T, ? extends T> bindUntilLifecycleEvent( final Context context, final Lifecycle event) { return bindUntilEvent(getLifecycle(context), event); } private static <T, R> Observable.Transformer<? super T, ? extends T> bindUntilEvent( final Observable<R> lifecycle, final R event) { if (lifecycle == null) { throw new IllegalArgumentException("Lifecycle must be given"); } else if (event == null) { throw new IllegalArgumentException("Event must be given"); } return new Observable.Transformer<T, T>() { @Override public Observable<T> call(Observable<T> source) { return source.takeUntil( lifecycle.takeFirst(new Func1<R, Boolean>() { @Override public Boolean call(R lifecycleEvent) { return lifecycleEvent == event; } }) ); } }; } /** * Binds the given source to a Lifecycle. * <p> * Use with {@link Observable#compose(Observable.Transformer)}: * {@code source.compose(RxLifecycle.bindActivity(lifecycle)).subscribe()} * <p> * This helper automatically determines (based on the lifecycle sequence itself) when the source * should stop emitting items. In the case that the lifecycle sequence is in the * creation phase (START, RESUME) it will choose the equivalent destructive phase (PAUSE, * STOP). If used in the destructive phase, the notifications will cease at the next event; * for example, if used in PAUSE, it will unsubscribe in STOP. * * @param lifecycle the lifecycle sequence of an Activity * * @return a reusable {@link Observable.Transformer} that unsubscribes the source during the Activity lifecycle */ public static <T> Observable.Transformer<? super T, ? extends T> bindLifecycle(Observable<Lifecycle> lifecycle) { return bind(lifecycle, LIFECYCLE); } public static <T> Observable.Transformer<? super T, ? extends T> bindLifecycle(MortarScope scope) { return bind(getLifecycle(scope), LIFECYCLE); } public static <T> Observable.Transformer<? super T, ? extends T> bindLifecycle(Context context) { return bind(getLifecycle(context), LIFECYCLE); } private static <T, R> Observable.Transformer<? super T, ? extends T> bind(Observable<R> lifecycle, final Func1<R, R> correspondingEvents) { if (lifecycle == null) { throw new IllegalArgumentException("Lifecycle must be given"); } // Make sure we're truly comparing a single stream to itself final Observable<R> sharedLifecycle = lifecycle.share(); // Keep emitting from source until the corresponding event occurs in the lifecycle return new Observable.Transformer<T, T>() { @Override public Observable<T> call(Observable<T> source) { return source.takeUntil( Observable.combineLatest( sharedLifecycle.take(1).map(correspondingEvents), sharedLifecycle.skip(1), new Func2<R, R, Boolean>() { @Override public Boolean call(R bindUntilEvent, R lifecycleEvent) { return lifecycleEvent == bindUntilEvent; } }) .onErrorReturn(RESUME_FUNCTION) .takeFirst(SHOULD_COMPLETE) ); } }; } private static final Func1<Throwable, Boolean> RESUME_FUNCTION = new Func1<Throwable, Boolean>() { @Override public Boolean call(Throwable throwable) { if (throwable instanceof OutsideLifecycleException) { return true; } Exceptions.propagate(throwable); return false; } }; private static final Func1<Boolean, Boolean> SHOULD_COMPLETE = new Func1<Boolean, Boolean>() { @Override public Boolean call(Boolean shouldComplete) { return shouldComplete; } }; // Figures out which corresponding next lifecycle event in which to unsubscribe private static final Func1<Lifecycle, Lifecycle> LIFECYCLE = new Func1<Lifecycle, Lifecycle>() { @Override public Lifecycle call(Lifecycle lastEvent) { switch (lastEvent) { case START: return Lifecycle.STOP; case RESUME: return Lifecycle.PAUSE; case PAUSE: return Lifecycle.STOP; case STOP: throw new OutsideLifecycleException("Cannot bind to Activity lifecycle when outside of it."); default: throw new UnsupportedOperationException("Binding to " + lastEvent + " not yet implemented"); } } }; private static class OutsideLifecycleException extends IllegalStateException { public OutsideLifecycleException(String detailMessage) { super(detailMessage); } } }