package com.kickstarter.libs; import android.content.Intent; import android.os.Bundle; import android.support.annotation.AnimRes; import android.support.annotation.CallSuper; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.v7.app.AppCompatActivity; import android.util.Pair; import com.kickstarter.ApplicationComponent; import com.kickstarter.KSApplication; import com.kickstarter.libs.qualifiers.RequiresActivityViewModel; import com.kickstarter.libs.utils.BundleUtils; import com.kickstarter.ui.data.ActivityResult; import com.trello.rxlifecycle.ActivityEvent; import com.trello.rxlifecycle.RxLifecycle; import com.trello.rxlifecycle.components.ActivityLifecycleProvider; import rx.Observable; import rx.Subscription; import rx.android.schedulers.AndroidSchedulers; import rx.subjects.BehaviorSubject; import rx.subjects.PublishSubject; import rx.subscriptions.CompositeSubscription; import timber.log.Timber; public abstract class BaseActivity<ViewModelType extends ActivityViewModel> extends AppCompatActivity implements ActivityLifecycleProvider, ActivityLifecycleType { private final PublishSubject<Void> back = PublishSubject.create(); private final BehaviorSubject<ActivityEvent> lifecycle = BehaviorSubject.create(); private static final String VIEW_MODEL_KEY = "viewModel"; private final CompositeSubscription subscriptions = new CompositeSubscription(); protected ViewModelType viewModel; /** * Get viewModel. */ public ViewModelType viewModel() { return viewModel; } /** * Returns an observable of the activity's lifecycle events. */ public final Observable<ActivityEvent> lifecycle() { return lifecycle.asObservable(); } /** * Completes an observable when an {@link ActivityEvent} occurs in the activity's lifecycle. */ public final <T> Observable.Transformer<T, T> bindUntilEvent(final ActivityEvent event) { return RxLifecycle.bindUntilActivityEvent(lifecycle, event); } /** * Completes an observable when the lifecycle event opposing the current lifecyle event is emitted. * For example, if a subscription is made during {@link ActivityEvent#CREATE}, the observable will be completed * in {@link ActivityEvent#DESTROY}. */ public final <T> Observable.Transformer<T, T> bindToLifecycle() { return RxLifecycle.bindActivity(lifecycle); } /** * Sends activity result data to the view model. */ @CallSuper @Override protected void onActivityResult(final int requestCode, final int resultCode, final @Nullable Intent intent) { super.onActivityResult(requestCode, resultCode, intent); viewModel.activityResult(ActivityResult.create(requestCode, resultCode, intent)); } @CallSuper @Override protected void onCreate(final @Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); Timber.d("onCreate %s", this.toString()); lifecycle.onNext(ActivityEvent.CREATE); assignViewModel(savedInstanceState); viewModel.intent(getIntent()); } /** * Called when an activity is set to `singleTop` and it is relaunched while at the top of the activity stack. */ @CallSuper @Override protected void onNewIntent(final Intent intent) { super.onNewIntent(intent); viewModel.intent(intent); } @CallSuper @Override protected void onStart() { super.onStart(); Timber.d("onStart %s", this.toString()); lifecycle.onNext(ActivityEvent.START); back .compose(bindUntilEvent(ActivityEvent.STOP)) .observeOn(AndroidSchedulers.mainThread()) .subscribe(__ -> goBack()); } @CallSuper @Override protected void onResume() { super.onResume(); Timber.d("onResume %s", this.toString()); lifecycle.onNext(ActivityEvent.RESUME); assignViewModel(null); if (viewModel != null) { viewModel.onResume(this); } } @CallSuper @Override protected void onPause() { lifecycle.onNext(ActivityEvent.PAUSE); super.onPause(); Timber.d("onPause %s", this.toString()); if (viewModel != null) { viewModel.onPause(); } } @CallSuper @Override protected void onStop() { lifecycle.onNext(ActivityEvent.STOP); super.onStop(); Timber.d("onStop %s", this.toString()); } @CallSuper @Override protected void onDestroy() { lifecycle.onNext(ActivityEvent.DESTROY); super.onDestroy(); Timber.d("onDestroy %s", this.toString()); subscriptions.clear(); if (isFinishing()) { if (viewModel != null) { ActivityViewModelManager.getInstance().destroy(viewModel); viewModel = null; } } } /** * @deprecated Use {@link #back()} instead. * * In rare situations, onBackPressed can be triggered after {@link #onSaveInstanceState(Bundle)} has been called. * This causes an {@link IllegalStateException} in the fragment manager's `checkStateLoss` method, because the * UI state has changed after being saved. The sequence of events might look like this: * * onSaveInstanceState -> onStop -> onBackPressed * * To avoid that situation, we need to ignore calls to `onBackPressed` after the activity has been saved. Since * the activity is stopped after `onSaveInstanceState` is called, we can create an observable of back events, * and a subscription that calls super.onBackPressed() only when the activity has not been stopped. */ @CallSuper @Override @Deprecated public void onBackPressed() { back(); } /** * Call when the user wants triggers a back event, e.g. clicking back in a toolbar or pressing the device back button. */ public void back() { back.onNext(null); } /** * Override in subclasses for custom exit transitions. First item in pair is the enter animation, * second item in pair is the exit animation. */ protected @Nullable Pair<Integer, Integer> exitTransition() { return null; } @CallSuper @Override protected void onSaveInstanceState(final @NonNull Bundle outState) { super.onSaveInstanceState(outState); Timber.d("onSaveInstanceState %s", this.toString()); final Bundle viewModelEnvelope = new Bundle(); if (viewModel != null) { ActivityViewModelManager.getInstance().save(viewModel, viewModelEnvelope); } outState.putBundle(VIEW_MODEL_KEY, viewModelEnvelope); } protected final void startActivityWithTransition(final @NonNull Intent intent, final @AnimRes int enterAnim, final @AnimRes int exitAnim) { startActivity(intent); overridePendingTransition(enterAnim, exitAnim); } /** * Returns the {@link KSApplication} instance. */ protected @NonNull KSApplication application() { return (KSApplication) getApplication(); } /** * Convenience method to return a Dagger component. */ protected @NonNull ApplicationComponent component() { return application().component(); } /** * Returns the application's {@link Environment}. */ protected @NonNull Environment environment() { return component().environment(); } /** * @deprecated Use {@link #bindToLifecycle()} or {@link #bindUntilEvent(ActivityEvent)} instead. */ @Deprecated protected final void addSubscription(final @NonNull Subscription subscription) { subscriptions.add(subscription); } /** * Triggers a back press with an optional transition. */ private void goBack() { super.onBackPressed(); final Pair<Integer, Integer> exitTransitions = exitTransition(); if (exitTransitions != null) { overridePendingTransition(exitTransitions.first, exitTransitions.second); } } private void assignViewModel(final @Nullable Bundle viewModelEnvelope) { if (viewModel == null) { final RequiresActivityViewModel annotation = getClass().getAnnotation(RequiresActivityViewModel.class); final Class<ViewModelType> viewModelClass = annotation == null ? null : (Class<ViewModelType>) annotation.value(); if (viewModelClass != null) { viewModel = ActivityViewModelManager.getInstance().fetch(this, viewModelClass, BundleUtils.maybeGetBundle(viewModelEnvelope, VIEW_MODEL_KEY)); } } } }