package com.bluelinelabs.conductor; import android.app.Activity; import android.content.Intent; import android.content.IntentSender; import android.os.Bundle; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.annotation.UiThread; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; import com.bluelinelabs.conductor.Controller.LifecycleListener; import com.bluelinelabs.conductor.ControllerChangeHandler.ControllerChangeListener; import com.bluelinelabs.conductor.changehandler.SimpleSwapChangeHandler; import com.bluelinelabs.conductor.internal.NoOpControllerChangeHandler; import com.bluelinelabs.conductor.internal.ThreadUtils; import com.bluelinelabs.conductor.internal.TransactionIndexer; import java.util.ArrayList; import java.util.Collections; import java.util.Iterator; import java.util.List; /** * A Router implements navigation and backstack handling for {@link Controller}s. Router objects are attached * to Activity/containing ViewGroup pairs. Routers do not directly render or push Views to the container ViewGroup, * but instead defer this responsibility to the {@link ControllerChangeHandler} specified in a given transaction. */ public abstract class Router { private static final String KEY_BACKSTACK = "Router.backstack"; private static final String KEY_POPS_LAST_VIEW = "Router.popsLastView"; protected final Backstack backstack = new Backstack(); private final List<ControllerChangeListener> changeListeners = new ArrayList<>(); final List<Controller> destroyingControllers = new ArrayList<>(); private boolean popsLastView = false; ViewGroup container; /** * Returns this Router's host Activity or {@code null} if it has either not yet been attached to * an Activity or if the Activity has been destroyed. */ @Nullable public abstract Activity getActivity(); /** * This should be called by the host Activity when its onActivityResult method is called if the instanceId * of the controller that called startActivityForResult is not known. * * @param requestCode The Activity's onActivityResult requestCode * @param resultCode The Activity's onActivityResult resultCode * @param data The Activity's onActivityResult data */ public abstract void onActivityResult(int requestCode, int resultCode, @Nullable Intent data); /** * This should be called by the host Activity when its onRequestPermissionsResult method is called. The call will be forwarded * to the {@link Controller} with the instanceId passed in. * * @param instanceId The instanceId of the Controller to which this result should be forwarded * @param requestCode The Activity's onRequestPermissionsResult requestCode * @param permissions The Activity's onRequestPermissionsResult permissions * @param grantResults The Activity's onRequestPermissionsResult grantResults */ public void onRequestPermissionsResult(@NonNull String instanceId, int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { Controller controller = getControllerWithInstanceId(instanceId); if (controller != null) { controller.requestPermissionsResult(requestCode, permissions, grantResults); } } /** * This should be called by the host Activity when its onBackPressed method is called. The call will be forwarded * to its top {@link Controller}. If that controller doesn't handle it, then it will be popped. * * @return Whether or not a back action was handled by the Router */ @UiThread public boolean handleBack() { ThreadUtils.ensureMainThread(); if (!backstack.isEmpty()) { //noinspection ConstantConditions if (backstack.peek().controller.handleBack()) { return true; } else if (popCurrentController()) { return true; } } return false; } /** * Pops the top {@link Controller} from the backstack * * @return Whether or not this Router still has controllers remaining on it after popping. */ @SuppressWarnings("WeakerAccess") @UiThread public boolean popCurrentController() { ThreadUtils.ensureMainThread(); RouterTransaction transaction = backstack.peek(); if (transaction == null) { throw new IllegalStateException("Trying to pop the current controller when there are none on the backstack."); } return popController(transaction.controller); } /** * Pops the passed {@link Controller} from the backstack * * @param controller The controller that should be popped from this Router * @return Whether or not this Router still has controllers remaining on it after popping. */ @UiThread public boolean popController(@NonNull Controller controller) { ThreadUtils.ensureMainThread(); RouterTransaction topController = backstack.peek(); boolean poppingTopController = topController != null && topController.controller == controller; if (poppingTopController) { trackDestroyingController(backstack.pop()); } else { for (RouterTransaction transaction : backstack) { if (transaction.controller == controller) { backstack.remove(transaction); break; } } } if (poppingTopController) { performControllerChange(backstack.peek(), topController, false); } if (popsLastView) { return topController != null; } else { return !backstack.isEmpty(); } } /** * Pushes a new {@link Controller} to the backstack * * @param transaction The transaction detailing what should be pushed, including the {@link Controller}, * and its push and pop {@link ControllerChangeHandler}, and its tag. */ @UiThread public void pushController(@NonNull RouterTransaction transaction) { ThreadUtils.ensureMainThread(); RouterTransaction from = backstack.peek(); pushToBackstack(transaction); performControllerChange(transaction, from, true); } /** * Replaces this Router's top {@link Controller} with a new {@link Controller} * * @param transaction The transaction detailing what should be pushed, including the {@link Controller}, * and its push and pop {@link ControllerChangeHandler}, and its tag. */ @SuppressWarnings("WeakerAccess") @UiThread public void replaceTopController(@NonNull RouterTransaction transaction) { ThreadUtils.ensureMainThread(); RouterTransaction topTransaction = backstack.peek(); if (!backstack.isEmpty()) { trackDestroyingController(backstack.pop()); } final ControllerChangeHandler handler = transaction.pushChangeHandler(); if (topTransaction != null) { //noinspection ConstantConditions final boolean oldHandlerRemovedViews = topTransaction.pushChangeHandler() == null || topTransaction.pushChangeHandler().removesFromViewOnPush(); final boolean newHandlerRemovesViews = handler == null || handler.removesFromViewOnPush(); if (!oldHandlerRemovedViews && newHandlerRemovesViews) { for (RouterTransaction visibleTransaction : getVisibleTransactions(backstack.iterator())) { performControllerChange(null, visibleTransaction, true, handler); } } } pushToBackstack(transaction); if (handler != null) { handler.setForceRemoveViewOnPush(true); } performControllerChange(transaction.pushChangeHandler(handler), topTransaction, true); } void destroy(boolean popViews) { popsLastView = true; final List<RouterTransaction> poppedControllers = backstack.popAll(); trackDestroyingControllers(poppedControllers); if (popViews && poppedControllers.size() > 0) { RouterTransaction topTransaction = poppedControllers.get(0); topTransaction.controller().addLifecycleListener(new LifecycleListener() { @Override public void onChangeEnd(@NonNull Controller controller, @NonNull ControllerChangeHandler changeHandler, @NonNull ControllerChangeType changeType) { if (changeType == ControllerChangeType.POP_EXIT) { for (int i = poppedControllers.size() - 1; i > 0; i--) { RouterTransaction transaction = poppedControllers.get(i); performControllerChange(null, transaction, true, new SimpleSwapChangeHandler()); } } } }); performControllerChange(null, topTransaction, false, topTransaction.popChangeHandler()); } } public int getContainerId() { return container != null ? container.getId() : 0; } /** * If set to true, this router will handle back presses by performing a change handler on the last controller and view * in the stack. This defaults to false so that the developer can either finish its containing Activity or otherwise * hide its parent view without any strange artifacting. */ @NonNull public Router setPopsLastView(boolean popsLastView) { this.popsLastView = popsLastView; return this; } /** * Pops all {@link Controller}s until only the root is left * * @return Whether or not any {@link Controller}s were popped in order to get to the root transaction */ @UiThread public boolean popToRoot() { ThreadUtils.ensureMainThread(); return popToRoot(null); } /** * Pops all {@link Controller} until only the root is left * * @param changeHandler The {@link ControllerChangeHandler} to handle this transaction * @return Whether or not any {@link Controller}s were popped in order to get to the root transaction */ @SuppressWarnings("WeakerAccess") @UiThread public boolean popToRoot(@Nullable ControllerChangeHandler changeHandler) { ThreadUtils.ensureMainThread(); if (backstack.size() > 1) { //noinspection ConstantConditions popToTransaction(backstack.root(), changeHandler); return true; } else { return false; } } /** * Pops all {@link Controller}s until the Controller with the passed tag is at the top * * @param tag The tag being popped to * @return Whether or not any {@link Controller}s were popped in order to get to the transaction with the passed tag */ @UiThread public boolean popToTag(@NonNull String tag) { ThreadUtils.ensureMainThread(); return popToTag(tag, null); } /** * Pops all {@link Controller}s until the {@link Controller} with the passed tag is at the top * * @param tag The tag being popped to * @param changeHandler The {@link ControllerChangeHandler} to handle this transaction * @return Whether or not the {@link Controller} with the passed tag is now at the top */ @SuppressWarnings("WeakerAccess") @UiThread public boolean popToTag(@NonNull String tag, @Nullable ControllerChangeHandler changeHandler) { ThreadUtils.ensureMainThread(); for (RouterTransaction transaction : backstack) { if (tag.equals(transaction.tag())) { popToTransaction(transaction, changeHandler); return true; } } return false; } /** * Sets the root Controller. If any {@link Controller}s are currently in the backstack, they will be removed. * * @param transaction The transaction detailing what should be pushed, including the {@link Controller}, * and its push and pop {@link ControllerChangeHandler}, and its tag. */ @UiThread public void setRoot(@NonNull RouterTransaction transaction) { ThreadUtils.ensureMainThread(); List<RouterTransaction> transactions = Collections.singletonList(transaction); setBackstack(transactions, transaction.pushChangeHandler()); } /** * Returns the hosted Controller with the given instance id or {@code null} if no such * Controller exists in this Router. * * @param instanceId The instance ID being searched for */ @Nullable public Controller getControllerWithInstanceId(@NonNull String instanceId) { for (RouterTransaction transaction : backstack) { Controller controllerWithId = transaction.controller.findController(instanceId); if (controllerWithId != null) { return controllerWithId; } } return null; } /** * Returns the hosted Controller that was pushed with the given tag or {@code null} if no * such Controller exists in this Router. * * @param tag The tag being searched for */ @Nullable public Controller getControllerWithTag(@NonNull String tag) { for (RouterTransaction transaction : backstack) { if (tag.equals(transaction.tag())) { return transaction.controller; } } return null; } /** * Returns the number of {@link Controller}s currently in the backstack */ @SuppressWarnings("WeakerAccess") public int getBackstackSize() { return backstack.size(); } /** * Returns the current backstack, ordered from root to most recently pushed. */ @NonNull public List<RouterTransaction> getBackstack() { List<RouterTransaction> list = new ArrayList<>(); Iterator<RouterTransaction> backstackIterator = backstack.reverseIterator(); while (backstackIterator.hasNext()) { list.add(backstackIterator.next()); } return list; } /** * Sets the backstack, transitioning from the current top controller to the top of the new stack (if different) * using the passed {@link ControllerChangeHandler} * * @param newBackstack The new backstack * @param changeHandler An optional change handler to be used to handle the root view of transition */ @SuppressWarnings("WeakerAccess") @UiThread public void setBackstack(@NonNull List<RouterTransaction> newBackstack, @Nullable ControllerChangeHandler changeHandler) { ThreadUtils.ensureMainThread(); List<RouterTransaction> oldVisibleTransactions = getVisibleTransactions(backstack.iterator()); boolean newRootRequiresPush = !(newBackstack.size() > 0 && backstack.contains(newBackstack.get(0))); removeAllExceptVisibleAndUnowned(); ensureOrderedTransactionIndices(newBackstack); backstack.setBackstack(newBackstack); for (RouterTransaction transaction : backstack) { transaction.onAttachedToRouter(); } if (newBackstack.size() > 0) { List<RouterTransaction> reverseNewBackstack = new ArrayList<>(newBackstack); Collections.reverse(reverseNewBackstack); List<RouterTransaction> newVisibleTransactions = getVisibleTransactions(reverseNewBackstack.iterator()); boolean visibleTransactionsChanged = !backstacksAreEqual(newVisibleTransactions, oldVisibleTransactions); if (visibleTransactionsChanged) { RouterTransaction rootTransaction = oldVisibleTransactions.size() > 0 ? oldVisibleTransactions.get(0) : null; // Replace the old root with the new one if (rootTransaction == null || rootTransaction.controller != newVisibleTransactions.get(0).controller) { performControllerChange(newVisibleTransactions.get(0), rootTransaction, newRootRequiresPush, changeHandler); } // Remove all visible controllers that were previously on the backstack for (int i = oldVisibleTransactions.size() - 1; i > 0; i--) { RouterTransaction transaction = oldVisibleTransactions.get(i); if (!newVisibleTransactions.contains(transaction)) { ControllerChangeHandler localHandler = changeHandler != null ? changeHandler.copy() : new SimpleSwapChangeHandler(); localHandler.setForceRemoveViewOnPush(true); performControllerChange(null, transaction, newRootRequiresPush, localHandler); } } // Add any new controllers to the backstack for (int i = 1; i < newVisibleTransactions.size(); i++) { RouterTransaction transaction = newVisibleTransactions.get(i); if (!oldVisibleTransactions.contains(transaction)) { performControllerChange(transaction, newVisibleTransactions.get(i - 1), true, transaction.pushChangeHandler()); } } } // Ensure all new controllers have a valid router set for (RouterTransaction transaction : newBackstack) { transaction.controller.setRouter(this); } } } /** * Returns whether or not this Router has a root {@link Controller} */ public boolean hasRootController() { return getBackstackSize() > 0; } /** * Adds a listener for all of this Router's {@link Controller} change events * * @param changeListener The listener */ @SuppressWarnings("WeakerAccess") public void addChangeListener(@NonNull ControllerChangeListener changeListener) { if (!changeListeners.contains(changeListener)) { changeListeners.add(changeListener); } } /** * Removes a previously added listener * * @param changeListener The listener to be removed */ @SuppressWarnings("WeakerAccess") public void removeChangeListener(@NonNull ControllerChangeListener changeListener) { changeListeners.remove(changeListener); } /** * Attaches this Router's existing backstack to its container if one exists. */ @UiThread public void rebindIfNeeded() { ThreadUtils.ensureMainThread(); Iterator<RouterTransaction> backstackIterator = backstack.reverseIterator(); while (backstackIterator.hasNext()) { RouterTransaction transaction = backstackIterator.next(); if (transaction.controller.getNeedsAttach()) { performControllerChange(transaction, null, true, new SimpleSwapChangeHandler(false)); } } } public final void onActivityResult(@NonNull String instanceId, int requestCode, int resultCode, @Nullable Intent data) { Controller controller = getControllerWithInstanceId(instanceId); if (controller != null) { controller.onActivityResult(requestCode, resultCode, data); } } public final void onActivityStarted(@NonNull Activity activity) { for (RouterTransaction transaction : backstack) { transaction.controller.activityStarted(activity); for (Router childRouter : transaction.controller.getChildRouters()) { childRouter.onActivityStarted(activity); } } } public final void onActivityResumed(@NonNull Activity activity) { for (RouterTransaction transaction : backstack) { transaction.controller.activityResumed(activity); for (Router childRouter : transaction.controller.getChildRouters()) { childRouter.onActivityResumed(activity); } } } public final void onActivityPaused(@NonNull Activity activity) { for (RouterTransaction transaction : backstack) { transaction.controller.activityPaused(activity); for (Router childRouter : transaction.controller.getChildRouters()) { childRouter.onActivityPaused(activity); } } } public final void onActivityStopped(@NonNull Activity activity) { for (RouterTransaction transaction : backstack) { transaction.controller.activityStopped(activity); for (Router childRouter : transaction.controller.getChildRouters()) { childRouter.onActivityStopped(activity); } } } public void onActivityDestroyed(@NonNull Activity activity) { prepareForContainerRemoval(); changeListeners.clear(); for (RouterTransaction transaction : backstack) { transaction.controller.activityDestroyed(activity.isChangingConfigurations()); for (Router childRouter : transaction.controller.getChildRouters()) { childRouter.onActivityDestroyed(activity); } } for (int index = destroyingControllers.size() - 1; index >= 0; index--) { Controller controller = destroyingControllers.get(index); controller.activityDestroyed(activity.isChangingConfigurations()); for (Router childRouter : controller.getChildRouters()) { childRouter.onActivityDestroyed(activity); } } container = null; } void prepareForHostDetach() { for (RouterTransaction transaction : backstack) { if (ControllerChangeHandler.completePushImmediately(transaction.controller.getInstanceId())) { transaction.controller.setNeedsAttach(); } transaction.controller.prepareForHostDetach(); } } public void saveInstanceState(@NonNull Bundle outState) { prepareForHostDetach(); Bundle backstackState = new Bundle(); backstack.saveInstanceState(backstackState); outState.putParcelable(KEY_BACKSTACK, backstackState); outState.putBoolean(KEY_POPS_LAST_VIEW, popsLastView); } public void restoreInstanceState(@NonNull Bundle savedInstanceState) { Bundle backstackBundle = savedInstanceState.getParcelable(KEY_BACKSTACK); //noinspection ConstantConditions backstack.restoreInstanceState(backstackBundle); popsLastView = savedInstanceState.getBoolean(KEY_POPS_LAST_VIEW); Iterator<RouterTransaction> backstackIterator = backstack.reverseIterator(); while (backstackIterator.hasNext()) { setControllerRouter(backstackIterator.next().controller); } } public final void onCreateOptionsMenu(@NonNull Menu menu, @NonNull MenuInflater inflater) { for (RouterTransaction transaction : backstack) { transaction.controller.createOptionsMenu(menu, inflater); for (Router childRouter : transaction.controller.getChildRouters()) { childRouter.onCreateOptionsMenu(menu, inflater); } } } public final void onPrepareOptionsMenu(@NonNull Menu menu) { for (RouterTransaction transaction : backstack) { transaction.controller.prepareOptionsMenu(menu); for (Router childRouter : transaction.controller.getChildRouters()) { childRouter.onPrepareOptionsMenu(menu); } } } public final boolean onOptionsItemSelected(@NonNull MenuItem item) { for (RouterTransaction transaction : backstack) { if (transaction.controller.optionsItemSelected(item)) { return true; } for (Router childRouter : transaction.controller.getChildRouters()) { if (childRouter.onOptionsItemSelected(item)) { return true; } } } return false; } private void popToTransaction(@NonNull RouterTransaction transaction, @Nullable ControllerChangeHandler changeHandler) { if (backstack.size() > 0) { RouterTransaction topTransaction = backstack.peek(); List<RouterTransaction> updatedBackstack = new ArrayList<>(); Iterator<RouterTransaction> backstackIterator = backstack.reverseIterator(); while (backstackIterator.hasNext()) { RouterTransaction existingTransaction = backstackIterator.next(); updatedBackstack.add(existingTransaction); if (existingTransaction == transaction) { break; } } if (changeHandler == null) { //noinspection ConstantConditions changeHandler = topTransaction.popChangeHandler(); } setBackstack(updatedBackstack, changeHandler); } } void prepareForContainerRemoval() { if (container != null) { container.setOnHierarchyChangeListener(null); } } @NonNull final List<Controller> getControllers() { List<Controller> controllers = new ArrayList<>(); Iterator<RouterTransaction> backstackIterator = backstack.reverseIterator(); while (backstackIterator.hasNext()) { controllers.add(backstackIterator.next().controller); } return controllers; } @Nullable public final Boolean handleRequestedPermission(@NonNull String permission) { for (RouterTransaction transaction : backstack) { if (transaction.controller.didRequestPermission(permission)) { return transaction.controller.shouldShowRequestPermissionRationale(permission); } } return null; } private void performControllerChange(@Nullable RouterTransaction to, @Nullable RouterTransaction from, boolean isPush) { if (isPush && to != null) { to.onAttachedToRouter(); } ControllerChangeHandler changeHandler; if (isPush) { //noinspection ConstantConditions changeHandler = to.pushChangeHandler(); } else if (from != null) { changeHandler = from.popChangeHandler(); } else { changeHandler = null; } performControllerChange(to, from, isPush, changeHandler); } private void performControllerChange(@Nullable final RouterTransaction to, @Nullable final RouterTransaction from, boolean isPush, @Nullable ControllerChangeHandler changeHandler) { Controller toController = to != null ? to.controller : null; Controller fromController = from != null ? from.controller : null; if (to != null) { to.ensureValidIndex(getTransactionIndexer()); setControllerRouter(toController); } else if (backstack.size() == 0 && !popsLastView) { // We're emptying out the backstack. Views get weird if you transition them out, so just no-op it. The hosting // Activity should be handling this by finishing or at least hiding this view. changeHandler = new NoOpControllerChangeHandler(); } ControllerChangeHandler.executeChange(toController, fromController, isPush, container, changeHandler, changeListeners); } protected void pushToBackstack(@NonNull RouterTransaction entry) { backstack.push(entry); } private void trackDestroyingController(@NonNull RouterTransaction transaction) { if (!transaction.controller.isDestroyed()) { destroyingControllers.add(transaction.controller); transaction.controller.addLifecycleListener(new LifecycleListener() { @Override public void postDestroy(@NonNull Controller controller) { destroyingControllers.remove(controller); } }); } } private void trackDestroyingControllers(@NonNull List<RouterTransaction> transactions) { for (RouterTransaction transaction : transactions) { trackDestroyingController(transaction); } } private void removeAllExceptVisibleAndUnowned() { List<View> views = new ArrayList<>(); for (RouterTransaction transaction : getVisibleTransactions(backstack.iterator())) { if (transaction.controller.getView() != null) { views.add(transaction.controller.getView()); } } for (Router router : getSiblingRouters()) { if (router.container == container) { addRouterViewsToList(router, views); } } final int childCount = container.getChildCount(); for (int i = 0; i < childCount; i++) { final View child = container.getChildAt(i); if (!views.contains(child)) { container.removeView(child); } } } // Swap around transaction indicies to ensure they don't get thrown out of order by the // developer rearranging the backstack at runtime. private void ensureOrderedTransactionIndices(List<RouterTransaction> backstack) { List<Integer> indices = new ArrayList<>(); for (RouterTransaction transaction : backstack) { transaction.ensureValidIndex(getTransactionIndexer()); indices.add(transaction.transactionIndex); } Collections.sort(indices); for (int i = 0; i < backstack.size(); i++) { backstack.get(i).transactionIndex = indices.get(i); } } private void addRouterViewsToList(@NonNull Router router, @NonNull List<View> list) { for (Controller controller : router.getControllers()) { if (controller.getView() != null) { list.add(controller.getView()); } for (Router child : controller.getChildRouters()) { addRouterViewsToList(child, list); } } } private List<RouterTransaction> getVisibleTransactions(@NonNull Iterator<RouterTransaction> backstackIterator) { List<RouterTransaction> transactions = new ArrayList<>(); while (backstackIterator.hasNext()) { RouterTransaction transaction = backstackIterator.next(); transactions.add(transaction); //noinspection ConstantConditions if (transaction.pushChangeHandler() == null || transaction.pushChangeHandler().removesFromViewOnPush()) { break; } } Collections.reverse(transactions); return transactions; } private boolean backstacksAreEqual(List<RouterTransaction> lhs, List<RouterTransaction> rhs) { if (lhs.size() != rhs.size()) { return false; } for (int i = 0; i < rhs.size(); i++) { if (rhs.get(i).controller() != lhs.get(i).controller()) { return false; } } return true; } void setControllerRouter(@NonNull Controller controller) { controller.setRouter(this); } abstract void invalidateOptionsMenu(); abstract void startActivity(@NonNull Intent intent); abstract void startActivityForResult(@NonNull String instanceId, @NonNull Intent intent, int requestCode); abstract void startActivityForResult(@NonNull String instanceId, @NonNull Intent intent, int requestCode, @Nullable Bundle options); abstract void startIntentSenderForResult(@NonNull String instanceId, @NonNull IntentSender intent, int requestCode, @Nullable Intent fillInIntent, int flagsMask, int flagsValues, int extraFlags, @Nullable Bundle options) throws IntentSender.SendIntentException; abstract void registerForActivityResult(@NonNull String instanceId, int requestCode); abstract void unregisterForActivityResults(@NonNull String instanceId); abstract void requestPermissions(@NonNull String instanceId, @NonNull String[] permissions, int requestCode); abstract boolean hasHost(); @NonNull abstract List<Router> getSiblingRouters(); @NonNull abstract Router getRootRouter(); @Nullable abstract TransactionIndexer getTransactionIndexer(); }