From 28cfa34776952b90a1001ce10c5d5f0390299a9d Mon Sep 17 00:00:00 2001 From: Sergey Petrov Date: Fri, 6 Sep 2019 19:46:42 +0700 Subject: [PATCH] #176: master/detail controller implementation --- .../com/bluelinelabs/conductor/Backstack.java | 4 + .../bluelinelabs/conductor/Controller.java | 41 +- .../conductor/MasterDetailController.java | 457 ++++++++++++++ .../com/bluelinelabs/conductor/Router.java | 46 +- .../conductor/RouterTransaction.java | 14 + .../conductor/BackstackTests.java | 15 + ...rollerLifecycleActivityReferenceTests.java | 242 +++++-- .../ControllerLifecycleCallbacksTests.java | 383 +++++++---- .../conductor/ControllerTests.java | 2 +- .../conductor/ControllerTransactionTests.java | 2 + .../MasterDetailControllerTests.java | 597 ++++++++++++++++++ .../conductor/ReattachCaseTests.java | 214 ++++++- .../conductor/RouterChangeHandlerTests.java | 274 ++++---- .../conductor/TargetControllerTests.java | 30 + .../conductor/util/ActivityProxy.java | 10 +- .../conductor/util/CallStateOwner.java | 5 + .../util/ChangeHandlerHistoryOwner.java | 5 + .../conductor/util/TestController.java | 15 +- .../util/TestMasterDetailController.java | 177 ++++++ .../listeners/BackstackChangeListener.java | 28 + .../demo/controllers/ListController.java | 216 +++++++ .../MasterDetailListController.java | 150 +---- .../controllers/NavigationDemoController.java | 4 + .../demo/controllers/base/BaseController.java | 3 + .../base/BaseMasterDetailController.java | 103 +++ .../conductor/demo/util/ObjectUtils.java | 8 + .../drawable/ic_chevron_right_black_24dp.xml | 9 + .../controller_master_detail_list.xml | 23 - .../layout-land/controller_master_details.xml | 38 ++ ...er_detail_list.xml => controller_list.xml} | 1 + .../res/layout/controller_master_details.xml | 5 + demo/src/main/res/layout/row_detail_item.xml | 20 +- demo/src/main/res/values/strings.xml | 1 + 33 files changed, 2664 insertions(+), 478 deletions(-) create mode 100644 conductor/src/main/java/com/bluelinelabs/conductor/MasterDetailController.java create mode 100644 conductor/src/test/java/com/bluelinelabs/conductor/MasterDetailControllerTests.java create mode 100644 conductor/src/test/java/com/bluelinelabs/conductor/util/CallStateOwner.java create mode 100644 conductor/src/test/java/com/bluelinelabs/conductor/util/ChangeHandlerHistoryOwner.java create mode 100644 conductor/src/test/java/com/bluelinelabs/conductor/util/TestMasterDetailController.java create mode 100644 demo/src/main/java/com/bluelinelabs/conductor/demo/changehandler/listeners/BackstackChangeListener.java create mode 100644 demo/src/main/java/com/bluelinelabs/conductor/demo/controllers/ListController.java create mode 100644 demo/src/main/java/com/bluelinelabs/conductor/demo/controllers/base/BaseMasterDetailController.java create mode 100644 demo/src/main/java/com/bluelinelabs/conductor/demo/util/ObjectUtils.java create mode 100644 demo/src/main/res/drawable/ic_chevron_right_black_24dp.xml delete mode 100644 demo/src/main/res/layout-land/controller_master_detail_list.xml create mode 100644 demo/src/main/res/layout-land/controller_master_details.xml rename demo/src/main/res/layout/{controller_master_detail_list.xml => controller_list.xml} (94%) create mode 100644 demo/src/main/res/layout/controller_master_details.xml diff --git a/conductor/src/main/java/com/bluelinelabs/conductor/Backstack.java b/conductor/src/main/java/com/bluelinelabs/conductor/Backstack.java index 39d0e5ca..f2f305a5 100644 --- a/conductor/src/main/java/com/bluelinelabs/conductor/Backstack.java +++ b/conductor/src/main/java/com/bluelinelabs/conductor/Backstack.java @@ -22,6 +22,10 @@ boolean isEmpty() { return backstack.isEmpty(); } + void clear() { + backstack.clear(); + } + int size() { return backstack.size(); } diff --git a/conductor/src/main/java/com/bluelinelabs/conductor/Controller.java b/conductor/src/main/java/com/bluelinelabs/conductor/Controller.java index cbb05f10..2cfe4607 100644 --- a/conductor/src/main/java/com/bluelinelabs/conductor/Controller.java +++ b/conductor/src/main/java/com/bluelinelabs/conductor/Controller.java @@ -52,6 +52,7 @@ public abstract class Controller { private static final String KEY_TARGET_INSTANCE_ID = "Controller.target.instanceId"; private static final String KEY_ARGS = "Controller.args"; private static final String KEY_NEEDS_ATTACH = "Controller.needsAttach"; + private static final String KEY_IS_DETAIL = "Controller.isDetail"; private static final String KEY_REQUESTED_PERMISSIONS = "Controller.requestedPermissions"; private static final String KEY_OVERRIDDEN_PUSH_HANDLER = "Controller.overriddenPushHandler"; private static final String KEY_OVERRIDDEN_POP_HANDLER = "Controller.overriddenPopHandler"; @@ -80,6 +81,7 @@ public abstract class Controller { private boolean awaitingParentAttach; private boolean hasSavedViewState; boolean isDetachFrozen; + boolean isDetail; private ControllerChangeHandler overriddenPushHandler; private ControllerChangeHandler overriddenPopHandler; private RetainViewMode retainViewMode = RetainViewMode.RELEASE_DETACH; @@ -158,10 +160,21 @@ protected Controller(@Nullable Bundle args) { protected abstract View onCreateView(@NonNull LayoutInflater inflater, @NonNull ViewGroup container); /** - * Returns the {@link Router} object that can be used for pushing or popping other Controllers + * Returns the {@link Router} object that can be used for pushing or popping other Controllers. + * NOTE: If this Controller is pushed to {@link MasterDetailController}, + * method will return either {@link MasterDetailController#getMasterRouter()} or + * {@link MasterDetailController#getDetailRouter()} */ public final Router getRouter() { - return router; + if (getMasterDetailController() != null) { + if (isDetail) { + return getMasterDetailController().getDetailRouter(); + } else { + return getMasterDetailController().getMasterRouter(); + } + } else { + return router; + } } /** @@ -272,6 +285,13 @@ public final boolean isAttached() { return attached; } + /** + * Returns whether or not this Controller is currently shown in master/detail as detail. + */ + public final boolean isDetail() { + return isDetail; + } + /** * Return this Controller's View or {@code null} if it has not yet been created or has been * destroyed. @@ -319,6 +339,17 @@ public final Controller getParentController() { return parentController; } + /** + * Returns this Controller's parent master/details Controller or {@code null} if + * it has no parent or parent is not a master/detail Controller. + */ + @Nullable + public final MasterDetailController getMasterDetailController() { + return parentController instanceof MasterDetailController + ? (MasterDetailController) parentController + : null; + } + /** * Returns this Controller's instance ID, which is generated when the instance is created and * retained across restarts. @@ -610,7 +641,7 @@ public int compare(RouterTransaction o1, RouterTransaction o2) { for (RouterTransaction transaction : childTransactions) { Controller childController = transaction.controller; - if (childController.isAttached() && childController.getRouter().handleBack()) { + if (childController.isAttached() && childController.router.handleBack()) { return true; } } @@ -1060,7 +1091,7 @@ private void restoreChildControllerHosts() { if (!childRouter.hasHost()) { View containerView = view.findViewById(childRouter.getHostId()); - if (containerView != null && containerView instanceof ViewGroup) { + if (containerView instanceof ViewGroup) { childRouter.setHost(this, (ViewGroup)containerView); childRouter.rebindIfNeeded(); } @@ -1173,6 +1204,7 @@ final Bundle saveInstanceState() { outState.putString(KEY_TARGET_INSTANCE_ID, targetInstanceId); outState.putStringArrayList(KEY_REQUESTED_PERMISSIONS, requestedPermissions); outState.putBoolean(KEY_NEEDS_ATTACH, needsAttach || attached); + outState.putBoolean(KEY_IS_DETAIL, isDetail); outState.putInt(KEY_RETAIN_VIEW_MODE, retainViewMode.ordinal()); if (overriddenPushHandler != null) { @@ -1215,6 +1247,7 @@ private void restoreInstanceState(@NonNull Bundle savedInstanceState) { overriddenPushHandler = ControllerChangeHandler.fromBundle(savedInstanceState.getBundle(KEY_OVERRIDDEN_PUSH_HANDLER)); overriddenPopHandler = ControllerChangeHandler.fromBundle(savedInstanceState.getBundle(KEY_OVERRIDDEN_POP_HANDLER)); needsAttach = savedInstanceState.getBoolean(KEY_NEEDS_ATTACH); + isDetail = savedInstanceState.getBoolean(KEY_IS_DETAIL); retainViewMode = RetainViewMode.values()[savedInstanceState.getInt(KEY_RETAIN_VIEW_MODE, 0)]; List childBundles = savedInstanceState.getParcelableArrayList(KEY_CHILD_ROUTERS); diff --git a/conductor/src/main/java/com/bluelinelabs/conductor/MasterDetailController.java b/conductor/src/main/java/com/bluelinelabs/conductor/MasterDetailController.java new file mode 100644 index 00000000..d691c4a9 --- /dev/null +++ b/conductor/src/main/java/com/bluelinelabs/conductor/MasterDetailController.java @@ -0,0 +1,457 @@ +package com.bluelinelabs.conductor; + +import android.os.Bundle; +import android.view.View; +import android.view.ViewGroup; + +import androidx.annotation.IdRes; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.bluelinelabs.conductor.changehandler.HorizontalChangeHandler; +import com.bluelinelabs.conductor.internal.ThreadUtils; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +/** + * Master/detail controller implementation. + * + * @see masterTransactions = new ArrayList<>(); + List detailTransactions = new ArrayList<>(); + List backstack = masterRouter.getBackstack(); + for (RouterTransaction transaction : backstack) { + if (transaction.isDetail()) { + detailTransactions.add(transaction); + } else { + masterTransactions.add(transaction); + } + } + if (!detailTransactions.isEmpty()) { + setNeedsAttachLast(detailTransactions); + setNeedsAttachLast(masterTransactions); + cachedDetailRouter.setBackstack(detailTransactions, null); + masterRouter.setBackstack(masterTransactions, null); + } + } + + private void mergeBackstacks() { + Router detailRouter = null; + for (Router router : getChildRouters()) { + if (((ControllerHostedRouter) router).getHostId() == detailContainerId) { + detailRouter = router; + break; + } + } + if (detailRouter != null && detailRouter.hasRootController()) { + List masterTransactions = masterRouter.getBackstack(); + masterTransactions.addAll(detailRouter.getBackstack()); + setNeedsAttachLast(masterTransactions); + masterRouter.setBackstack(masterTransactions, null); + detailRouter.backstack.clear(); + } + } + + private void setNeedsAttachLast(List transactions) { + for (int i = 0; i < transactions.size(); i++) { + RouterTransaction transaction = transactions.get(i); + transaction.controller.setNeedsAttach(i == transactions.size() - 1); + } + } + + private List getChildBackstack(boolean isDetail) { + List transactions = new ArrayList<>(); + Iterator iterator = masterRouter.backstack.reverseIterator(); + while (iterator.hasNext()) { + RouterTransaction transaction = iterator.next(); + if (isDetail == transaction.isDetail()) { + transactions.add(transaction); + } + } + return transactions; + } + + private int getChildBackstackSize(boolean isDetail) { + int count = 0; + for (RouterTransaction transaction : masterRouter.backstack) { + if (transaction.isDetail() == isDetail) { + count++; + } + } + return count; + } + + /** + * Master Router used in single-pane mode. + * Manages the transactions which belong to master Router + * but doesn't touch the detail Router transactions + * which are temporarily placed in the master Router. + */ + private class MasterRouterForSinglePane extends ControllerHostedRouter { + + @NonNull + @Override + public Router setPopsLastView(boolean popsLastView) { + masterRouter.setPopsLastView(popsLastView); + return masterRouter; + } + + @Override + public boolean popController(@NonNull Controller controller) { + return masterRouter.popController(controller); + } + + @Override + public boolean popCurrentController() { + ThreadUtils.ensureMainThread(); + + if (getChildBackstackSize(true) == 0) { + return masterRouter.popCurrentController(); + } else { + List masterTransactions = getChildBackstack(false); + if (masterTransactions.isEmpty()) { + throw new IllegalStateException("Trying to pop the current controller when there are none on the backstack."); + } + masterTransactions.remove(masterTransactions.size() - 1); + masterTransactions.addAll(getChildBackstack(true)); + setNeedsAttachLast(masterTransactions); + masterRouter.setBackstack(masterTransactions, null); + return true; + } + } + + @Override + public void pushController(@NonNull RouterTransaction transaction) { + ThreadUtils.ensureMainThread(); + + if (getChildBackstackSize(true) == 0) { + masterRouter.pushController(transaction); + } else { + List masterTransactions = getChildBackstack(false); + masterTransactions.add(transaction); + masterTransactions.addAll(getChildBackstack(true)); + setNeedsAttachLast(masterTransactions); + masterRouter.setBackstack(masterTransactions, null); + } + } + + @Override + public void replaceTopController(@NonNull RouterTransaction transaction) { + ThreadUtils.ensureMainThread(); + + if (getChildBackstackSize(true) == 0) { + masterRouter.replaceTopController(transaction); + } else { + List masterTransactions = getChildBackstack(false); + if (masterTransactions.isEmpty()) { + masterTransactions.add(transaction); + } else { + masterTransactions.set(masterTransactions.size() - 1, transaction); + } + masterTransactions.addAll(getChildBackstack(true)); + setNeedsAttachLast(masterTransactions); + masterRouter.setBackstack(masterTransactions, null); + } + } + + @Override + public boolean popToRoot(@Nullable ControllerChangeHandler changeHandler) { + ThreadUtils.ensureMainThread(); + + if (getChildBackstackSize(true) == 0) { + return masterRouter.popToRoot(changeHandler); + } else { + List masterTransactions = getChildBackstack(false); + if (!masterTransactions.isEmpty()) { + List transactions = getChildBackstack(true); + transactions.add(0, masterTransactions.get(0)); + setNeedsAttachLast(transactions); + masterRouter.setBackstack(transactions, null); + return true; + } else { + return false; + } + } + } + + @Override + public boolean popToTag(@NonNull String tag, @Nullable ControllerChangeHandler changeHandler) { + ThreadUtils.ensureMainThread(); + + if (getChildBackstackSize(true) == 0) { + return masterRouter.popToTag(tag, changeHandler); + } else { + int index = -1; + List masterTransactions = getChildBackstack(false); + for (int i = 0; i < masterTransactions.size(); i++) { + if (tag.equals(masterTransactions.get(i).tag())) { + index = i; + break; + } + } + if (index == -1) { + return false; + } + List transactions = new ArrayList<>(masterTransactions.subList(0, index + 1)); + transactions.addAll(getChildBackstack(true)); + setNeedsAttachLast(transactions); + masterRouter.setBackstack(transactions, null); + return true; + } + } + + @Override + public void setRoot(@NonNull RouterTransaction transaction) { + ThreadUtils.ensureMainThread(); + + if (getChildBackstackSize(true) == 0) { + masterRouter.setRoot(transaction); + } else { + List transactions = getChildBackstack(true); + transactions.add(0, transaction); + setNeedsAttachLast(transactions); + masterRouter.setBackstack(transactions, null); + } + } + + @Override @NonNull + public List getBackstack() { + return getChildBackstack(false); + } + + @Override + public int getBackstackSize() { + return getChildBackstackSize(false); + } + } + + /** + * Detail Router used in single-pane mode. + * Manages the transactions which belong to detail Router + * but temporarily are placed in the master Router. + */ + private class DetailRouterForSinglePane extends ControllerHostedRouter { + + @Override @NonNull + public Router setPopsLastView(boolean popsLastView) { + ThreadUtils.ensureMainThread(); + + detailPopsLastView = popsLastView; + return this; + } + + @Override + public boolean popController(@NonNull Controller controller) { + return masterRouter.popController(controller); + } + + @Override + public boolean popCurrentController() { + return masterRouter.popCurrentController(); + } + + @Override + public void pushController(@NonNull RouterTransaction transaction) { + masterRouter.pushController(transaction.isDetail(true)); + } + + @Override + public void replaceTopController(@NonNull RouterTransaction transaction) { + masterRouter.replaceTopController(transaction.isDetail(true)); + } + + @Override + public boolean popToRoot(@Nullable ControllerChangeHandler changeHandler) { + ThreadUtils.ensureMainThread(); + + Iterator iterator = masterRouter.backstack.reverseIterator(); + while (iterator.hasNext()) { + RouterTransaction transaction = iterator.next(); + if (transaction.isDetail()) { + masterRouter.popToTransaction(transaction, changeHandler); + return true; + } + } + return false; + } + + @Override + public boolean popToTag(@NonNull String tag, @Nullable ControllerChangeHandler changeHandler) { + ThreadUtils.ensureMainThread(); + + for (RouterTransaction transaction : masterRouter.backstack) { + if (transaction.isDetail() && tag.equals(transaction.tag())) { + masterRouter.popToTransaction(transaction, changeHandler); + return true; + } + } + return false; + } + + @Override + public void setRoot(@NonNull RouterTransaction transaction) { + ThreadUtils.ensureMainThread(); + + List transactions = getChildBackstack(false); + transactions.add(transaction.isDetail(true)); + masterRouter.setBackstack(transactions, transaction.pushChangeHandler() != null + ? transaction.pushChangeHandler() + : getRootDetailPushHandler()); + } + + @Override @NonNull + public List getBackstack() { + ThreadUtils.ensureMainThread(); + + return getChildBackstack(true); + } + + @Override + public int getBackstackSize() { + ThreadUtils.ensureMainThread(); + + return getChildBackstackSize(true); + } + } +} diff --git a/conductor/src/main/java/com/bluelinelabs/conductor/Router.java b/conductor/src/main/java/com/bluelinelabs/conductor/Router.java index b0d8b095..779c9b46 100644 --- a/conductor/src/main/java/com/bluelinelabs/conductor/Router.java +++ b/conductor/src/main/java/com/bluelinelabs/conductor/Router.java @@ -36,6 +36,7 @@ public abstract class Router { private static final String KEY_BACKSTACK = "Router.backstack"; private static final String KEY_POPS_LAST_VIEW = "Router.popsLastView"; + private static final String KEY_IS_DETAIL = "Router.isDetail"; final Backstack backstack = new Backstack(); private final List changeListeners = new ArrayList<>(); @@ -45,6 +46,7 @@ public abstract class Router { private boolean popsLastView = false; boolean containerFullyAttached = false; boolean isActivityStopped = false; + boolean isDetail = false; ViewGroup container; @@ -412,6 +414,7 @@ public void setBackstack(@NonNull List newBackstack, @Nullabl removeAllExceptVisibleAndUnowned(); ensureOrderedTransactionIndices(newBackstack); ensureNoDuplicateControllers(newBackstack); + updateIsDetail(newBackstack); backstack.setBackstack(newBackstack); @@ -425,7 +428,7 @@ public void setBackstack(@NonNull List newBackstack, @Nullabl } } - if (!contains) { + if (!contains && oldTransaction.controller.router == this) { // Inform the controller that it will be destroyed soon oldTransaction.controller.isBeingDestroyed = true; transactionsToBeRemoved.add(oldTransaction); @@ -647,6 +650,7 @@ public void saveInstanceState(@NonNull Bundle outState) { outState.putParcelable(KEY_BACKSTACK, backstackState); outState.putBoolean(KEY_POPS_LAST_VIEW, popsLastView); + outState.putBoolean(KEY_IS_DETAIL, isDetail); } public void restoreInstanceState(@NonNull Bundle savedInstanceState) { @@ -654,6 +658,7 @@ public void restoreInstanceState(@NonNull Bundle savedInstanceState) { //noinspection ConstantConditions backstack.restoreInstanceState(backstackBundle); popsLastView = savedInstanceState.getBoolean(KEY_POPS_LAST_VIEW); + isDetail = savedInstanceState.getBoolean(KEY_IS_DETAIL); Iterator backstackIterator = backstack.reverseIterator(); while (backstackIterator.hasNext()) { @@ -696,7 +701,7 @@ public final boolean onOptionsItemSelected(@NonNull MenuItem item) { return false; } - private void popToTransaction(@NonNull RouterTransaction transaction, @Nullable ControllerChangeHandler changeHandler) { + void popToTransaction(@NonNull RouterTransaction transaction, @Nullable ControllerChangeHandler changeHandler) { if (backstack.size() > 0) { RouterTransaction topTransaction = backstack.peek(); @@ -779,7 +784,7 @@ private void performControllerChange(@Nullable RouterTransaction to, @Nullable R changeHandler = null; } - performControllerChange(to, from, isPush, changeHandler); + performControllerChange(to, from, isPush, validateChangeHandler(to, from, isPush, changeHandler)); } void performControllerChange(@Nullable RouterTransaction to, @Nullable RouterTransaction from, boolean isPush, @Nullable ControllerChangeHandler changeHandler) { @@ -831,6 +836,25 @@ public void run() { } } + @Nullable + private ControllerChangeHandler validateChangeHandler(@Nullable RouterTransaction to, @Nullable RouterTransaction from, boolean isPush, @Nullable ControllerChangeHandler changeHandler) { + if (changeHandler == null && to != null && from != null && to.isDetail() != from.isDetail()) { + // It looks like we are in the master/detail controller being in the single-pane mode + // and there is a transition between master and detail routers. + MasterDetailController masterDetailController = to.controller.getMasterDetailController(); + if (masterDetailController != null) { + if (isPush && !from.isDetail() && to.isDetail()) { + // Root detail controller is pushed to the master router. + return masterDetailController.getRootDetailPushHandler(); + } else if (!isPush && from.isDetail() && !to.isDetail()) { + // Root detail controller is popped out of the master router. + return masterDetailController.getRootDetailPopHandler(); + } + } + } + return changeHandler; + } + void performPendingControllerChanges() { // We're intentionally using dynamic size checking (list.size()) here so we can account for changes // that occur during this loop (ex: if a controller is popped from within onAttach) @@ -844,6 +868,7 @@ protected void pushToBackstack(@NonNull RouterTransaction entry) { if (backstack.contains(entry.controller)) { throw new IllegalStateException("Trying to push a controller that already exists on the backstack."); } + updateIsDetail(entry); backstack.push(entry); } @@ -917,6 +942,21 @@ private void ensureNoDuplicateControllers(List backstack) { } } + private void updateIsDetail(List transactions) { + for (RouterTransaction transaction : transactions) { + updateIsDetail(transaction); + } + } + + private void updateIsDetail(RouterTransaction transaction) { + // Router can only set transaction isDetail to TRUE and never to FALSE + // because transactions can be moved from router to router. + // Once isDetail is set to TRUE it must be persisted. + if (isDetail) { + transaction.isDetail(true); + } + } + private void addRouterViewsToList(@NonNull Router router, @NonNull List list) { for (Controller controller : router.getControllers()) { if (controller.getView() != null) { diff --git a/conductor/src/main/java/com/bluelinelabs/conductor/RouterTransaction.java b/conductor/src/main/java/com/bluelinelabs/conductor/RouterTransaction.java index 57b59ed1..dc4879ff 100644 --- a/conductor/src/main/java/com/bluelinelabs/conductor/RouterTransaction.java +++ b/conductor/src/main/java/com/bluelinelabs/conductor/RouterTransaction.java @@ -17,6 +17,7 @@ public class RouterTransaction { private static final String KEY_PUSH_TRANSITION = "RouterTransaction.pushControllerChangeHandler"; private static final String KEY_POP_TRANSITION = "RouterTransaction.popControllerChangeHandler"; private static final String KEY_TAG = "RouterTransaction.tag"; + private static final String KEY_IS_DETAIL = "RouterTransaction.isDetail"; private static final String KEY_INDEX = "RouterTransaction.transactionIndex"; private static final String KEY_ATTACHED_TO_ROUTER = "RouterTransaction.attachedToRouter"; @@ -26,6 +27,7 @@ public class RouterTransaction { private ControllerChangeHandler pushControllerChangeHandler; private ControllerChangeHandler popControllerChangeHandler; private boolean attachedToRouter; + private boolean isDetail; int transactionIndex = INVALID_INDEX; @NonNull @@ -42,6 +44,7 @@ private RouterTransaction(@NonNull Controller controller) { pushControllerChangeHandler = ControllerChangeHandler.fromBundle(bundle.getBundle(KEY_PUSH_TRANSITION)); popControllerChangeHandler = ControllerChangeHandler.fromBundle(bundle.getBundle(KEY_POP_TRANSITION)); tag = bundle.getString(KEY_TAG); + isDetail = bundle.getBoolean(KEY_IS_DETAIL); transactionIndex = bundle.getInt(KEY_INDEX); attachedToRouter = bundle.getBoolean(KEY_ATTACHED_TO_ROUTER); } @@ -114,6 +117,16 @@ void ensureValidIndex(@NonNull TransactionIndexer indexer) { } } + RouterTransaction isDetail(boolean isDetail) { + this.isDetail = isDetail; + this.controller.isDetail = isDetail; + return this; + } + + boolean isDetail() { + return isDetail; + } + /** * Used to serialize this transaction into a Bundle */ @@ -131,6 +144,7 @@ public Bundle saveInstanceState() { } bundle.putString(KEY_TAG, tag); + bundle.putBoolean(KEY_IS_DETAIL, isDetail); bundle.putInt(KEY_INDEX, transactionIndex); bundle.putBoolean(KEY_ATTACHED_TO_ROUTER, attachedToRouter); diff --git a/conductor/src/test/java/com/bluelinelabs/conductor/BackstackTests.java b/conductor/src/test/java/com/bluelinelabs/conductor/BackstackTests.java index 9783f99b..b3d499dc 100644 --- a/conductor/src/test/java/com/bluelinelabs/conductor/BackstackTests.java +++ b/conductor/src/test/java/com/bluelinelabs/conductor/BackstackTests.java @@ -66,4 +66,19 @@ public void testPopTo() { assertEquals(1, backstack.size()); assertEquals(transaction1, backstack.peek()); } + + @Test + public void testClear() { + RouterTransaction transaction1 = RouterTransaction.with(new TestController()); + RouterTransaction transaction2 = RouterTransaction.with(new TestController()); + RouterTransaction transaction3 = RouterTransaction.with(new TestController()); + + backstack.push(transaction1); + backstack.push(transaction2); + backstack.push(transaction3); + assertEquals(3, backstack.size()); + + backstack.clear(); + assertEquals(0, backstack.size()); + } } diff --git a/conductor/src/test/java/com/bluelinelabs/conductor/ControllerLifecycleActivityReferenceTests.java b/conductor/src/test/java/com/bluelinelabs/conductor/ControllerLifecycleActivityReferenceTests.java index 651e3e66..8422e902 100644 --- a/conductor/src/test/java/com/bluelinelabs/conductor/ControllerLifecycleActivityReferenceTests.java +++ b/conductor/src/test/java/com/bluelinelabs/conductor/ControllerLifecycleActivityReferenceTests.java @@ -1,13 +1,15 @@ package com.bluelinelabs.conductor; import android.os.Bundle; -import androidx.annotation.NonNull; import android.view.View; import android.view.ViewGroup; +import androidx.annotation.NonNull; + import com.bluelinelabs.conductor.util.ActivityProxy; import com.bluelinelabs.conductor.util.MockChangeHandler; import com.bluelinelabs.conductor.util.TestController; +import com.bluelinelabs.conductor.util.TestMasterDetailController; import org.junit.Before; import org.junit.Test; @@ -52,8 +54,186 @@ public void setup() { @Test public void testSingleControllerActivityOnPush() { - Controller controller = new TestController(); + testSingleControllerActivityOnPush(new TestController()); + } + + @Test + public void testMasterDetailControllerActivityOnPush() { + testSingleControllerActivityOnPush(new TestMasterDetailController()); + } + + @Test + public void testChildControllerActivityOnPush() { + Controller parent = new TestController(); + router.pushController(RouterTransaction.with(parent) + .pushChangeHandler(MockChangeHandler.defaultHandler()) + .popChangeHandler(MockChangeHandler.defaultHandler())); + + TestController child = new TestController(); + Router router = parent.getChildRouter((ViewGroup)parent.getView().findViewById(TestController.VIEW_ID)); + testChildControllerActivityOnPush(router, child); + } + + @Test + public void testMasterControllerActivityOnPush() { + TestMasterDetailController parent = new TestMasterDetailController(); + router.pushController(RouterTransaction.with(parent) + .pushChangeHandler(MockChangeHandler.defaultHandler()) + .popChangeHandler(MockChangeHandler.defaultHandler())); + TestController child = new TestController(); + Router router = parent.getMasterRouter(); + testChildControllerActivityOnPush(router, child); + } + + @Test + public void testDetailControllerActivityOnPush() { + TestMasterDetailController parent = new TestMasterDetailController(); + router.pushController(RouterTransaction.with(parent) + .pushChangeHandler(MockChangeHandler.defaultHandler()) + .popChangeHandler(MockChangeHandler.defaultHandler())); + TestController child = new TestController(); + Router router = parent.getDetailRouter(); + testChildControllerActivityOnPush(router, child); + } + + @Test + public void testSingleControllerActivityOnPop() { + testSingleControllerActivityOnPop(new TestController()); + } + + @Test + public void testMasterDetailControllerActivityOnPop() { + testSingleControllerActivityOnPop(new TestMasterDetailController()); + } + + @Test + public void testChildControllerActivityOnPop() { + Controller parent = new TestController(); + + router.pushController(RouterTransaction.with(parent) + .pushChangeHandler(MockChangeHandler.defaultHandler()) + .popChangeHandler(MockChangeHandler.defaultHandler())); + + TestController child = new TestController(); + Router router = parent.getChildRouter((ViewGroup)parent.getView().findViewById(TestController.VIEW_ID)); + testChildControllerActivityOnPop(router, child); + } + + @Test + public void testMasterControllerActivityOnPop() { + TestMasterDetailController parent = new TestMasterDetailController(); + + router.pushController(RouterTransaction.with(parent) + .pushChangeHandler(MockChangeHandler.defaultHandler()) + .popChangeHandler(MockChangeHandler.defaultHandler())); + + TestController child = new TestController(); + Router router = parent.getMasterRouter(); + testChildControllerActivityOnPop(router, child); + } + + @Test + public void testDetailControllerActivityOnPop() { + TestMasterDetailController parent = new TestMasterDetailController(); + + router.pushController(RouterTransaction.with(parent) + .pushChangeHandler(MockChangeHandler.defaultHandler()) + .popChangeHandler(MockChangeHandler.defaultHandler())); + + TestController child = new TestController(); + Router router = parent.getDetailRouter(); + testChildControllerActivityOnPop(router, child); + } + + @Test + public void testChildControllerActivityOnParentPop() { + Controller parent = new TestController(); + + router.pushController(RouterTransaction.with(parent) + .pushChangeHandler(MockChangeHandler.defaultHandler()) + .popChangeHandler(MockChangeHandler.defaultHandler())); + + TestController child = new TestController(); + Router childRouter = parent.getChildRouter((ViewGroup)parent.getView().findViewById(TestController.VIEW_ID)); + testChildControllerActivityOnParentPop(childRouter, child); + } + + @Test + public void testMasterControllerActivityOnParentPop() { + TestMasterDetailController parent = new TestMasterDetailController(); + + router.pushController(RouterTransaction.with(parent) + .pushChangeHandler(MockChangeHandler.defaultHandler()) + .popChangeHandler(MockChangeHandler.defaultHandler())); + + TestController child = new TestController(); + Router childRouter = parent.getMasterRouter(); + testChildControllerActivityOnParentPop(childRouter, child); + } + + @Test + public void testDetailControllerActivityOnParentPop() { + TestMasterDetailController parent = new TestMasterDetailController(); + + router.pushController(RouterTransaction.with(parent) + .pushChangeHandler(MockChangeHandler.defaultHandler()) + .popChangeHandler(MockChangeHandler.defaultHandler())); + + TestController child = new TestController(); + Router childRouter = parent.getDetailRouter(); + testChildControllerActivityOnParentPop(childRouter, child); + } + + @Test + public void testSingleControllerActivityOnDestroy() { + testSingleControllerActivityOnDestroy(new TestController()); + } + + @Test + public void testMasterDetailControllerActivityOnDestroy() { + testSingleControllerActivityOnDestroy(new TestMasterDetailController()); + } + + @Test + public void testChildControllerActivityOnDestroy() { + Controller parent = new TestController(); + + router.pushController(RouterTransaction.with(parent) + .pushChangeHandler(MockChangeHandler.defaultHandler()) + .popChangeHandler(MockChangeHandler.defaultHandler())); + + TestController child = new TestController(); + Router childRouter = parent.getChildRouter((ViewGroup)parent.getView().findViewById(TestController.VIEW_ID)); + testChildControllerActivityOnDestroy(childRouter, child); + } + + @Test + public void testMasterControllerActivityOnDestroy() { + TestMasterDetailController parent = new TestMasterDetailController(); + + router.pushController(RouterTransaction.with(parent) + .pushChangeHandler(MockChangeHandler.defaultHandler()) + .popChangeHandler(MockChangeHandler.defaultHandler())); + + TestController child = new TestController(); + Router childRouter = parent.getMasterRouter(); + testChildControllerActivityOnDestroy(childRouter, child); + } + + @Test + public void testDetailControllerActivityOnDestroy() { + TestMasterDetailController parent = new TestMasterDetailController(); + router.pushController(RouterTransaction.with(parent) + .pushChangeHandler(MockChangeHandler.defaultHandler()) + .popChangeHandler(MockChangeHandler.defaultHandler())); + + TestController child = new TestController(); + Router childRouter = parent.getDetailRouter(); + testChildControllerActivityOnDestroy(childRouter, child); + } + + private void testSingleControllerActivityOnPush(Controller controller) { assertNull(controller.getActivity()); ActivityReferencingLifecycleListener listener = new ActivityReferencingLifecycleListener(); @@ -71,21 +251,12 @@ public void testSingleControllerActivityOnPush() { assertEquals(Collections.emptyList(), listener.postDestroyReferences); } - @Test - public void testChildControllerActivityOnPush() { - Controller parent = new TestController(); - router.pushController(RouterTransaction.with(parent) - .pushChangeHandler(MockChangeHandler.defaultHandler()) - .popChangeHandler(MockChangeHandler.defaultHandler())); - - TestController child = new TestController(); - + private void testChildControllerActivityOnPush(Router childRouter, Controller child) { assertNull(child.getActivity()); ActivityReferencingLifecycleListener listener = new ActivityReferencingLifecycleListener(); child.addLifecycleListener(listener); - Router childRouter = parent.getChildRouter((ViewGroup)parent.getView().findViewById(TestController.VIEW_ID)); childRouter.pushController(RouterTransaction.with(child) .pushChangeHandler(MockChangeHandler.defaultHandler()) .popChangeHandler(MockChangeHandler.defaultHandler())); @@ -98,10 +269,7 @@ public void testChildControllerActivityOnPush() { assertEquals(Collections.emptyList(), listener.postDestroyReferences); } - @Test - public void testSingleControllerActivityOnPop() { - Controller controller = new TestController(); - + private void testSingleControllerActivityOnPop(Controller controller) { ActivityReferencingLifecycleListener listener = new ActivityReferencingLifecycleListener(); controller.addLifecycleListener(listener); @@ -119,20 +287,10 @@ public void testSingleControllerActivityOnPop() { assertEquals(Collections.singletonList(true), listener.postDestroyReferences); } - @Test - public void testChildControllerActivityOnPop() { - Controller parent = new TestController(); - - router.pushController(RouterTransaction.with(parent) - .pushChangeHandler(MockChangeHandler.defaultHandler()) - .popChangeHandler(MockChangeHandler.defaultHandler())); - - TestController child = new TestController(); - + private void testChildControllerActivityOnPop(Router childRouter, Controller child) { ActivityReferencingLifecycleListener listener = new ActivityReferencingLifecycleListener(); child.addLifecycleListener(listener); - Router childRouter = parent.getChildRouter((ViewGroup)parent.getView().findViewById(TestController.VIEW_ID)); childRouter.setPopsLastView(true); childRouter.pushController(RouterTransaction.with(child) .pushChangeHandler(MockChangeHandler.defaultHandler()) @@ -148,20 +306,10 @@ public void testChildControllerActivityOnPop() { assertEquals(Collections.singletonList(true), listener.postDestroyReferences); } - @Test - public void testChildControllerActivityOnParentPop() { - Controller parent = new TestController(); - - router.pushController(RouterTransaction.with(parent) - .pushChangeHandler(MockChangeHandler.defaultHandler()) - .popChangeHandler(MockChangeHandler.defaultHandler())); - - TestController child = new TestController(); - + private void testChildControllerActivityOnParentPop(Router childRouter, Controller child) { ActivityReferencingLifecycleListener listener = new ActivityReferencingLifecycleListener(); child.addLifecycleListener(listener); - Router childRouter = parent.getChildRouter((ViewGroup)parent.getView().findViewById(TestController.VIEW_ID)); childRouter.setPopsLastView(true); childRouter.pushController(RouterTransaction.with(child) .pushChangeHandler(MockChangeHandler.defaultHandler()) @@ -177,10 +325,7 @@ public void testChildControllerActivityOnParentPop() { assertEquals(Collections.singletonList(true), listener.postDestroyReferences); } - @Test - public void testSingleControllerActivityOnDestroy() { - Controller controller = new TestController(); - + private void testSingleControllerActivityOnDestroy(Controller controller) { ActivityReferencingLifecycleListener listener = new ActivityReferencingLifecycleListener(); controller.addLifecycleListener(listener); @@ -198,20 +343,10 @@ public void testSingleControllerActivityOnDestroy() { assertEquals(Collections.singletonList(true), listener.postDestroyReferences); } - @Test - public void testChildControllerActivityOnDestroy() { - Controller parent = new TestController(); - - router.pushController(RouterTransaction.with(parent) - .pushChangeHandler(MockChangeHandler.defaultHandler()) - .popChangeHandler(MockChangeHandler.defaultHandler())); - - TestController child = new TestController(); - + private void testChildControllerActivityOnDestroy(Router childRouter, Controller child) { ActivityReferencingLifecycleListener listener = new ActivityReferencingLifecycleListener(); child.addLifecycleListener(listener); - Router childRouter = parent.getChildRouter((ViewGroup)parent.getView().findViewById(TestController.VIEW_ID)); childRouter.setPopsLastView(true); childRouter.pushController(RouterTransaction.with(child) .pushChangeHandler(MockChangeHandler.defaultHandler()) @@ -227,6 +362,7 @@ public void testChildControllerActivityOnDestroy() { assertEquals(Collections.singletonList(true), listener.postDestroyReferences); } + static class ActivityReferencingLifecycleListener extends Controller.LifecycleListener { final List changeEndReferences = new ArrayList<>(); final List postCreateViewReferences = new ArrayList<>(); diff --git a/conductor/src/test/java/com/bluelinelabs/conductor/ControllerLifecycleCallbacksTests.java b/conductor/src/test/java/com/bluelinelabs/conductor/ControllerLifecycleCallbacksTests.java index 0b44c02a..9d3de9cc 100644 --- a/conductor/src/test/java/com/bluelinelabs/conductor/ControllerLifecycleCallbacksTests.java +++ b/conductor/src/test/java/com/bluelinelabs/conductor/ControllerLifecycleCallbacksTests.java @@ -2,18 +2,21 @@ import android.content.Context; import android.os.Bundle; -import androidx.annotation.NonNull; import android.view.View; import android.view.ViewGroup; +import androidx.annotation.NonNull; + import com.bluelinelabs.conductor.Controller.LifecycleListener; import com.bluelinelabs.conductor.Controller.RetainViewMode; import com.bluelinelabs.conductor.changehandler.SimpleSwapChangeHandler; import com.bluelinelabs.conductor.util.ActivityProxy; import com.bluelinelabs.conductor.util.CallState; +import com.bluelinelabs.conductor.util.CallStateOwner; import com.bluelinelabs.conductor.util.MockChangeHandler; import com.bluelinelabs.conductor.util.MockChangeHandler.ChangeHandlerListener; import com.bluelinelabs.conductor.util.TestController; +import com.bluelinelabs.conductor.util.TestMasterDetailController; import com.bluelinelabs.conductor.util.ViewUtils; import org.junit.Before; @@ -59,7 +62,173 @@ public void setup() { @Test public void testNormalLifecycle() { - TestController controller = new TestController(); + testNormalLifecycle(new TestController()); + } + + @Test + public void testNormalLifecycleMasterDetail() { + testNormalLifecycle(new TestMasterDetailController()); + } + + @Test + public void testLifecycleWithActivityStop() { + testLifecycleWithActivityStop(new TestController()); + } + + @Test + public void testLifecycleWithActivityStopMasterDetail() { + testLifecycleWithActivityStop(new TestMasterDetailController()); + } + + @Test + public void testLifecycleWithActivityDestroy() { + testLifecycleWithActivityDestroy(new TestController()); + } + + @Test + public void testLifecycleWithActivityDestroyMasterDetail() { + testLifecycleWithActivityDestroy(new TestController()); + } + + @Test + public void testLifecycleWithActivityConfigurationChange() { + testLifecycleWithActivityConfigurationChange(new TestController()); + } + + @Test + public void testLifecycleWithActivityConfigurationChangeMasterDetail() { + testLifecycleWithActivityConfigurationChange(new TestMasterDetailController()); + } + + @Test + public void testLifecycleWithActivityBackground() { + testLifecycleWithActivityBackground(new TestController()); + } + + @Test + public void testLifecycleWithActivityBackgroundMasterDetail() { + testLifecycleWithActivityBackground(new TestMasterDetailController()); + } + + @Test + public void testLifecycleCallOrder() { + testLifecycleCallOrder(new TestController()); + } + + @Test + public void testLifecycleCallOrderMasterDetail() { + testLifecycleCallOrder(new TestMasterDetailController()); + } + + @Test + public void testChildLifecycle() { + Controller parent = new TestController(); + router.pushController(RouterTransaction.with(parent) + .pushChangeHandler(MockChangeHandler.defaultHandler())); + + TestController child = new TestController(); + Router childRouter = parent.getChildRouter((ViewGroup)parent.getView().findViewById(TestController.VIEW_ID)); + testChildLifecycle(parent, childRouter, child); + } + + @Test + public void testChildLifecycleMaster() { + TestMasterDetailController parent = new TestMasterDetailController(); + router.pushController(RouterTransaction.with(parent) + .pushChangeHandler(MockChangeHandler.defaultHandler())); + + TestController child = new TestController(); + Router childRouter = parent.getMasterRouter(); + testChildLifecycle(parent, childRouter, child); + } + + @Test + public void testChildLifecycleDetail() { + TestMasterDetailController parent = new TestMasterDetailController(); + router.pushController(RouterTransaction.with(parent) + .pushChangeHandler(MockChangeHandler.defaultHandler())); + + TestController child = new TestController(); + Router childRouter = parent.getDetailRouter(); + testChildLifecycle(parent, childRouter, child); + } + + @Test + public void testChildLifecycle2() { + Controller parent = new TestController(); + router.pushController(RouterTransaction.with(parent) + .pushChangeHandler(MockChangeHandler.defaultHandler()) + .popChangeHandler(MockChangeHandler.defaultHandler())); + + TestController child = new TestController(); + Router childRouter = parent.getChildRouter((ViewGroup)parent.getView().findViewById(TestController.VIEW_ID)); + testChildLifecycle2(parent, childRouter, child); + } + + @Test + public void testChildLifecycle2Master() { + TestMasterDetailController parent = new TestMasterDetailController(); + router.pushController(RouterTransaction.with(parent) + .pushChangeHandler(MockChangeHandler.defaultHandler()) + .popChangeHandler(MockChangeHandler.defaultHandler())); + + TestController child = new TestController(); + Router childRouter = parent.getMasterRouter(); + testChildLifecycle2(parent, childRouter, child); + } + + @Test + public void testChildLifecycle2Detail() { + TestMasterDetailController parent = new TestMasterDetailController(); + router.pushController(RouterTransaction.with(parent) + .pushChangeHandler(MockChangeHandler.defaultHandler()) + .popChangeHandler(MockChangeHandler.defaultHandler())); + + TestController child = new TestController(); + Router childRouter = parent.getDetailRouter(); + testChildLifecycle2(parent, childRouter, child); + } + + @Test + public void testChildLifecycleOrderingAfterUnexpectedAttach() { + Controller parent = new TestController(); + parent.setRetainViewMode(RetainViewMode.RETAIN_DETACH); + router.pushController(RouterTransaction.with(parent) + .pushChangeHandler(MockChangeHandler.defaultHandler()) + .popChangeHandler(MockChangeHandler.defaultHandler())); + + TestController child = new TestController(); + Router childRouter = parent.getChildRouter((ViewGroup)parent.getView().findViewById(TestController.VIEW_ID)); + testChildLifecycleOrderingAfterUnexpectedAttach(parent, childRouter, child); + } + + @Test + public void testChildLifecycleOrderingAfterUnexpectedAttachMaster() { + TestMasterDetailController parent = new TestMasterDetailController(); + parent.setRetainViewMode(RetainViewMode.RETAIN_DETACH); + router.pushController(RouterTransaction.with(parent) + .pushChangeHandler(MockChangeHandler.defaultHandler()) + .popChangeHandler(MockChangeHandler.defaultHandler())); + + TestController child = new TestController(); + Router childRouter = parent.getMasterRouter(); + testChildLifecycleOrderingAfterUnexpectedAttach(parent, childRouter, child); + } + + @Test + public void testChildLifecycleOrderingAfterUnexpectedAttachDetail() { + TestMasterDetailController parent = new TestMasterDetailController(); + parent.setRetainViewMode(RetainViewMode.RETAIN_DETACH); + router.pushController(RouterTransaction.with(parent) + .pushChangeHandler(MockChangeHandler.defaultHandler()) + .popChangeHandler(MockChangeHandler.defaultHandler())); + + TestController child = new TestController(); + Router childRouter = parent.getDetailRouter(); + testChildLifecycleOrderingAfterUnexpectedAttach(parent, childRouter, child); + } + + private void testNormalLifecycle(Controller controller) { attachLifecycleListener(controller); CallState expectedCallState = new CallState(false); @@ -78,9 +247,7 @@ public void testNormalLifecycle() { assertCalls(expectedCallState, controller); } - @Test - public void testLifecycleWithActivityStop() { - TestController controller = new TestController(); + private void testLifecycleWithActivityStop(Controller controller) { attachLifecycleListener(controller); CallState expectedCallState = new CallState(false); @@ -109,9 +276,7 @@ public void testLifecycleWithActivityStop() { assertCalls(expectedCallState, controller); } - @Test - public void testLifecycleWithActivityDestroy() { - TestController controller = new TestController(); + private void testLifecycleWithActivityDestroy(Controller controller) { attachLifecycleListener(controller); CallState expectedCallState = new CallState(false); @@ -141,9 +306,7 @@ public void testLifecycleWithActivityDestroy() { assertCalls(expectedCallState, controller); } - @Test - public void testLifecycleWithActivityConfigurationChange() { - TestController controller = new TestController(); + private void testLifecycleWithActivityConfigurationChange(Controller controller) { attachLifecycleListener(controller); CallState expectedCallState = new CallState(false); @@ -177,7 +340,7 @@ public void testLifecycleWithActivityConfigurationChange() { assertCalls(expectedCallState, controller); createActivityController(bundle, false); - controller = (TestController)router.getControllerWithTag("root"); + controller = router.getControllerWithTag("root"); expectedCallState.contextAvailableCalls++; expectedCallState.restoreInstanceStateCalls++; @@ -185,20 +348,23 @@ public void testLifecycleWithActivityConfigurationChange() { expectedCallState.changeStartCalls++; expectedCallState.createViewCalls++; + assertTrue(controller instanceof CallStateOwner); + CallStateOwner owner = (CallStateOwner) controller; + // Lifecycle listener isn't attached during restore, grab the current views from the controller for this stuff... - currentCallState.restoreInstanceStateCalls = controller.currentCallState.restoreInstanceStateCalls; - currentCallState.restoreViewStateCalls = controller.currentCallState.restoreViewStateCalls; - currentCallState.changeStartCalls = controller.currentCallState.changeStartCalls; - currentCallState.changeEndCalls = controller.currentCallState.changeEndCalls; - currentCallState.createViewCalls = controller.currentCallState.createViewCalls; - currentCallState.attachCalls = controller.currentCallState.attachCalls; - currentCallState.contextAvailableCalls = controller.currentCallState.contextAvailableCalls; + currentCallState.restoreInstanceStateCalls = owner.currentCallState().restoreInstanceStateCalls; + currentCallState.restoreViewStateCalls = owner.currentCallState().restoreViewStateCalls; + currentCallState.changeStartCalls = owner.currentCallState().changeStartCalls; + currentCallState.changeEndCalls = owner.currentCallState().changeEndCalls; + currentCallState.createViewCalls = owner.currentCallState().createViewCalls; + currentCallState.attachCalls = owner.currentCallState().attachCalls; + currentCallState.contextAvailableCalls = owner.currentCallState().contextAvailableCalls; assertCalls(expectedCallState, controller); activityProxy.start().resume(); - currentCallState.changeEndCalls = controller.currentCallState.changeEndCalls; - currentCallState.attachCalls = controller.currentCallState.attachCalls; + currentCallState.changeEndCalls = owner.currentCallState().changeEndCalls; + currentCallState.attachCalls = owner.currentCallState().attachCalls; expectedCallState.changeEndCalls++; expectedCallState.attachCalls++; @@ -208,9 +374,7 @@ public void testLifecycleWithActivityConfigurationChange() { assertCalls(expectedCallState, controller); } - @Test - public void testLifecycleWithActivityBackground() { - TestController controller = new TestController(); + private void testLifecycleWithActivityBackground(Controller controller) { attachLifecycleListener(controller); CallState expectedCallState = new CallState(false); @@ -235,208 +399,208 @@ public void testLifecycleWithActivityBackground() { assertCalls(expectedCallState, controller); } - @Test - public void testLifecycleCallOrder() { - final TestController testController = new TestController(); + private void testLifecycleCallOrder(Controller controller) { + assertTrue(controller instanceof CallStateOwner); final CallState callState = new CallState(false); + final CallState controllerCallState = ((CallStateOwner) controller).currentCallState(); - testController.addLifecycleListener(new LifecycleListener() { + controller.addLifecycleListener(new LifecycleListener() { @Override public void preCreateView(@NonNull Controller controller) { callState.createViewCalls++; assertEquals(1, callState.createViewCalls); - assertEquals(0, testController.currentCallState.createViewCalls); + assertEquals(0, controllerCallState.createViewCalls); assertEquals(0, callState.attachCalls); - assertEquals(0, testController.currentCallState.attachCalls); + assertEquals(0, controllerCallState.attachCalls); assertEquals(0, callState.detachCalls); - assertEquals(0, testController.currentCallState.detachCalls); + assertEquals(0, controllerCallState.detachCalls); assertEquals(0, callState.destroyViewCalls); - assertEquals(0, testController.currentCallState.destroyViewCalls); + assertEquals(0, controllerCallState.destroyViewCalls); assertEquals(0, callState.destroyCalls); - assertEquals(0, testController.currentCallState.destroyCalls); + assertEquals(0, controllerCallState.destroyCalls); } @Override public void postCreateView(@NonNull Controller controller, @NonNull View view) { callState.createViewCalls++; assertEquals(2, callState.createViewCalls); - assertEquals(1, testController.currentCallState.createViewCalls); + assertEquals(1, controllerCallState.createViewCalls); assertEquals(0, callState.attachCalls); - assertEquals(0, testController.currentCallState.attachCalls); + assertEquals(0, controllerCallState.attachCalls); assertEquals(0, callState.detachCalls); - assertEquals(0, testController.currentCallState.detachCalls); + assertEquals(0, controllerCallState.detachCalls); assertEquals(0, callState.destroyViewCalls); - assertEquals(0, testController.currentCallState.destroyViewCalls); + assertEquals(0, controllerCallState.destroyViewCalls); assertEquals(0, callState.destroyCalls); - assertEquals(0, testController.currentCallState.destroyCalls); + assertEquals(0, controllerCallState.destroyCalls); } @Override public void preAttach(@NonNull Controller controller, @NonNull View view) { callState.attachCalls++; assertEquals(2, callState.createViewCalls); - assertEquals(1, testController.currentCallState.createViewCalls); + assertEquals(1, controllerCallState.createViewCalls); assertEquals(1, callState.attachCalls); - assertEquals(0, testController.currentCallState.attachCalls); + assertEquals(0, controllerCallState.attachCalls); assertEquals(0, callState.detachCalls); - assertEquals(0, testController.currentCallState.detachCalls); + assertEquals(0, controllerCallState.detachCalls); assertEquals(0, callState.destroyViewCalls); - assertEquals(0, testController.currentCallState.destroyViewCalls); + assertEquals(0, controllerCallState.destroyViewCalls); assertEquals(0, callState.destroyCalls); - assertEquals(0, testController.currentCallState.destroyCalls); + assertEquals(0, controllerCallState.destroyCalls); } @Override public void postAttach(@NonNull Controller controller, @NonNull View view) { callState.attachCalls++; assertEquals(2, callState.createViewCalls); - assertEquals(1, testController.currentCallState.createViewCalls); + assertEquals(1, controllerCallState.createViewCalls); assertEquals(2, callState.attachCalls); - assertEquals(1, testController.currentCallState.attachCalls); + assertEquals(1, controllerCallState.attachCalls); assertEquals(0, callState.detachCalls); - assertEquals(0, testController.currentCallState.detachCalls); + assertEquals(0, controllerCallState.detachCalls); assertEquals(0, callState.destroyViewCalls); - assertEquals(0, testController.currentCallState.destroyViewCalls); + assertEquals(0, controllerCallState.destroyViewCalls); assertEquals(0, callState.destroyCalls); - assertEquals(0, testController.currentCallState.destroyCalls); + assertEquals(0, controllerCallState.destroyCalls); } @Override public void preDetach(@NonNull Controller controller, @NonNull View view) { callState.detachCalls++; assertEquals(2, callState.createViewCalls); - assertEquals(1, testController.currentCallState.createViewCalls); + assertEquals(1, controllerCallState.createViewCalls); assertEquals(2, callState.attachCalls); - assertEquals(1, testController.currentCallState.attachCalls); + assertEquals(1, controllerCallState.attachCalls); assertEquals(1, callState.detachCalls); - assertEquals(0, testController.currentCallState.detachCalls); + assertEquals(0, controllerCallState.detachCalls); assertEquals(0, callState.destroyViewCalls); - assertEquals(0, testController.currentCallState.destroyViewCalls); + assertEquals(0, controllerCallState.destroyViewCalls); assertEquals(0, callState.destroyCalls); - assertEquals(0, testController.currentCallState.destroyCalls); + assertEquals(0, controllerCallState.destroyCalls); } @Override public void postDetach(@NonNull Controller controller, @NonNull View view) { callState.detachCalls++; assertEquals(2, callState.createViewCalls); - assertEquals(1, testController.currentCallState.createViewCalls); + assertEquals(1, controllerCallState.createViewCalls); assertEquals(2, callState.attachCalls); - assertEquals(1, testController.currentCallState.attachCalls); + assertEquals(1, controllerCallState.attachCalls); assertEquals(2, callState.detachCalls); - assertEquals(1, testController.currentCallState.detachCalls); + assertEquals(1, controllerCallState.detachCalls); assertEquals(0, callState.destroyViewCalls); - assertEquals(0, testController.currentCallState.destroyViewCalls); + assertEquals(0, controllerCallState.destroyViewCalls); assertEquals(0, callState.destroyCalls); - assertEquals(0, testController.currentCallState.destroyCalls); + assertEquals(0, controllerCallState.destroyCalls); } @Override public void preDestroyView(@NonNull Controller controller, @NonNull View view) { callState.destroyViewCalls++; assertEquals(2, callState.createViewCalls); - assertEquals(1, testController.currentCallState.createViewCalls); + assertEquals(1, controllerCallState.createViewCalls); assertEquals(2, callState.attachCalls); - assertEquals(1, testController.currentCallState.attachCalls); + assertEquals(1, controllerCallState.attachCalls); assertEquals(2, callState.detachCalls); - assertEquals(1, testController.currentCallState.detachCalls); + assertEquals(1, controllerCallState.detachCalls); assertEquals(1, callState.destroyViewCalls); - assertEquals(0, testController.currentCallState.destroyViewCalls); + assertEquals(0, controllerCallState.destroyViewCalls); assertEquals(0, callState.destroyCalls); - assertEquals(0, testController.currentCallState.destroyCalls); + assertEquals(0, controllerCallState.destroyCalls); } @Override public void postDestroyView(@NonNull Controller controller) { callState.destroyViewCalls++; assertEquals(2, callState.createViewCalls); - assertEquals(1, testController.currentCallState.createViewCalls); + assertEquals(1, controllerCallState.createViewCalls); assertEquals(2, callState.attachCalls); - assertEquals(1, testController.currentCallState.attachCalls); + assertEquals(1, controllerCallState.attachCalls); assertEquals(2, callState.detachCalls); - assertEquals(1, testController.currentCallState.detachCalls); + assertEquals(1, controllerCallState.detachCalls); assertEquals(2, callState.destroyViewCalls); - assertEquals(1, testController.currentCallState.destroyViewCalls); + assertEquals(1, controllerCallState.destroyViewCalls); assertEquals(0, callState.destroyCalls); - assertEquals(0, testController.currentCallState.destroyCalls); + assertEquals(0, controllerCallState.destroyCalls); } @Override public void preDestroy(@NonNull Controller controller) { callState.destroyCalls++; assertEquals(2, callState.createViewCalls); - assertEquals(1, testController.currentCallState.createViewCalls); + assertEquals(1, controllerCallState.createViewCalls); assertEquals(2, callState.attachCalls); - assertEquals(1, testController.currentCallState.attachCalls); + assertEquals(1, controllerCallState.attachCalls); assertEquals(2, callState.detachCalls); - assertEquals(1, testController.currentCallState.detachCalls); + assertEquals(1, controllerCallState.detachCalls); assertEquals(2, callState.destroyViewCalls); - assertEquals(1, testController.currentCallState.destroyViewCalls); + assertEquals(1, controllerCallState.destroyViewCalls); assertEquals(1, callState.destroyCalls); - assertEquals(0, testController.currentCallState.destroyCalls); + assertEquals(0, controllerCallState.destroyCalls); } @Override public void postDestroy(@NonNull Controller controller) { callState.destroyCalls++; assertEquals(2, callState.createViewCalls); - assertEquals(1, testController.currentCallState.createViewCalls); + assertEquals(1, controllerCallState.createViewCalls); assertEquals(2, callState.attachCalls); - assertEquals(1, testController.currentCallState.attachCalls); + assertEquals(1, controllerCallState.attachCalls); assertEquals(2, callState.detachCalls); - assertEquals(1, testController.currentCallState.detachCalls); + assertEquals(1, controllerCallState.detachCalls); assertEquals(2, callState.destroyViewCalls); - assertEquals(1, testController.currentCallState.destroyViewCalls); + assertEquals(1, controllerCallState.destroyViewCalls); assertEquals(2, callState.destroyCalls); - assertEquals(1, testController.currentCallState.destroyCalls); + assertEquals(1, controllerCallState.destroyCalls); } }); - router.pushController(RouterTransaction.with(testController) + router.pushController(RouterTransaction.with(controller) .pushChangeHandler(MockChangeHandler.defaultHandler()) .popChangeHandler(MockChangeHandler.defaultHandler())); - router.popController(testController); + router.popController(controller); assertEquals(2, callState.createViewCalls); assertEquals(2, callState.attachCalls); @@ -445,24 +609,17 @@ public void postDestroy(@NonNull Controller controller) { assertEquals(2, callState.destroyCalls); } - @Test - public void testChildLifecycle() { - Controller parent = new TestController(); - router.pushController(RouterTransaction.with(parent) - .pushChangeHandler(MockChangeHandler.defaultHandler())); - TestController child = new TestController(); + private void testChildLifecycle(Controller parent, Router childRouter, Controller child) { attachLifecycleListener(child); CallState expectedCallState = new CallState(false); assertCalls(expectedCallState, child); - Router childRouter = parent.getChildRouter((ViewGroup)parent.getView().findViewById(TestController.VIEW_ID)); - childRouter - .setRoot(RouterTransaction.with(child) - .pushChangeHandler(getPushHandler(expectedCallState, child)) - .popChangeHandler(getPopHandler(expectedCallState, child))); + childRouter.setRoot(RouterTransaction.with(child) + .pushChangeHandler(getPushHandler(expectedCallState, child)) + .popChangeHandler(getPopHandler(expectedCallState, child))); assertCalls(expectedCallState, child); @@ -471,25 +628,16 @@ public void testChildLifecycle() { assertCalls(expectedCallState, child); } - @Test - public void testChildLifecycle2() { - Controller parent = new TestController(); - router.pushController(RouterTransaction.with(parent) - .pushChangeHandler(MockChangeHandler.defaultHandler()) - .popChangeHandler(MockChangeHandler.defaultHandler())); - - TestController child = new TestController(); + private void testChildLifecycle2(Controller parent, Router childRouter, Controller child) { attachLifecycleListener(child); CallState expectedCallState = new CallState(false); assertCalls(expectedCallState, child); - Router childRouter = parent.getChildRouter((ViewGroup)parent.getView().findViewById(TestController.VIEW_ID)); - childRouter - .setRoot(RouterTransaction.with(child) - .pushChangeHandler(getPushHandler(expectedCallState, child)) - .popChangeHandler(getPopHandler(expectedCallState, child))); + childRouter.setRoot(RouterTransaction.with(child) + .pushChangeHandler(getPushHandler(expectedCallState, child)) + .popChangeHandler(getPopHandler(expectedCallState, child))); assertCalls(expectedCallState, child); @@ -503,21 +651,11 @@ public void testChildLifecycle2() { assertCalls(expectedCallState, child); } - @Test - public void testChildLifecycleOrderingAfterUnexpectedAttach() { - Controller parent = new TestController(); - parent.setRetainViewMode(RetainViewMode.RETAIN_DETACH); - router.pushController(RouterTransaction.with(parent) - .pushChangeHandler(MockChangeHandler.defaultHandler()) - .popChangeHandler(MockChangeHandler.defaultHandler())); - - TestController child = new TestController(); + private void testChildLifecycleOrderingAfterUnexpectedAttach(Controller parent, Router childRouter, Controller child) { child.setRetainViewMode(RetainViewMode.RETAIN_DETACH); - Router childRouter = parent.getChildRouter((ViewGroup)parent.getView().findViewById(TestController.VIEW_ID)); - childRouter - .setRoot(RouterTransaction.with(child) - .pushChangeHandler(new SimpleSwapChangeHandler()) - .popChangeHandler(new SimpleSwapChangeHandler())); + childRouter.setRoot(RouterTransaction.with(child) + .pushChangeHandler(new SimpleSwapChangeHandler()) + .popChangeHandler(new SimpleSwapChangeHandler())); assertTrue(parent.isAttached()); assertTrue(child.isAttached()); @@ -535,7 +673,7 @@ public void testChildLifecycleOrderingAfterUnexpectedAttach() { assertTrue(child.isAttached()); } - private MockChangeHandler getPushHandler(final CallState expectedCallState, final TestController controller) { + private MockChangeHandler getPushHandler(final CallState expectedCallState, final Controller controller) { return MockChangeHandler.listeningChangeHandler(new ChangeHandlerListener() { @Override public void willStartChange() { @@ -559,7 +697,7 @@ public void didEndChange() { }); } - private MockChangeHandler getPopHandler(final CallState expectedCallState, final TestController controller) { + private MockChangeHandler getPopHandler(final CallState expectedCallState, final Controller controller) { return MockChangeHandler.listeningChangeHandler(new ChangeHandlerListener() { @Override public void willStartChange() { @@ -584,8 +722,9 @@ public void didEndChange() { }); } - private void assertCalls(CallState callState, TestController controller) { - assertEquals("Expected call counts and controller call counts do not match.", callState, controller.currentCallState); + private void assertCalls(CallState callState, Controller controller) { + assertTrue(controller instanceof CallStateOwner); + assertEquals("Expected call counts and controller call counts do not match.", callState, ((CallStateOwner) controller).currentCallState()); assertEquals("Expected call counts and lifecycle call counts do not match.", callState, currentCallState); } diff --git a/conductor/src/test/java/com/bluelinelabs/conductor/ControllerTests.java b/conductor/src/test/java/com/bluelinelabs/conductor/ControllerTests.java index 25d51212..f3c9a71f 100644 --- a/conductor/src/test/java/com/bluelinelabs/conductor/ControllerTests.java +++ b/conductor/src/test/java/com/bluelinelabs/conductor/ControllerTests.java @@ -406,7 +406,7 @@ public void testRestoredChildRouterBackstack() { } private void assertCalls(CallState callState, TestController controller) { - assertEquals("Expected call counts and controller call counts do not match.", callState, controller.currentCallState); + assertEquals("Expected call counts and controller call counts do not match.", callState, controller.currentCallState()); } } diff --git a/conductor/src/test/java/com/bluelinelabs/conductor/ControllerTransactionTests.java b/conductor/src/test/java/com/bluelinelabs/conductor/ControllerTransactionTests.java index 8cced734..5dde9497 100644 --- a/conductor/src/test/java/com/bluelinelabs/conductor/ControllerTransactionTests.java +++ b/conductor/src/test/java/com/bluelinelabs/conductor/ControllerTransactionTests.java @@ -16,6 +16,7 @@ public class ControllerTransactionTests { public void testRouterSaveRestore() { RouterTransaction transaction = RouterTransaction.with(new TestController()) .pushChangeHandler(new HorizontalChangeHandler()) + .isDetail(true) .popChangeHandler(new VerticalChangeHandler()) .tag("Test Tag"); @@ -27,6 +28,7 @@ public void testRouterSaveRestore() { assertEquals(transaction.pushChangeHandler().getClass(), restoredTransaction.pushChangeHandler().getClass()); assertEquals(transaction.popChangeHandler().getClass(), restoredTransaction.popChangeHandler().getClass()); assertEquals(transaction.tag(), restoredTransaction.tag()); + assertEquals(transaction.isDetail(), restoredTransaction.isDetail()); } } diff --git a/conductor/src/test/java/com/bluelinelabs/conductor/MasterDetailControllerTests.java b/conductor/src/test/java/com/bluelinelabs/conductor/MasterDetailControllerTests.java new file mode 100644 index 00000000..62a15e64 --- /dev/null +++ b/conductor/src/test/java/com/bluelinelabs/conductor/MasterDetailControllerTests.java @@ -0,0 +1,597 @@ +package com.bluelinelabs.conductor; + +import android.app.Activity; +import android.content.Intent; +import android.os.Bundle; + +import com.bluelinelabs.conductor.util.ActivityProxy; +import com.bluelinelabs.conductor.util.CallState; +import com.bluelinelabs.conductor.util.CallStateOwner; +import com.bluelinelabs.conductor.util.TestController; +import com.bluelinelabs.conductor.util.TestMasterDetailController; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +@RunWith(RobolectricTestRunner.class) +@Config(manifest = Config.NONE) +public class MasterDetailControllerTests { + + private ActivityProxy activityProxy; + private Router router; + + private RouterTransaction masterTransaction1 = RouterTransaction.with(new TestController()); + private RouterTransaction masterTransaction2 = RouterTransaction.with(new TestController()).tag("Test1"); + private RouterTransaction masterTransaction3 = RouterTransaction.with(new TestController()).tag("Test2"); + + private RouterTransaction detailTransaction1 = RouterTransaction.with(new TestController()); + private RouterTransaction detailTransaction2 = RouterTransaction.with(new TestController()).tag("Test1"); + private RouterTransaction detailTransaction3 = RouterTransaction.with(new TestController()).tag("Test2"); + + public void createActivityController(Bundle savedInstanceState) { + activityProxy = new ActivityProxy().create(savedInstanceState).start().resume(); + router = Conductor.attachRouter(activityProxy.getActivity(), activityProxy.getView(), savedInstanceState); + } + + @Before + public void setup() { + createActivityController(null); + } + + + @Test + public void testActivityResult() { + TestMasterDetailController controller = new TestMasterDetailController(); + CallState expectedCallState = new CallState(true); + + router.pushController(RouterTransaction.with(controller)); + + // Ensure that calling onActivityResult w/o requesting a result doesn't do anything + router.onActivityResult(1, Activity.RESULT_OK, null); + assertCalls(expectedCallState, controller); + + // Ensure starting an activity for result gets us the result back + controller.startActivityForResult(new Intent("action"), 1); + router.onActivityResult(1, Activity.RESULT_OK, null); + expectedCallState.onActivityResultCalls++; + assertCalls(expectedCallState, controller); + + // Ensure requesting a result w/o calling startActivityForResult works + controller.registerForActivityResult(2); + router.onActivityResult(2, Activity.RESULT_OK, null); + expectedCallState.onActivityResultCalls++; + assertCalls(expectedCallState, controller); + } + + @Test + public void testActivityResultForMasterDetailPortrait() { + testActivityResultForMasterDetail(false); + } + + @Test + public void testActivityResultForMasterDetailLandscape() { + testActivityResultForMasterDetail(true); + } + + @Test + public void testPermissionResult() { + final String[] requestedPermissions = new String[] {"test"}; + + TestMasterDetailController controller = new TestMasterDetailController(); + CallState expectedCallState = new CallState(true); + + router.pushController(RouterTransaction.with(controller)); + + // Ensure that calling handleRequestedPermission w/o requesting a result doesn't do anything + router.onRequestPermissionsResult("anotherId", 1, requestedPermissions, new int[] {1}); + assertCalls(expectedCallState, controller); + + // Ensure requesting the permission gets us the result back + try { + controller.requestPermissions(requestedPermissions, 1); + } catch (NoSuchMethodError ignored) { } + + router.onRequestPermissionsResult(controller.getInstanceId(), 1, requestedPermissions, new int[] {1}); + expectedCallState.onRequestPermissionsResultCalls++; + assertCalls(expectedCallState, controller); + } + + @Test + public void testPermissionResultForMasterDetailPortrait() { + testPermissionResultForMasterDetail(false); + } + + @Test + public void testPermissionResultForMasterDetailLandscape() { + testPermissionResultForMasterDetail(true); + } + + @Test + public void testOptionsMenu() { + TestMasterDetailController controller = new TestMasterDetailController(); + CallState expectedCallState = new CallState(true); + + router.pushController(RouterTransaction.with(controller)); + + // Ensure that calling onCreateOptionsMenu w/o declaring that we have one doesn't do anything + router.onCreateOptionsMenu(null, null); + assertCalls(expectedCallState, controller); + + // Ensure calling onCreateOptionsMenu with a menu works + controller.setHasOptionsMenu(true); + + // Ensure it'll still get called back next time onCreateOptionsMenu is called + router.onCreateOptionsMenu(null, null); + expectedCallState.createOptionsMenuCalls++; + assertCalls(expectedCallState, controller); + + // Ensure we stop getting them when we hide it + controller.setOptionsMenuHidden(true); + router.onCreateOptionsMenu(null, null); + assertCalls(expectedCallState, controller); + + // Ensure we get the callback them when we un-hide it + controller.setOptionsMenuHidden(false); + router.onCreateOptionsMenu(null, null); + expectedCallState.createOptionsMenuCalls++; + assertCalls(expectedCallState, controller); + + // Ensure we don't get the callback when we no longer have a menu + controller.setHasOptionsMenu(false); + router.onCreateOptionsMenu(null, null); + assertCalls(expectedCallState, controller); + } + + @Test + public void testOptionsMenuForMasterDetailPortrait() { + testOptionsMenuForMasterDetail(false); + } + + @Test + public void testOptionsMenuForMasterDetailLandscape() { + testOptionsMenuForMasterDetail(true); + } + + @Test + public void testRestoreBackstacksOnRotate() { + TestMasterDetailController controller = new TestMasterDetailController(); + router.setRoot(RouterTransaction.with(controller).tag("root")); + assertFalse(controller.isTwoPanesMode()); + + TestController masterController = new TestController(); + controller.getMasterRouter().setRoot(RouterTransaction.with(masterController)); + + TestController masterController1 = new TestController(); + masterController.getRouter().pushController(RouterTransaction.with(masterController1)); + + TestController detailController = new TestController(); + controller.getDetailRouter().setRoot(RouterTransaction.with(detailController)); + + TestController detailController1 = new TestController(); + detailController.getRouter().pushController(RouterTransaction.with(detailController1)); + + assertEquals(2, controller.getMasterRouter().getBackstackSize()); + assertEquals(2, controller.getDetailRouter().getBackstackSize()); + + RouterTransaction masterTransaction = controller.getMasterRouter().getBackstack().get(0); + RouterTransaction masterTransaction1 = controller.getMasterRouter().getBackstack().get(1); + RouterTransaction detailTransaction = controller.getDetailRouter().getBackstack().get(0); + RouterTransaction detailTransaction1 = controller.getDetailRouter().getBackstack().get(1); + + assertEquals(masterController, masterTransaction.controller); + assertEquals(masterController1, masterTransaction1.controller); + assertEquals(detailController, detailTransaction.controller); + assertEquals(detailController1, detailTransaction1.controller); + + controller = rotateAndGetController("root"); + assertTrue(controller.isTwoPanesMode()); + + assertEquals(2, controller.getMasterRouter().getBackstackSize()); + assertEquals(2, controller.getDetailRouter().getBackstackSize()); + compareTransactions(masterTransaction, controller.getMasterRouter().getBackstack().get(0)); + compareTransactions(masterTransaction1, controller.getMasterRouter().getBackstack().get(1)); + compareTransactions(detailTransaction, controller.getDetailRouter().getBackstack().get(0)); + compareTransactions(detailTransaction1, controller.getDetailRouter().getBackstack().get(1)); + } + + @Test + public void testPopCurrentController() { + TestMasterDetailController controller = prepareController("root"); + controller.getMasterRouter().popCurrentController(); + controller.getDetailRouter().popCurrentController(); + + assertEquals(2, controller.getMasterRouter().getBackstackSize()); + assertEquals(2, controller.getDetailRouter().getBackstackSize()); + assertEquals(masterTransaction1, controller.getMasterRouter().getBackstack().get(0)); + assertEquals(masterTransaction2, controller.getMasterRouter().getBackstack().get(1)); + assertEquals(detailTransaction1, controller.getDetailRouter().getBackstack().get(0)); + assertEquals(detailTransaction2, controller.getDetailRouter().getBackstack().get(1)); + } + + @Test + public void testPopToRootController() { + TestMasterDetailController controller = prepareController("root"); + controller.getMasterRouter().popToRoot(); + controller.getDetailRouter().popToRoot(); + + assertEquals(1, controller.getMasterRouter().getBackstackSize()); + assertEquals(1, controller.getDetailRouter().getBackstackSize()); + assertEquals(masterTransaction1, controller.getMasterRouter().getBackstack().get(0)); + assertEquals(detailTransaction1, controller.getDetailRouter().getBackstack().get(0)); + } + + @Test + public void testPopToTagController() { + TestMasterDetailController controller = prepareController("root"); + controller.getMasterRouter().popToTag("Test1"); + controller.getDetailRouter().popToTag("Test1"); + + assertEquals(2, controller.getMasterRouter().getBackstackSize()); + assertEquals(2, controller.getDetailRouter().getBackstackSize()); + assertEquals(masterTransaction1, controller.getMasterRouter().getBackstack().get(0)); + assertEquals(masterTransaction2, controller.getMasterRouter().getBackstack().get(1)); + assertEquals(detailTransaction1, controller.getDetailRouter().getBackstack().get(0)); + assertEquals(detailTransaction2, controller.getDetailRouter().getBackstack().get(1)); + } + + @Test + public void testPushController() { + TestMasterDetailController controller = prepareController("root"); + RouterTransaction masterTransaction4 = RouterTransaction.with(new TestController()); + RouterTransaction detailTransaction4 = RouterTransaction.with(new TestController()); + + controller.getMasterRouter().pushController(masterTransaction4); + controller.getDetailRouter().pushController(detailTransaction4); + + assertEquals(4, controller.getMasterRouter().getBackstackSize()); + assertEquals(4, controller.getDetailRouter().getBackstackSize()); + assertEquals(masterTransaction1, controller.getMasterRouter().getBackstack().get(0)); + assertEquals(masterTransaction2, controller.getMasterRouter().getBackstack().get(1)); + assertEquals(masterTransaction3, controller.getMasterRouter().getBackstack().get(2)); + assertEquals(masterTransaction4, controller.getMasterRouter().getBackstack().get(3)); + assertEquals(detailTransaction1, controller.getDetailRouter().getBackstack().get(0)); + assertEquals(detailTransaction2, controller.getDetailRouter().getBackstack().get(1)); + assertEquals(detailTransaction3, controller.getDetailRouter().getBackstack().get(2)); + assertEquals(detailTransaction4, controller.getDetailRouter().getBackstack().get(3)); + } + + @Test + public void testReplaceTopController() { + TestMasterDetailController controller = prepareController("root"); + RouterTransaction masterTransaction4 = RouterTransaction.with(new TestController()); + RouterTransaction detailTransaction4 = RouterTransaction.with(new TestController()); + + controller.getMasterRouter().replaceTopController(masterTransaction4); + controller.getDetailRouter().replaceTopController(detailTransaction4); + + assertEquals(3, controller.getMasterRouter().getBackstackSize()); + assertEquals(3, controller.getDetailRouter().getBackstackSize()); + assertEquals(masterTransaction1, controller.getMasterRouter().getBackstack().get(0)); + assertEquals(masterTransaction2, controller.getMasterRouter().getBackstack().get(1)); + assertEquals(masterTransaction4, controller.getMasterRouter().getBackstack().get(2)); + assertEquals(detailTransaction1, controller.getDetailRouter().getBackstack().get(0)); + assertEquals(detailTransaction2, controller.getDetailRouter().getBackstack().get(1)); + assertEquals(detailTransaction4, controller.getDetailRouter().getBackstack().get(2)); + } + + @Test + public void testSetRoot() { + TestMasterDetailController controller = prepareController("root"); + RouterTransaction masterTransaction4 = RouterTransaction.with(new TestController()); + RouterTransaction detailTransaction4 = RouterTransaction.with(new TestController()); + + controller.getMasterRouter().setRoot(masterTransaction4); + assertEquals(1, controller.getMasterRouter().getBackstackSize()); + assertEquals(3, controller.getDetailRouter().getBackstackSize()); + assertEquals(masterTransaction4, controller.getMasterRouter().getBackstack().get(0)); + assertEquals(detailTransaction1, controller.getDetailRouter().getBackstack().get(0)); + assertEquals(detailTransaction2, controller.getDetailRouter().getBackstack().get(1)); + assertEquals(detailTransaction3, controller.getDetailRouter().getBackstack().get(2)); + + controller.getDetailRouter().setRoot(detailTransaction4); + assertEquals(1, controller.getMasterRouter().getBackstackSize()); + assertEquals(1, controller.getDetailRouter().getBackstackSize()); + assertEquals(masterTransaction4, controller.getMasterRouter().getBackstack().get(0)); + assertEquals(detailTransaction4.controller, controller.getDetailRouter().getBackstack().get(0).controller); + } + + @Test + public void testPopToRootFromMaster() { + TestMasterDetailController controller = prepareController("root"); + Controller masterController4 = new TestController(); + RouterTransaction masterTransaction4 = RouterTransaction.with(masterController4); + controller.getMasterRouter().pushController(masterTransaction4); + + assertEquals(4, controller.getMasterRouter().getBackstackSize()); + assertEquals(3, controller.getDetailRouter().getBackstackSize()); + assertEquals(masterTransaction1, controller.getMasterRouter().getBackstack().get(0)); + assertEquals(masterTransaction2, controller.getMasterRouter().getBackstack().get(1)); + assertEquals(masterTransaction3, controller.getMasterRouter().getBackstack().get(2)); + assertEquals(masterTransaction4, controller.getMasterRouter().getBackstack().get(3)); + assertEquals(detailTransaction1, controller.getDetailRouter().getBackstack().get(0)); + assertEquals(detailTransaction2, controller.getDetailRouter().getBackstack().get(1)); + assertEquals(detailTransaction3, controller.getDetailRouter().getBackstack().get(2)); + + masterController4.getRouter().popToRoot(); + assertEquals(1, controller.getMasterRouter().getBackstackSize()); + assertEquals(3, controller.getDetailRouter().getBackstackSize()); + assertEquals(masterTransaction1, controller.getMasterRouter().getBackstack().get(0)); + assertEquals(detailTransaction1, controller.getDetailRouter().getBackstack().get(0)); + assertEquals(detailTransaction2, controller.getDetailRouter().getBackstack().get(1)); + assertEquals(detailTransaction3, controller.getDetailRouter().getBackstack().get(2)); + } + + @Test + public void testPopToRootFromDetail() { + TestMasterDetailController controller = prepareController("root"); + Controller detailController4 = new TestController(); + RouterTransaction detailTransaction4 = RouterTransaction.with(detailController4); + controller.getDetailRouter().pushController(detailTransaction4); + + assertEquals(3, controller.getMasterRouter().getBackstackSize()); + assertEquals(4, controller.getDetailRouter().getBackstackSize()); + assertEquals(masterTransaction1, controller.getMasterRouter().getBackstack().get(0)); + assertEquals(masterTransaction2, controller.getMasterRouter().getBackstack().get(1)); + assertEquals(masterTransaction3, controller.getMasterRouter().getBackstack().get(2)); + assertEquals(detailTransaction1, controller.getDetailRouter().getBackstack().get(0)); + assertEquals(detailTransaction2, controller.getDetailRouter().getBackstack().get(1)); + assertEquals(detailTransaction3, controller.getDetailRouter().getBackstack().get(2)); + assertEquals(detailTransaction4, controller.getDetailRouter().getBackstack().get(3)); + + detailController4.getRouter().popToRoot(); + assertEquals(3, controller.getMasterRouter().getBackstackSize()); + assertEquals(1, controller.getDetailRouter().getBackstackSize()); + assertEquals(masterTransaction1, controller.getMasterRouter().getBackstack().get(0)); + assertEquals(masterTransaction2, controller.getMasterRouter().getBackstack().get(1)); + assertEquals(masterTransaction3, controller.getMasterRouter().getBackstack().get(2)); + assertEquals(detailTransaction1, controller.getDetailRouter().getBackstack().get(0)); + } + + @Test + public void testControllersPushedToProperBackstacks() { + TestMasterDetailController controller = prepareController("root"); + assertFalse(controller.isTwoPanesMode()); + + Controller masterController4 = new TestController(); + RouterTransaction masterTransaction4 = RouterTransaction.with(masterController4); + Controller masterController3 = controller.getMasterRouter().getBackstack().get(2).controller; + masterController3.getRouter().pushController(masterTransaction4); + + Controller detailController4 = new TestController(); + RouterTransaction detailTransaction4 = RouterTransaction.with(detailController4); + Controller detailController3 = controller.getDetailRouter().getBackstack().get(2).controller; + detailController3.getRouter().pushController(detailTransaction4); + + assertEquals(4, controller.getMasterRouter().getBackstackSize()); + assertEquals(4, controller.getDetailRouter().getBackstackSize()); + assertEquals(masterTransaction1, controller.getMasterRouter().getBackstack().get(0)); + assertEquals(masterTransaction2, controller.getMasterRouter().getBackstack().get(1)); + assertEquals(masterTransaction3, controller.getMasterRouter().getBackstack().get(2)); + assertEquals(masterTransaction4, controller.getMasterRouter().getBackstack().get(3)); + assertEquals(detailTransaction1, controller.getDetailRouter().getBackstack().get(0)); + assertEquals(detailTransaction2, controller.getDetailRouter().getBackstack().get(1)); + assertEquals(detailTransaction3, controller.getDetailRouter().getBackstack().get(2)); + assertEquals(detailTransaction4, controller.getDetailRouter().getBackstack().get(3)); + + controller = rotateAndGetController("root"); + assertTrue(controller.isTwoPanesMode()); + assertEquals(4, controller.getMasterRouter().getBackstackSize()); + assertEquals(4, controller.getDetailRouter().getBackstackSize()); + compareTransactions(masterTransaction1, controller.getMasterRouter().getBackstack().get(0)); + compareTransactions(masterTransaction2, controller.getMasterRouter().getBackstack().get(1)); + compareTransactions(masterTransaction3, controller.getMasterRouter().getBackstack().get(2)); + compareTransactions(masterTransaction4, controller.getMasterRouter().getBackstack().get(3)); + compareTransactions(detailTransaction1, controller.getDetailRouter().getBackstack().get(0)); + compareTransactions(detailTransaction2, controller.getDetailRouter().getBackstack().get(1)); + compareTransactions(detailTransaction3, controller.getDetailRouter().getBackstack().get(2)); + compareTransactions(detailTransaction4, controller.getDetailRouter().getBackstack().get(3)); + } + + private TestMasterDetailController prepareController(String tag) { + TestMasterDetailController controller = new TestMasterDetailController(); + router.setRoot(RouterTransaction.with(controller).tag(tag)); + assertFalse(controller.isTwoPanesMode()); + + controller.getMasterRouter().pushController(masterTransaction1); + controller.getMasterRouter().pushController(masterTransaction2); + controller.getMasterRouter().pushController(masterTransaction3); + controller.getDetailRouter().pushController(detailTransaction1); + controller.getDetailRouter().pushController(detailTransaction2); + controller.getDetailRouter().pushController(detailTransaction3); + + return controller; + } + + private TestMasterDetailController rotateAndGetController(String tag) { + Bundle bundle = new Bundle(); + activityProxy.saveInstanceState(bundle); + activityProxy.stop(true); + activityProxy.rotate(); + + router = Conductor.attachRouter(activityProxy.getActivity(), activityProxy.getView(), bundle); + Controller controller = router.getControllerWithTag(tag); + assertTrue(controller instanceof TestMasterDetailController); + + return (TestMasterDetailController)controller; + } + + private void compareTransactions(RouterTransaction transaction1, RouterTransaction transaction2) { + assertEquals(transaction1.transactionIndex, transaction2.transactionIndex); + assertEquals(transaction1.isDetail(), transaction2.isDetail()); + assertEquals(transaction1.tag(), transaction2.tag()); + assertEquals(transaction1.controller.getInstanceId(), transaction2.controller.getInstanceId()); + } + + private void testActivityResultForMasterDetail(boolean isLandscape) { + if (isLandscape) { + activityProxy.rotate(); + } + + TestMasterDetailController controller = new TestMasterDetailController(); + router.setRoot(RouterTransaction.with(controller)); + + assertEquals(isLandscape, controller.isTwoPanesMode()); + + TestController masterController = new TestController(); + controller.getMasterRouter().setRoot(RouterTransaction.with(masterController)); + testActivityResultForChildController(masterController); + + TestController masterController1 = new TestController(); + masterController.getRouter().pushController(RouterTransaction.with(masterController1)); + testActivityResultForChildController(masterController1); + + TestController detailController = new TestController(); + controller.getDetailRouter().setRoot(RouterTransaction.with(detailController)); + testActivityResultForChildController(detailController); + + TestController detailController1 = new TestController(); + detailController.getRouter().pushController(RouterTransaction.with(detailController1)); + testActivityResultForChildController(detailController1); + } + + private void testActivityResultForChildController(TestController controller) { + CallState expectedCallState = new CallState(true); + + // Ensure that calling onActivityResult w/o requesting a result doesn't do anything + router.onActivityResult(1, Activity.RESULT_OK, null); + assertCalls(expectedCallState, controller); + + // Ensure starting an activity for result gets us the result back + controller.startActivityForResult(new Intent("action"), 1); + router.onActivityResult(1, Activity.RESULT_OK, null); + expectedCallState.onActivityResultCalls++; + assertCalls(expectedCallState, controller); + + // Ensure requesting a result w/o calling startActivityForResult works + controller.registerForActivityResult(2); + router.onActivityResult(2, Activity.RESULT_OK, null); + expectedCallState.onActivityResultCalls++; + assertCalls(expectedCallState, controller); + } + + private void testPermissionResultForMasterDetail(boolean isLandscape) { + if (isLandscape) { + activityProxy.rotate(); + } + + TestMasterDetailController controller = new TestMasterDetailController(); + router.setRoot(RouterTransaction.with(controller)); + + assertEquals(isLandscape, controller.isTwoPanesMode()); + + TestController masterController = new TestController(); + controller.getMasterRouter().setRoot(RouterTransaction.with(masterController)); + testPermissionResultForChildController(controller, masterController); + + TestController masterController1 = new TestController(); + masterController.getRouter().pushController(RouterTransaction.with(masterController1)); + testPermissionResultForChildController(controller, masterController1); + + TestController detailController = new TestController(); + controller.getDetailRouter().setRoot(RouterTransaction.with(detailController)); + testPermissionResultForChildController(controller, detailController); + + TestController detailController1 = new TestController(); + detailController.getRouter().pushController(RouterTransaction.with(detailController1)); + testPermissionResultForChildController(controller, detailController1); + } + + private void testPermissionResultForChildController(TestMasterDetailController parent, TestController child) { + final String[] requestedPermissions = new String[] {"test"}; + + CallState childExpectedCallState = new CallState(true); + CallState parentExpectedCallState = new CallState(true); + + // Ensure that calling handleRequestedPermission w/o requesting a result doesn't do anything + router.onRequestPermissionsResult("anotherId", 1, requestedPermissions, new int[] {1}); + assertCalls(childExpectedCallState, child); + assertCalls(parentExpectedCallState, parent); + + // Ensure requesting the permission gets us the result back + try { + child.requestPermissions(requestedPermissions, 1); + } catch (NoSuchMethodError ignored) { } + + router.onRequestPermissionsResult(child.getInstanceId(), 1, requestedPermissions, new int[] {1}); + childExpectedCallState.onRequestPermissionsResultCalls++; + assertCalls(childExpectedCallState, child); + assertCalls(parentExpectedCallState, parent); + } + + private void testOptionsMenuForMasterDetail(boolean isLandscape) { + if (isLandscape) { + activityProxy.rotate(); + } + + TestMasterDetailController controller = new TestMasterDetailController(); + router.setRoot(RouterTransaction.with(controller)); + + assertEquals(isLandscape, controller.isTwoPanesMode()); + + TestController masterController = new TestController(); + controller.getMasterRouter().setRoot(RouterTransaction.with(masterController)); + testOptionsMenuForChildController(controller, masterController); + + TestController masterController1 = new TestController(); + masterController.getRouter().pushController(RouterTransaction.with(masterController1)); + testOptionsMenuForChildController(controller, masterController1); + + TestController detailController = new TestController(); + controller.getDetailRouter().setRoot(RouterTransaction.with(detailController)); + testOptionsMenuForChildController(controller, detailController); + + TestController detailController1 = new TestController(); + detailController.getRouter().pushController(RouterTransaction.with(detailController1)); + testOptionsMenuForChildController(controller, detailController1); + } + + private void testOptionsMenuForChildController(TestMasterDetailController parent, TestController child) { + CallState childExpectedCallState = new CallState(true); + CallState parentExpectedCallState = new CallState(true); + + // Ensure that calling onCreateOptionsMenu w/o declaring that we have one doesn't do anything + router.onCreateOptionsMenu(null, null); + assertCalls(childExpectedCallState, child); + assertCalls(parentExpectedCallState, parent); + + // Ensure calling onCreateOptionsMenu with a menu works + child.setHasOptionsMenu(true); + + // Ensure it'll still get called back next time onCreateOptionsMenu is called + router.onCreateOptionsMenu(null, null); + childExpectedCallState.createOptionsMenuCalls++; + assertCalls(childExpectedCallState, child); + assertCalls(parentExpectedCallState, parent); + + // Ensure we stop getting them when we hide it + child.setOptionsMenuHidden(true); + router.onCreateOptionsMenu(null, null); + assertCalls(childExpectedCallState, child); + assertCalls(parentExpectedCallState, parent); + + // Ensure we get the callback them when we un-hide it + child.setOptionsMenuHidden(false); + router.onCreateOptionsMenu(null, null); + childExpectedCallState.createOptionsMenuCalls++; + assertCalls(childExpectedCallState, child); + assertCalls(parentExpectedCallState, parent); + + // Ensure we don't get the callback when we no longer have a menu + child.setHasOptionsMenu(false); + router.onCreateOptionsMenu(null, null); + assertCalls(childExpectedCallState, child); + assertCalls(parentExpectedCallState, parent); + } + + private void assertCalls(CallState callState, CallStateOwner controller) { + assertEquals("Expected call counts and controller call counts do not match.", callState, controller.currentCallState()); + } + +} diff --git a/conductor/src/test/java/com/bluelinelabs/conductor/ReattachCaseTests.java b/conductor/src/test/java/com/bluelinelabs/conductor/ReattachCaseTests.java index 88c76fb1..fc68fb6c 100644 --- a/conductor/src/test/java/com/bluelinelabs/conductor/ReattachCaseTests.java +++ b/conductor/src/test/java/com/bluelinelabs/conductor/ReattachCaseTests.java @@ -6,7 +6,7 @@ import com.bluelinelabs.conductor.util.ActivityProxy; import com.bluelinelabs.conductor.util.MockChangeHandler; import com.bluelinelabs.conductor.util.TestController; -import com.bluelinelabs.conductor.util.ViewUtils; +import com.bluelinelabs.conductor.util.TestMasterDetailController; import org.junit.Before; import org.junit.Test; @@ -110,6 +110,59 @@ public void testChildNeedsAttachOnPauseAndOrientation() { assertTrue(controllerB.isAttached()); } + @Test + public void testMasterDetailNeedsAttachOnPauseAndOrientation() { + final TestMasterDetailController controllerA = new TestMasterDetailController(); + final Controller childController1 = new TestController(); + final Controller childController2 = new TestController(); + final Controller controllerB = new TestController(); + + router.pushController(RouterTransaction.with(controllerA) + .pushChangeHandler(MockChangeHandler.defaultHandler()) + .popChangeHandler(MockChangeHandler.defaultHandler())); + + Router masterRouter = controllerA.getMasterRouter(); + masterRouter.pushController(RouterTransaction.with(childController1) + .pushChangeHandler(MockChangeHandler.defaultHandler()) + .popChangeHandler(MockChangeHandler.defaultHandler())); + + Router detailRouter = controllerA.getDetailRouter(); + detailRouter.pushController(RouterTransaction.with(childController2) + .pushChangeHandler(MockChangeHandler.defaultHandler()) + .popChangeHandler(MockChangeHandler.defaultHandler())); + + assertTrue(controllerA.isAttached()); + assertFalse(childController1.isAttached()); + assertTrue(childController2.isAttached()); + assertFalse(controllerB.isAttached()); + + sleepWakeDevice(); + + assertTrue(controllerA.isAttached()); + assertFalse(childController1.isAttached()); + assertTrue(childController2.isAttached()); + assertFalse(controllerB.isAttached()); + + router.pushController(RouterTransaction.with(controllerB) + .pushChangeHandler(MockChangeHandler.defaultHandler()) + .popChangeHandler(MockChangeHandler.defaultHandler())); + + assertFalse(controllerA.isAttached()); + assertFalse(childController1.isAttached()); + assertFalse(childController2.isAttached()); + assertTrue(controllerB.isAttached()); + + activityProxy.rotate(); + router.rebindIfNeeded(); + + assertFalse(controllerA.isAttached()); + assertFalse(childController1.isAttached()); + assertFalse(childController1.getNeedsAttach()); + assertFalse(childController2.isAttached()); + assertTrue(childController2.getNeedsAttach()); + assertTrue(controllerB.isAttached()); + } + @Test public void testChildHandleBackOnOrientation() { final TestController controllerA = new TestController(); @@ -158,6 +211,87 @@ public void testChildHandleBackOnOrientation() { assertFalse(childController.isAttached()); } + @Test + public void testMasterDetailHandleBackOnOrientation() { + TestController controllerA = new TestController(); + TestMasterDetailController controllerB = new TestMasterDetailController(); + TestController childController1 = new TestController(); + TestController childController2 = new TestController(); + + router.pushController(RouterTransaction.with(controllerA) + .tag("ControllerA") + .pushChangeHandler(MockChangeHandler.defaultHandler()) + .popChangeHandler(MockChangeHandler.defaultHandler())); + + assertTrue(controllerA.isAttached()); + assertFalse(controllerB.isAttached()); + assertFalse(childController1.isAttached()); + assertFalse(childController2.isAttached()); + + router.pushController(RouterTransaction.with(controllerB) + .tag("ControllerB") + .pushChangeHandler(MockChangeHandler.defaultHandler()) + .popChangeHandler(MockChangeHandler.defaultHandler())); + assertFalse(controllerB.isTwoPanesMode()); + + Router masterRouter = controllerB.getMasterRouter(); + masterRouter.setPopsLastView(true); + masterRouter.pushController(RouterTransaction.with(childController1) + .tag("Controller1") + .pushChangeHandler(MockChangeHandler.defaultHandler()) + .popChangeHandler(MockChangeHandler.defaultHandler())); + + Router detailRouter = controllerB.getDetailRouter(); + detailRouter.setPopsLastView(true); + detailRouter.pushController(RouterTransaction.with(childController2) + .tag("Controller2") + .pushChangeHandler(MockChangeHandler.defaultHandler()) + .popChangeHandler(MockChangeHandler.defaultHandler())); + + assertFalse(controllerA.isAttached()); + assertTrue(controllerB.isAttached()); + assertFalse(childController1.isAttached()); + assertTrue(childController2.isAttached()); + + Bundle bundle = new Bundle(); + activityProxy.saveInstanceState(bundle); + activityProxy.stop(true); + activityProxy.rotate(); + + router = Conductor.attachRouter(activityProxy.getActivity(), activityProxy.getView(), bundle); + controllerA = (TestController) router.getControllerWithTag("ControllerA"); + controllerB = (TestMasterDetailController) router.getControllerWithTag("ControllerB"); + childController1 = (TestController) controllerB.getMasterRouter().getControllerWithTag("Controller1"); + childController2 = (TestController) controllerB.getDetailRouter().getControllerWithTag("Controller2"); + assertTrue(controllerB.isTwoPanesMode()); + + activityProxy.start().resume(); + assertFalse(controllerA.isAttached()); + assertTrue(controllerB.isAttached()); + assertTrue(childController1.isAttached()); + assertTrue(childController2.isAttached()); + + router.handleBack(); + + assertFalse(controllerA.isAttached()); + assertTrue(controllerB.isAttached()); + assertTrue(childController1.isAttached()); + assertFalse(childController2.isAttached()); + + router.handleBack(); + + assertFalse(controllerA.isAttached()); + assertTrue(controllerB.isAttached()); + assertFalse(childController1.isAttached()); + assertFalse(childController2.isAttached()); + + router.handleBack(); + assertTrue(controllerA.isAttached()); + assertFalse(controllerB.isAttached()); + assertFalse(childController1.isAttached()); + assertFalse(childController2.isAttached()); + } + // Attempt to test https://github.com/bluelinelabs/Conductor/issues/86#issuecomment-231381271 @Test public void testReusedChildRouterHandleBackOnOrientation() { @@ -233,6 +367,84 @@ public void testReusedChildRouterHandleBackOnOrientation() { assertFalse(childController.isAttached()); } + @Test + public void testReusedDetailRouterHandleBackOnOrientation() { + TestController controllerA = new TestController(); + TestMasterDetailController controllerB = new TestMasterDetailController(); + TestController childController = new TestController(); + + router.pushController(RouterTransaction.with(controllerA) + .pushChangeHandler(MockChangeHandler.defaultHandler()) + .popChangeHandler(MockChangeHandler.defaultHandler())); + + assertTrue(controllerA.isAttached()); + assertFalse(controllerB.isAttached()); + assertFalse(childController.isAttached()); + + router.pushController(RouterTransaction.with(controllerB) + .pushChangeHandler(MockChangeHandler.defaultHandler()) + .popChangeHandler(MockChangeHandler.defaultHandler())); + + Router masterRouter = controllerB.getMasterRouter(); + masterRouter.setPopsLastView(true); + + Router childRouter = controllerB.getDetailRouter(); + childRouter.setPopsLastView(true); + childRouter.pushController(RouterTransaction.with(childController) + .pushChangeHandler(MockChangeHandler.defaultHandler()) + .popChangeHandler(MockChangeHandler.defaultHandler())); + + assertFalse(controllerA.isAttached()); + assertTrue(controllerB.isAttached()); + assertTrue(childController.isAttached()); + + router.handleBack(); + + assertFalse(controllerA.isAttached()); + assertTrue(controllerB.isAttached()); + assertFalse(childController.isAttached()); + + childController = new TestController(); + childRouter.pushController(RouterTransaction.with(childController) + .pushChangeHandler(MockChangeHandler.defaultHandler()) + .popChangeHandler(MockChangeHandler.defaultHandler())); + + assertFalse(controllerA.isAttached()); + assertTrue(controllerB.isAttached()); + assertTrue(childController.isAttached()); + + activityProxy.rotate(); + router.rebindIfNeeded(); + + assertFalse(controllerA.isAttached()); + assertTrue(controllerB.isAttached()); + assertTrue(childController.isAttached()); + + router.handleBack(); + + childController = new TestController(); + childRouter.pushController(RouterTransaction.with(childController) + .pushChangeHandler(MockChangeHandler.defaultHandler()) + .popChangeHandler(MockChangeHandler.defaultHandler())); + + assertFalse(controllerA.isAttached()); + assertTrue(controllerB.isAttached()); + assertTrue(childController.isAttached()); + + router.handleBack(); + + assertFalse(controllerA.isAttached()); + assertTrue(controllerB.isAttached()); + assertFalse(childController.isAttached()); + + router.handleBack(); + + assertTrue(controllerA.isAttached()); + assertFalse(controllerB.isAttached()); + assertFalse(childController.isAttached()); + } + + // Attempt to test https://github.com/bluelinelabs/Conductor/issues/367 @Test public void testViewIsAttachedAfterStartedActivityIsRecreated() { diff --git a/conductor/src/test/java/com/bluelinelabs/conductor/RouterChangeHandlerTests.java b/conductor/src/test/java/com/bluelinelabs/conductor/RouterChangeHandlerTests.java index 373fa700..8013f8d2 100644 --- a/conductor/src/test/java/com/bluelinelabs/conductor/RouterChangeHandlerTests.java +++ b/conductor/src/test/java/com/bluelinelabs/conductor/RouterChangeHandlerTests.java @@ -39,12 +39,12 @@ public void testSetRootHandler() { TestController rootController = new TestController(); router.setRoot(RouterTransaction.with(rootController).pushChangeHandler(handler)); - assertTrue(rootController.changeHandlerHistory.isValidHistory); - assertNull(rootController.changeHandlerHistory.latestFromView()); - assertNotNull(rootController.changeHandlerHistory.latestToView()); - assertEquals(rootController.getView(), rootController.changeHandlerHistory.latestToView()); - assertTrue(rootController.changeHandlerHistory.latestIsPush()); - assertEquals(handler.tag, rootController.changeHandlerHistory.latestChangeHandler().tag); + assertTrue(rootController.changeHandlerHistory().isValidHistory); + assertNull(rootController.changeHandlerHistory().latestFromView()); + assertNotNull(rootController.changeHandlerHistory().latestToView()); + assertEquals(rootController.getView(), rootController.changeHandlerHistory().latestToView()); + assertTrue(rootController.changeHandlerHistory().latestIsPush()); + assertEquals(handler.tag, rootController.changeHandlerHistory().latestChangeHandler().tag); } @Test @@ -58,25 +58,25 @@ public void testPushPopHandlers() { TestController pushController = new TestController(); router.pushController(RouterTransaction.with(pushController).pushChangeHandler(pushHandler).popChangeHandler(popHandler)); - assertTrue(rootController.changeHandlerHistory.isValidHistory); - assertTrue(pushController.changeHandlerHistory.isValidHistory); + assertTrue(rootController.changeHandlerHistory().isValidHistory); + assertTrue(pushController.changeHandlerHistory().isValidHistory); - assertNotNull(pushController.changeHandlerHistory.latestFromView()); - assertNotNull(pushController.changeHandlerHistory.latestToView()); - assertEquals(rootView, pushController.changeHandlerHistory.latestFromView()); - assertEquals(pushController.getView(), pushController.changeHandlerHistory.latestToView()); - assertTrue(pushController.changeHandlerHistory.latestIsPush()); - assertEquals(pushHandler.tag, pushController.changeHandlerHistory.latestChangeHandler().tag); + assertNotNull(pushController.changeHandlerHistory().latestFromView()); + assertNotNull(pushController.changeHandlerHistory().latestToView()); + assertEquals(rootView, pushController.changeHandlerHistory().latestFromView()); + assertEquals(pushController.getView(), pushController.changeHandlerHistory().latestToView()); + assertTrue(pushController.changeHandlerHistory().latestIsPush()); + assertEquals(pushHandler.tag, pushController.changeHandlerHistory().latestChangeHandler().tag); View pushView = pushController.getView(); router.popController(pushController); - assertNotNull(pushController.changeHandlerHistory.latestFromView()); - assertNotNull(pushController.changeHandlerHistory.latestToView()); - assertEquals(pushView, pushController.changeHandlerHistory.fromViewAt(1)); - assertEquals(rootController.getView(), pushController.changeHandlerHistory.latestToView()); - assertFalse(pushController.changeHandlerHistory.latestIsPush()); - assertEquals(popHandler.tag, pushController.changeHandlerHistory.latestChangeHandler().tag); + assertNotNull(pushController.changeHandlerHistory().latestFromView()); + assertNotNull(pushController.changeHandlerHistory().latestToView()); + assertEquals(pushView, pushController.changeHandlerHistory().fromViewAt(1)); + assertEquals(rootController.getView(), pushController.changeHandlerHistory().latestToView()); + assertFalse(pushController.changeHandlerHistory().latestIsPush()); + assertEquals(popHandler.tag, pushController.changeHandlerHistory().latestChangeHandler().tag); } @Test @@ -98,30 +98,30 @@ public void testResetRootHandlers() { router.setRoot(RouterTransaction.with(newRootController).pushChangeHandler(newRootHandler)); - assertTrue(initialController1.changeHandlerHistory.isValidHistory); - assertTrue(initialController2.changeHandlerHistory.isValidHistory); - assertTrue(newRootController.changeHandlerHistory.isValidHistory); - - assertEquals(3, initialController1.changeHandlerHistory.size()); - assertEquals(2, initialController2.changeHandlerHistory.size()); - assertEquals(1, newRootController.changeHandlerHistory.size()); - - assertNotNull(initialController1.changeHandlerHistory.latestToView()); - assertEquals(newRootController.getView(), initialController1.changeHandlerHistory.latestToView()); - assertEquals(initialView1, initialController1.changeHandlerHistory.latestFromView()); - assertEquals(newRootHandler.tag, initialController1.changeHandlerHistory.latestChangeHandler().tag); - assertTrue(initialController1.changeHandlerHistory.latestIsPush()); - - assertNull(initialController2.changeHandlerHistory.latestToView()); - assertEquals(initialView2, initialController2.changeHandlerHistory.latestFromView()); - assertEquals(newRootHandler.tag, initialController2.changeHandlerHistory.latestChangeHandler().tag); - assertTrue(initialController2.changeHandlerHistory.latestIsPush()); - - assertNotNull(newRootController.changeHandlerHistory.latestToView()); - assertEquals(newRootController.getView(), newRootController.changeHandlerHistory.latestToView()); - assertEquals(initialView1, newRootController.changeHandlerHistory.latestFromView()); - assertEquals(newRootHandler.tag, newRootController.changeHandlerHistory.latestChangeHandler().tag); - assertTrue(newRootController.changeHandlerHistory.latestIsPush()); + assertTrue(initialController1.changeHandlerHistory().isValidHistory); + assertTrue(initialController2.changeHandlerHistory().isValidHistory); + assertTrue(newRootController.changeHandlerHistory().isValidHistory); + + assertEquals(3, initialController1.changeHandlerHistory().size()); + assertEquals(2, initialController2.changeHandlerHistory().size()); + assertEquals(1, newRootController.changeHandlerHistory().size()); + + assertNotNull(initialController1.changeHandlerHistory().latestToView()); + assertEquals(newRootController.getView(), initialController1.changeHandlerHistory().latestToView()); + assertEquals(initialView1, initialController1.changeHandlerHistory().latestFromView()); + assertEquals(newRootHandler.tag, initialController1.changeHandlerHistory().latestChangeHandler().tag); + assertTrue(initialController1.changeHandlerHistory().latestIsPush()); + + assertNull(initialController2.changeHandlerHistory().latestToView()); + assertEquals(initialView2, initialController2.changeHandlerHistory().latestFromView()); + assertEquals(newRootHandler.tag, initialController2.changeHandlerHistory().latestChangeHandler().tag); + assertTrue(initialController2.changeHandlerHistory().latestIsPush()); + + assertNotNull(newRootController.changeHandlerHistory().latestToView()); + assertEquals(newRootController.getView(), newRootController.changeHandlerHistory().latestToView()); + assertEquals(initialView1, newRootController.changeHandlerHistory().latestFromView()); + assertEquals(newRootHandler.tag, newRootController.changeHandlerHistory().latestChangeHandler().tag); + assertTrue(newRootController.changeHandlerHistory().latestIsPush()); } @Test @@ -149,31 +149,31 @@ public void testSetBackstackHandlers() { router.setBackstack(newBackstack, setBackstackHandler); - assertTrue(initialController1.changeHandlerHistory.isValidHistory); - assertTrue(initialController2.changeHandlerHistory.isValidHistory); - assertTrue(newController1.changeHandlerHistory.isValidHistory); - - assertEquals(3, initialController1.changeHandlerHistory.size()); - assertEquals(2, initialController2.changeHandlerHistory.size()); - assertEquals(0, newController1.changeHandlerHistory.size()); - assertEquals(1, newController2.changeHandlerHistory.size()); - - assertNotNull(initialController1.changeHandlerHistory.latestToView()); - assertEquals(newController2.getView(), initialController1.changeHandlerHistory.latestToView()); - assertEquals(initialView1, initialController1.changeHandlerHistory.latestFromView()); - assertEquals(setBackstackHandler.tag, initialController1.changeHandlerHistory.latestChangeHandler().tag); - assertTrue(initialController1.changeHandlerHistory.latestIsPush()); - - assertNull(initialController2.changeHandlerHistory.latestToView()); - assertEquals(initialView2, initialController2.changeHandlerHistory.latestFromView()); - assertEquals(setBackstackHandler.tag, initialController2.changeHandlerHistory.latestChangeHandler().tag); - assertTrue(initialController2.changeHandlerHistory.latestIsPush()); - - assertNotNull(newController2.changeHandlerHistory.latestToView()); - assertEquals(newController2.getView(), newController2.changeHandlerHistory.latestToView()); - assertEquals(initialView1, newController2.changeHandlerHistory.latestFromView()); - assertEquals(setBackstackHandler.tag, newController2.changeHandlerHistory.latestChangeHandler().tag); - assertTrue(newController2.changeHandlerHistory.latestIsPush()); + assertTrue(initialController1.changeHandlerHistory().isValidHistory); + assertTrue(initialController2.changeHandlerHistory().isValidHistory); + assertTrue(newController1.changeHandlerHistory().isValidHistory); + + assertEquals(3, initialController1.changeHandlerHistory().size()); + assertEquals(2, initialController2.changeHandlerHistory().size()); + assertEquals(0, newController1.changeHandlerHistory().size()); + assertEquals(1, newController2.changeHandlerHistory().size()); + + assertNotNull(initialController1.changeHandlerHistory().latestToView()); + assertEquals(newController2.getView(), initialController1.changeHandlerHistory().latestToView()); + assertEquals(initialView1, initialController1.changeHandlerHistory().latestFromView()); + assertEquals(setBackstackHandler.tag, initialController1.changeHandlerHistory().latestChangeHandler().tag); + assertTrue(initialController1.changeHandlerHistory().latestIsPush()); + + assertNull(initialController2.changeHandlerHistory().latestToView()); + assertEquals(initialView2, initialController2.changeHandlerHistory().latestFromView()); + assertEquals(setBackstackHandler.tag, initialController2.changeHandlerHistory().latestChangeHandler().tag); + assertTrue(initialController2.changeHandlerHistory().latestIsPush()); + + assertNotNull(newController2.changeHandlerHistory().latestToView()); + assertEquals(newController2.getView(), newController2.changeHandlerHistory().latestToView()); + assertEquals(initialView1, newController2.changeHandlerHistory().latestFromView()); + assertEquals(setBackstackHandler.tag, newController2.changeHandlerHistory().latestChangeHandler().tag); + assertTrue(newController2.changeHandlerHistory().latestIsPush()); } @Test @@ -201,40 +201,40 @@ public void testSetBackstackWithTwoVisibleHandlers() { router.setBackstack(newBackstack, setBackstackHandler); - assertTrue(initialController1.changeHandlerHistory.isValidHistory); - assertTrue(initialController2.changeHandlerHistory.isValidHistory); - assertTrue(newController1.changeHandlerHistory.isValidHistory); - - assertEquals(3, initialController1.changeHandlerHistory.size()); - assertEquals(2, initialController2.changeHandlerHistory.size()); - assertEquals(2, newController1.changeHandlerHistory.size()); - assertEquals(1, newController2.changeHandlerHistory.size()); - - assertNotNull(initialController1.changeHandlerHistory.latestToView()); - assertEquals(newController1.getView(), initialController1.changeHandlerHistory.latestToView()); - assertEquals(initialView1, initialController1.changeHandlerHistory.latestFromView()); - assertEquals(setBackstackHandler.tag, initialController1.changeHandlerHistory.latestChangeHandler().tag); - assertTrue(initialController1.changeHandlerHistory.latestIsPush()); - - assertNull(initialController2.changeHandlerHistory.latestToView()); - assertEquals(initialView2, initialController2.changeHandlerHistory.latestFromView()); - assertEquals(setBackstackHandler.tag, initialController2.changeHandlerHistory.latestChangeHandler().tag); - assertTrue(initialController2.changeHandlerHistory.latestIsPush()); - - assertNotNull(newController1.changeHandlerHistory.latestToView()); - assertEquals(newController1.getView(), newController1.changeHandlerHistory.toViewAt(0)); - assertEquals(newController2.getView(), newController1.changeHandlerHistory.latestToView()); - assertEquals(initialView1, newController1.changeHandlerHistory.fromViewAt(0)); - assertEquals(newController1.getView(), newController1.changeHandlerHistory.latestFromView()); - assertEquals(setBackstackHandler.tag, newController1.changeHandlerHistory.changeHandlerAt(0).tag); - assertEquals(pushController2Handler.tag, newController1.changeHandlerHistory.latestChangeHandler().tag); - assertTrue(newController1.changeHandlerHistory.latestIsPush()); - - assertNotNull(newController2.changeHandlerHistory.latestToView()); - assertEquals(newController2.getView(), newController2.changeHandlerHistory.latestToView()); - assertEquals(newController1.getView(), newController2.changeHandlerHistory.latestFromView()); - assertEquals(pushController2Handler.tag, newController2.changeHandlerHistory.latestChangeHandler().tag); - assertTrue(newController2.changeHandlerHistory.latestIsPush()); + assertTrue(initialController1.changeHandlerHistory().isValidHistory); + assertTrue(initialController2.changeHandlerHistory().isValidHistory); + assertTrue(newController1.changeHandlerHistory().isValidHistory); + + assertEquals(3, initialController1.changeHandlerHistory().size()); + assertEquals(2, initialController2.changeHandlerHistory().size()); + assertEquals(2, newController1.changeHandlerHistory().size()); + assertEquals(1, newController2.changeHandlerHistory().size()); + + assertNotNull(initialController1.changeHandlerHistory().latestToView()); + assertEquals(newController1.getView(), initialController1.changeHandlerHistory().latestToView()); + assertEquals(initialView1, initialController1.changeHandlerHistory().latestFromView()); + assertEquals(setBackstackHandler.tag, initialController1.changeHandlerHistory().latestChangeHandler().tag); + assertTrue(initialController1.changeHandlerHistory().latestIsPush()); + + assertNull(initialController2.changeHandlerHistory().latestToView()); + assertEquals(initialView2, initialController2.changeHandlerHistory().latestFromView()); + assertEquals(setBackstackHandler.tag, initialController2.changeHandlerHistory().latestChangeHandler().tag); + assertTrue(initialController2.changeHandlerHistory().latestIsPush()); + + assertNotNull(newController1.changeHandlerHistory().latestToView()); + assertEquals(newController1.getView(), newController1.changeHandlerHistory().toViewAt(0)); + assertEquals(newController2.getView(), newController1.changeHandlerHistory().latestToView()); + assertEquals(initialView1, newController1.changeHandlerHistory().fromViewAt(0)); + assertEquals(newController1.getView(), newController1.changeHandlerHistory().latestFromView()); + assertEquals(setBackstackHandler.tag, newController1.changeHandlerHistory().changeHandlerAt(0).tag); + assertEquals(pushController2Handler.tag, newController1.changeHandlerHistory().latestChangeHandler().tag); + assertTrue(newController1.changeHandlerHistory().latestIsPush()); + + assertNotNull(newController2.changeHandlerHistory().latestToView()); + assertEquals(newController2.getView(), newController2.changeHandlerHistory().latestToView()); + assertEquals(newController1.getView(), newController2.changeHandlerHistory().latestFromView()); + assertEquals(pushController2Handler.tag, newController2.changeHandlerHistory().latestChangeHandler().tag); + assertTrue(newController2.changeHandlerHistory().latestIsPush()); } @Test @@ -256,18 +256,18 @@ public void testSetBackstackForPushHandlers() { router.setBackstack(newBackstack, setBackstackHandler); - assertTrue(initialController.changeHandlerHistory.isValidHistory); - assertTrue(newController.changeHandlerHistory.isValidHistory); + assertTrue(initialController.changeHandlerHistory().isValidHistory); + assertTrue(newController.changeHandlerHistory().isValidHistory); - assertEquals(2, initialController.changeHandlerHistory.size()); - assertEquals(1, newController.changeHandlerHistory.size()); + assertEquals(2, initialController.changeHandlerHistory().size()); + assertEquals(1, newController.changeHandlerHistory().size()); - assertNotNull(initialController.changeHandlerHistory.latestToView()); - assertEquals(newController.getView(), initialController.changeHandlerHistory.latestToView()); - assertEquals(initialView, initialController.changeHandlerHistory.latestFromView()); - assertEquals(setBackstackHandler.tag, initialController.changeHandlerHistory.latestChangeHandler().tag); - assertTrue(initialController.changeHandlerHistory.latestIsPush()); - assertTrue(newController.changeHandlerHistory.latestIsPush()); + assertNotNull(initialController.changeHandlerHistory().latestToView()); + assertEquals(newController.getView(), initialController.changeHandlerHistory().latestToView()); + assertEquals(initialView, initialController.changeHandlerHistory().latestFromView()); + assertEquals(setBackstackHandler.tag, initialController.changeHandlerHistory().latestChangeHandler().tag); + assertTrue(initialController.changeHandlerHistory().latestIsPush()); + assertTrue(newController.changeHandlerHistory().latestIsPush()); } @Test @@ -293,21 +293,21 @@ public void testSetBackstackForInvertHandlersWithRemovesView() { router.setBackstack(newBackstack, setBackstackHandler); - assertTrue(initialController1.changeHandlerHistory.isValidHistory); - assertTrue(initialController2.changeHandlerHistory.isValidHistory); + assertTrue(initialController1.changeHandlerHistory().isValidHistory); + assertTrue(initialController2.changeHandlerHistory().isValidHistory); - assertEquals(3, initialController1.changeHandlerHistory.size()); - assertEquals(2, initialController2.changeHandlerHistory.size()); + assertEquals(3, initialController1.changeHandlerHistory().size()); + assertEquals(2, initialController2.changeHandlerHistory().size()); - assertNotNull(initialController1.changeHandlerHistory.latestToView()); - assertEquals(initialView2, initialController1.changeHandlerHistory.latestFromView()); - assertEquals(setBackstackHandler.tag, initialController1.changeHandlerHistory.latestChangeHandler().tag); - assertFalse(initialController1.changeHandlerHistory.latestIsPush()); + assertNotNull(initialController1.changeHandlerHistory().latestToView()); + assertEquals(initialView2, initialController1.changeHandlerHistory().latestFromView()); + assertEquals(setBackstackHandler.tag, initialController1.changeHandlerHistory().latestChangeHandler().tag); + assertFalse(initialController1.changeHandlerHistory().latestIsPush()); - assertNotNull(initialController2.changeHandlerHistory.latestToView()); - assertEquals(initialView2, initialController2.changeHandlerHistory.latestFromView()); - assertEquals(setBackstackHandler.tag, initialController2.changeHandlerHistory.latestChangeHandler().tag); - assertFalse(initialController2.changeHandlerHistory.latestIsPush()); + assertNotNull(initialController2.changeHandlerHistory().latestToView()); + assertEquals(initialView2, initialController2.changeHandlerHistory().latestFromView()); + assertEquals(setBackstackHandler.tag, initialController2.changeHandlerHistory().latestChangeHandler().tag); + assertFalse(initialController2.changeHandlerHistory().latestIsPush()); } @Test @@ -334,21 +334,21 @@ public void testSetBackstackForInvertHandlersWithoutRemovesView() { router.setBackstack(newBackstack, setBackstackHandler); - assertTrue(initialController1.changeHandlerHistory.isValidHistory); - assertTrue(initialController2.changeHandlerHistory.isValidHistory); + assertTrue(initialController1.changeHandlerHistory().isValidHistory); + assertTrue(initialController2.changeHandlerHistory().isValidHistory); - assertEquals(2, initialController1.changeHandlerHistory.size()); - assertEquals(2, initialController2.changeHandlerHistory.size()); + assertEquals(2, initialController1.changeHandlerHistory().size()); + assertEquals(2, initialController2.changeHandlerHistory().size()); - assertNotNull(initialController1.changeHandlerHistory.latestToView()); - assertEquals(initialView1, initialController1.changeHandlerHistory.latestFromView()); - assertEquals(initialPushHandler2.tag, initialController1.changeHandlerHistory.latestChangeHandler().tag); - assertTrue(initialController1.changeHandlerHistory.latestIsPush()); + assertNotNull(initialController1.changeHandlerHistory().latestToView()); + assertEquals(initialView1, initialController1.changeHandlerHistory().latestFromView()); + assertEquals(initialPushHandler2.tag, initialController1.changeHandlerHistory().latestChangeHandler().tag); + assertTrue(initialController1.changeHandlerHistory().latestIsPush()); - assertNull(initialController2.changeHandlerHistory.latestToView()); - assertEquals(initialView2, initialController2.changeHandlerHistory.latestFromView()); - assertEquals(setBackstackHandler.tag, initialController2.changeHandlerHistory.latestChangeHandler().tag); - assertFalse(initialController2.changeHandlerHistory.latestIsPush()); + assertNull(initialController2.changeHandlerHistory().latestToView()); + assertEquals(initialView2, initialController2.changeHandlerHistory().latestFromView()); + assertEquals(setBackstackHandler.tag, initialController2.changeHandlerHistory().latestChangeHandler().tag); + assertFalse(initialController2.changeHandlerHistory().latestIsPush()); } } diff --git a/conductor/src/test/java/com/bluelinelabs/conductor/TargetControllerTests.java b/conductor/src/test/java/com/bluelinelabs/conductor/TargetControllerTests.java index 9a91ca0c..9c2a6d31 100644 --- a/conductor/src/test/java/com/bluelinelabs/conductor/TargetControllerTests.java +++ b/conductor/src/test/java/com/bluelinelabs/conductor/TargetControllerTests.java @@ -6,6 +6,7 @@ import com.bluelinelabs.conductor.util.ActivityProxy; import com.bluelinelabs.conductor.util.MockChangeHandler; import com.bluelinelabs.conductor.util.TestController; +import com.bluelinelabs.conductor.util.TestMasterDetailController; import org.junit.Before; import org.junit.Test; @@ -103,4 +104,33 @@ public void testChildParentTarget() { assertEquals(controllerB, controllerA.getTargetController()); } + @Test + public void testMasterDetailTarget() { + final TestMasterDetailController controller = new TestMasterDetailController(); + final TestController controllerA = new TestController(); + final TestController controllerB = new TestController(); + + assertNull(controller.getTargetController()); + assertNull(controllerA.getTargetController()); + assertNull(controllerB.getTargetController()); + + router.pushController(RouterTransaction.with(controller) + .pushChangeHandler(MockChangeHandler.defaultHandler()) + .popChangeHandler(MockChangeHandler.defaultHandler())); + + controller.getMasterRouter().pushController(RouterTransaction.with(controllerA) + .pushChangeHandler(MockChangeHandler.defaultHandler()) + .popChangeHandler(MockChangeHandler.defaultHandler())); + + + controllerB.setTargetController(controllerA); + + router.pushController(RouterTransaction.with(controllerB) + .pushChangeHandler(MockChangeHandler.defaultHandler()) + .popChangeHandler(MockChangeHandler.defaultHandler())); + + assertNull(controllerA.getTargetController()); + assertEquals(controllerA, controllerB.getTargetController()); + } + } diff --git a/conductor/src/test/java/com/bluelinelabs/conductor/util/ActivityProxy.java b/conductor/src/test/java/com/bluelinelabs/conductor/util/ActivityProxy.java index b29deffa..f49446e2 100644 --- a/conductor/src/test/java/com/bluelinelabs/conductor/util/ActivityProxy.java +++ b/conductor/src/test/java/com/bluelinelabs/conductor/util/ActivityProxy.java @@ -1,6 +1,8 @@ package com.bluelinelabs.conductor.util; +import android.content.res.Configuration; import android.os.Bundle; + import androidx.annotation.IdRes; import org.robolectric.Robolectric; @@ -62,8 +64,14 @@ public ActivityProxy destroy() { } public ActivityProxy rotate() { + Configuration configuration = getActivity().getResources().getConfiguration(); + if (configuration.orientation == Configuration.ORIENTATION_LANDSCAPE) { + configuration.orientation = Configuration.ORIENTATION_PORTRAIT; + } else { + configuration.orientation = Configuration.ORIENTATION_LANDSCAPE; + } getActivity().isChangingConfigurations = true; - getActivity().recreate(); + activityController.configurationChange(configuration); return this; } diff --git a/conductor/src/test/java/com/bluelinelabs/conductor/util/CallStateOwner.java b/conductor/src/test/java/com/bluelinelabs/conductor/util/CallStateOwner.java new file mode 100644 index 00000000..82215e17 --- /dev/null +++ b/conductor/src/test/java/com/bluelinelabs/conductor/util/CallStateOwner.java @@ -0,0 +1,5 @@ +package com.bluelinelabs.conductor.util; + +public interface CallStateOwner { + CallState currentCallState(); +} diff --git a/conductor/src/test/java/com/bluelinelabs/conductor/util/ChangeHandlerHistoryOwner.java b/conductor/src/test/java/com/bluelinelabs/conductor/util/ChangeHandlerHistoryOwner.java new file mode 100644 index 00000000..40d1b46f --- /dev/null +++ b/conductor/src/test/java/com/bluelinelabs/conductor/util/ChangeHandlerHistoryOwner.java @@ -0,0 +1,5 @@ +package com.bluelinelabs.conductor.util; + +public interface ChangeHandlerHistoryOwner { + ChangeHandlerHistory changeHandlerHistory(); +} diff --git a/conductor/src/test/java/com/bluelinelabs/conductor/util/TestController.java b/conductor/src/test/java/com/bluelinelabs/conductor/util/TestController.java index 5175386b..4304297d 100644 --- a/conductor/src/test/java/com/bluelinelabs/conductor/util/TestController.java +++ b/conductor/src/test/java/com/bluelinelabs/conductor/util/TestController.java @@ -16,7 +16,7 @@ import com.bluelinelabs.conductor.ControllerChangeHandler; import com.bluelinelabs.conductor.ControllerChangeType; -public class TestController extends Controller { +public class TestController extends Controller implements CallStateOwner, ChangeHandlerHistoryOwner { @IdRes public static final int VIEW_ID = 2342; @IdRes public static final int CHILD_VIEW_ID_1 = 2343; @@ -24,8 +24,8 @@ public class TestController extends Controller { private static final String KEY_CALL_STATE = "TestController.currentCallState"; - public CallState currentCallState = new CallState(false); - public ChangeHandlerHistory changeHandlerHistory = new ChangeHandlerHistory(); + private CallState currentCallState = new CallState(false); + private ChangeHandlerHistory changeHandlerHistory = new ChangeHandlerHistory(); @NonNull @Override @@ -151,4 +151,13 @@ public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { currentCallState.createOptionsMenuCalls++; } + @Override + public CallState currentCallState() { + return currentCallState; + } + + @Override + public ChangeHandlerHistory changeHandlerHistory() { + return changeHandlerHistory; + } } diff --git a/conductor/src/test/java/com/bluelinelabs/conductor/util/TestMasterDetailController.java b/conductor/src/test/java/com/bluelinelabs/conductor/util/TestMasterDetailController.java new file mode 100644 index 00000000..b7ff7b34 --- /dev/null +++ b/conductor/src/test/java/com/bluelinelabs/conductor/util/TestMasterDetailController.java @@ -0,0 +1,177 @@ +package com.bluelinelabs.conductor.util; + +import android.content.Context; +import android.content.Intent; +import android.content.res.Configuration; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.FrameLayout; + +import androidx.annotation.IdRes; +import androidx.annotation.NonNull; + +import com.bluelinelabs.conductor.ControllerChangeHandler; +import com.bluelinelabs.conductor.ControllerChangeType; +import com.bluelinelabs.conductor.MasterDetailController; + +public class TestMasterDetailController extends MasterDetailController implements CallStateOwner, ChangeHandlerHistoryOwner { + + @IdRes public static final int VIEW_ID = 2442; + @IdRes public static final int CHILD_VIEW_ID_1 = 2443; + @IdRes public static final int CHILD_VIEW_ID_2 = 2444; + + private static final String KEY_CALL_STATE = "TestController.currentCallState"; + + private CallState currentCallState = new CallState(false); + private ChangeHandlerHistory changeHandlerHistory = new ChangeHandlerHistory(); + + @Override @NonNull + protected View onCreateView(@NonNull LayoutInflater inflater, @NonNull ViewGroup container) { + currentCallState.createViewCalls++; + FrameLayout view = new AttachFakingFrameLayout(inflater.getContext()); + view.setId(VIEW_ID); + + FrameLayout childContainer1 = new AttachFakingFrameLayout(inflater.getContext()); + childContainer1.setId(CHILD_VIEW_ID_1); + view.addView(childContainer1); + + if (getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE) { + FrameLayout childContainer2 = new AttachFakingFrameLayout(inflater.getContext()); + childContainer2.setId(CHILD_VIEW_ID_2); + view.addView(childContainer2); + } + + initRouters(view, CHILD_VIEW_ID_1, CHILD_VIEW_ID_2); + return view; + } + + @Override + protected void onChangeStarted(@NonNull ControllerChangeHandler changeHandler, @NonNull ControllerChangeType changeType) { + super.onChangeStarted(changeHandler, changeType); + currentCallState.changeStartCalls++; + } + + @Override + protected void onChangeEnded(@NonNull ControllerChangeHandler changeHandler, @NonNull ControllerChangeType changeType) { + super.onChangeEnded(changeHandler, changeType); + currentCallState.changeEndCalls++; + + if (changeHandler instanceof MockChangeHandler) { + MockChangeHandler mockHandler = (MockChangeHandler)changeHandler; + changeHandlerHistory.addEntry(mockHandler.from, mockHandler.to, changeType.isPush, mockHandler); + } else { + changeHandlerHistory.isValidHistory = false; + } + } + + @Override + protected void onContextAvailable(@NonNull Context context) { + super.onContextAvailable(context); + currentCallState.contextAvailableCalls++; + } + + @Override + protected void onContextUnavailable() { + super.onContextUnavailable(); + currentCallState.contextUnavailableCalls++; + } + + @Override + protected void onAttach(@NonNull View view) { + super.onAttach(view); + currentCallState.attachCalls++; + } + + @Override + protected void onDetach(@NonNull View view) { + super.onDetach(view); + currentCallState.detachCalls++; + } + + @Override + protected void onDestroyView(@NonNull View view) { + super.onDestroyView(view); + currentCallState.destroyViewCalls++; + } + + @Override + protected void onDestroy() { + super.onDestroy(); + currentCallState.destroyCalls++; + } + + @Override + protected void onSaveViewState(@NonNull View view, @NonNull Bundle outState) { + super.onSaveViewState(view, outState); + currentCallState.saveViewStateCalls++; + } + + @Override + protected void onRestoreViewState(@NonNull View view, @NonNull Bundle savedViewState) { + super.onRestoreViewState(view, savedViewState); + currentCallState.restoreViewStateCalls++; + } + + @Override + protected void onSaveInstanceState(@NonNull Bundle outState) { + currentCallState.saveInstanceStateCalls++; + + outState.putParcelable(KEY_CALL_STATE, currentCallState); + + super.onSaveInstanceState(outState); + } + + @Override + protected void onRestoreInstanceState(@NonNull Bundle savedInstanceState) { + super.onRestoreInstanceState(savedInstanceState); + + currentCallState = savedInstanceState.getParcelable(KEY_CALL_STATE); + + currentCallState.restoreInstanceStateCalls++; + } + + @Override + public void onActivityResult(int requestCode, int resultCode, Intent data) { + super.onActivityResult(requestCode, resultCode, data); + + currentCallState.onActivityResultCalls++; + } + + @Override + public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults); + + currentCallState.onRequestPermissionsResultCalls++; + } + + @Override + public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { + super.onCreateOptionsMenu(menu, inflater); + + currentCallState.createOptionsMenuCalls++; + } + + @Override + public ControllerChangeHandler getRootDetailPushHandler() { + return MockChangeHandler.taggedHandler("push", true); + } + + @Override + public ControllerChangeHandler getRootDetailPopHandler() { + return MockChangeHandler.taggedHandler("pop", true); + } + + @Override + public CallState currentCallState() { + return currentCallState; + } + + @Override + public ChangeHandlerHistory changeHandlerHistory() { + return changeHandlerHistory; + } +} diff --git a/demo/src/main/java/com/bluelinelabs/conductor/demo/changehandler/listeners/BackstackChangeListener.java b/demo/src/main/java/com/bluelinelabs/conductor/demo/changehandler/listeners/BackstackChangeListener.java new file mode 100644 index 00000000..0ee9a40a --- /dev/null +++ b/demo/src/main/java/com/bluelinelabs/conductor/demo/changehandler/listeners/BackstackChangeListener.java @@ -0,0 +1,28 @@ +package com.bluelinelabs.conductor.demo.changehandler.listeners; + +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.bluelinelabs.conductor.Controller; +import com.bluelinelabs.conductor.ControllerChangeHandler; + +public class BackstackChangeListener implements ControllerChangeHandler.ControllerChangeListener { + + private final Runnable runnable; + + public BackstackChangeListener(Runnable runnable) { + this.runnable = runnable; + } + + @Override + public void onChangeStarted(@Nullable Controller to, @Nullable Controller from, boolean isPush, @NonNull ViewGroup container, @NonNull ControllerChangeHandler handler) { + + } + + @Override + public void onChangeCompleted(@Nullable Controller to, @Nullable Controller from, boolean isPush, @NonNull ViewGroup container, @NonNull ControllerChangeHandler handler) { + runnable.run(); + } +} diff --git a/demo/src/main/java/com/bluelinelabs/conductor/demo/controllers/ListController.java b/demo/src/main/java/com/bluelinelabs/conductor/demo/controllers/ListController.java new file mode 100644 index 00000000..e7cec6ce --- /dev/null +++ b/demo/src/main/java/com/bluelinelabs/conductor/demo/controllers/ListController.java @@ -0,0 +1,216 @@ +package com.bluelinelabs.conductor.demo.controllers; + +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.core.content.ContextCompat; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + +import com.bluelinelabs.conductor.Controller; +import com.bluelinelabs.conductor.RouterTransaction; +import com.bluelinelabs.conductor.changehandler.HorizontalChangeHandler; +import com.bluelinelabs.conductor.demo.R; +import com.bluelinelabs.conductor.demo.changehandler.listeners.BackstackChangeListener; +import com.bluelinelabs.conductor.demo.controllers.base.BaseController; +import com.bluelinelabs.conductor.demo.util.BundleBuilder; +import com.bluelinelabs.conductor.demo.util.ObjectUtils; + +import java.util.ArrayList; +import java.util.List; + +import butterknife.BindView; +import butterknife.ButterKnife; +import butterknife.OnClick; + +public class ListController extends BaseController { + + private static final String KEY_INDEX = "ItemListController.index"; + + private static final int ITEMS_COUNT = 6; + + private int index; + + @Nullable + private Integer selectedIndex; + + @Nullable + private ItemAdapter adapter; + + @BindView(R.id.text_view) + TextView textView; + + @BindView(R.id.recycler_view) + RecyclerView recyclerView; + + private BackstackChangeListener changeListener = new BackstackChangeListener(this::checkSelectedIndex); + + public ListController(int index) { + this(new BundleBuilder(new Bundle()) + .putInt(KEY_INDEX, index) + .build()); + + } + + public ListController(Bundle args) { + super(args); + index = getArgs().getInt(KEY_INDEX); + } + + @Override + protected String getTitle() { + return "Master/Detail Flow"; + } + + @Override + protected View inflateView(@NonNull LayoutInflater inflater, @NonNull ViewGroup container) { + return inflater.inflate(R.layout.controller_list, container, false); + } + + @Override + protected void onViewBound(@NonNull View view) { + super.onViewBound(view); + + adapter = new ItemAdapter(); + recyclerView.setHasFixedSize(true); + recyclerView.setLayoutManager(new LinearLayoutManager(view.getContext())); + recyclerView.setAdapter(adapter); + textView.setVisibility(isTwoPanesMode() ? View.GONE : View.VISIBLE); + if (isTwoPanesMode()) { + getMasterDetailController().getDetailRouter().addChangeListener(changeListener); + checkSelectedIndex(); + } + } + + @Override + protected void onDestroyView(@NonNull View view) { + if (isTwoPanesMode()) { + getMasterDetailController().getDetailRouter().removeChangeListener(changeListener); + } + recyclerView.setAdapter(null); + super.onDestroyView(view); + } + + private void onItemSelected(ItemAdapter.Item item) { + if (item.disclosable) { + getRouter().pushController(RouterTransaction.with(new ListController(item.index + ITEMS_COUNT)) + .pushChangeHandler(new HorizontalChangeHandler()) + .popChangeHandler(new HorizontalChangeHandler())); + } else { + Controller controller = new NavigationDemoController(item.index, NavigationDemoController.DisplayUpMode.HIDE); + if (getMasterDetailController() != null) { + getMasterDetailController().getDetailRouter().setRoot(RouterTransaction.with(controller)); + } else { + // Won't happen in this example. + // However, in real scenario same controller can be used + // either in master/detail (tablet) or in regular (phone) parent controller + getRouter().pushController(RouterTransaction.with(controller) + .pushChangeHandler(new HorizontalChangeHandler()) + .popChangeHandler(new HorizontalChangeHandler())); + } + } + } + + private boolean isTwoPanesMode() { + return getMasterDetailController() != null && getMasterDetailController().isTwoPanesMode(); + } + + private void checkSelectedIndex() { + if (isTwoPanesMode()) { + List transactions = getMasterDetailController().getDetailRouter().getBackstack(); + if (transactions.isEmpty()) { + setSelectedIndex(null); + } else { + Controller controller = transactions.get(0).controller(); + if (controller instanceof NavigationDemoController) { + setSelectedIndex(((NavigationDemoController) controller).getIndex()); + } + } + } + } + + private void setSelectedIndex(@Nullable Integer index) { + if (!ObjectUtils.equals(selectedIndex, index)) { + selectedIndex = index; + if (adapter != null) { + adapter.notifyDataSetChanged(); + } + } + } + + class ItemAdapter extends RecyclerView.Adapter { + + class Item { + int index; + public String name; + boolean disclosable; + } + + private final List items; + + ItemAdapter() { + this.items = new ArrayList<>(ITEMS_COUNT); + for (int i = 0; i < ITEMS_COUNT; i++) { + Item item = new Item(); + item.index = index + i; + item.disclosable = i < ITEMS_COUNT / 2; + item.name = (item.disclosable ? "Folder " : "File ") + item.index; + items.add(item); + } + } + + @Override + public ItemAdapter.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { + LayoutInflater inflater = LayoutInflater.from(parent.getContext()); + return new ItemAdapter.ViewHolder(inflater.inflate(R.layout.row_detail_item, parent, false)); + } + + @Override + public void onBindViewHolder(ItemAdapter.ViewHolder holder, int position) { + holder.bind(items.get(position)); + } + + @Override + public int getItemCount() { + return items.size(); + } + + class ViewHolder extends RecyclerView.ViewHolder { + + @BindView(R.id.row_root) View root; + @BindView(R.id.tv_title) TextView tvTitle; + @BindView(R.id.iv_chevron) ImageView ivChevron; + private Item item; + + ViewHolder(View itemView) { + super(itemView); + ButterKnife.bind(this, itemView); + } + + void bind(Item item) { + this.item = item; + + tvTitle.setText(item.name); + ivChevron.setVisibility(item.disclosable ? View.VISIBLE : View.GONE); + + if (isTwoPanesMode() && ObjectUtils.equals(selectedIndex, item.index)) { + root.setBackgroundColor(ContextCompat.getColor(root.getContext(), R.color.grey_300)); + } else { + root.setBackgroundColor(ContextCompat.getColor(root.getContext(), android.R.color.transparent)); + } + } + + @OnClick(R.id.row_root) + void onRowClick() { + onItemSelected(item); + } + + } + } +} diff --git a/demo/src/main/java/com/bluelinelabs/conductor/demo/controllers/MasterDetailListController.java b/demo/src/main/java/com/bluelinelabs/conductor/demo/controllers/MasterDetailListController.java index 9b788790..73713540 100644 --- a/demo/src/main/java/com/bluelinelabs/conductor/demo/controllers/MasterDetailListController.java +++ b/demo/src/main/java/com/bluelinelabs/conductor/demo/controllers/MasterDetailListController.java @@ -1,158 +1,56 @@ package com.bluelinelabs.conductor.demo.controllers; -import android.os.Bundle; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.core.content.ContextCompat; -import androidx.recyclerview.widget.LinearLayoutManager; -import androidx.recyclerview.widget.RecyclerView; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.TextView; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + import com.bluelinelabs.conductor.RouterTransaction; -import com.bluelinelabs.conductor.changehandler.HorizontalChangeHandler; import com.bluelinelabs.conductor.demo.R; -import com.bluelinelabs.conductor.demo.controllers.base.BaseController; +import com.bluelinelabs.conductor.demo.changehandler.listeners.BackstackChangeListener; +import com.bluelinelabs.conductor.demo.controllers.base.BaseMasterDetailController; import butterknife.BindView; -import butterknife.ButterKnife; -import butterknife.OnClick; - -public class MasterDetailListController extends BaseController { - private static final String KEY_SELECTED_INDEX = "MasterDetailListController.selectedIndex"; - - public enum DetailItemModel { - ONE("Item 1", "This is a quick demo of master/detail flow using Conductor. In portrait mode you'll see a standard list. In landscape, you'll see a two-pane layout.", R.color.green_300), - TWO("Item 2", "This is another item.", R.color.cyan_300), - THREE("Item 3", "Wow, a 3rd item!", R.color.deep_purple_300); - - String title; - String detail; - int backgroundColor; - - DetailItemModel(String title, String detail, int backgroundColor) { - this.title = title; - this.detail = detail; - this.backgroundColor = backgroundColor; - } - } +public class MasterDetailListController extends BaseMasterDetailController { - @BindView(R.id.recycler_view) RecyclerView recyclerView; - @Nullable @BindView(R.id.detail_container) ViewGroup detailContainer; + @Nullable @BindView(R.id.tv_detail_empty) TextView tvDetailEmpty; - private int selectedIndex; - private boolean twoPaneView; + private BackstackChangeListener changeListener = new BackstackChangeListener(this::onDetailChange); @Override protected View inflateView(@NonNull LayoutInflater inflater, @NonNull ViewGroup container) { - return inflater.inflate(R.layout.controller_master_detail_list, container, false); + return inflater.inflate(R.layout.controller_master_details, container, false); } @Override protected void onViewBound(@NonNull View view) { super.onViewBound(view); - - recyclerView.setHasFixedSize(true); - recyclerView.setLayoutManager(new LinearLayoutManager(view.getContext())); - recyclerView.setAdapter(new DetailItemAdapter(LayoutInflater.from(view.getContext()), DetailItemModel.values())); - - twoPaneView = (detailContainer != null); - if (twoPaneView) { - onRowSelected(selectedIndex); + initRouters(view, R.id.master_container, R.id.detail_container); + if (!getMasterRouter().hasRootController()) { + getMasterRouter().setRoot(RouterTransaction.with(new ListController(1))); + } + if (isTwoPanesMode()) { + getDetailRouter().setPopsLastView(true); + getDetailRouter().addChangeListener(changeListener); + onDetailChange(); } } @Override - protected void onSaveInstanceState(@NonNull Bundle outState) { - super.onSaveInstanceState(outState); - - outState.putInt(KEY_SELECTED_INDEX, selectedIndex); - } - - @Override - protected void onRestoreInstanceState(@NonNull Bundle savedInstanceState) { - super.onRestoreInstanceState(savedInstanceState); - - selectedIndex = savedInstanceState.getInt(KEY_SELECTED_INDEX); - } - - @Override - protected String getTitle() { - return "Master/Detail Flow"; - } - - void onRowSelected(int index) { - selectedIndex = index; - - DetailItemModel model = DetailItemModel.values()[index]; - ChildController controller = new ChildController(model.detail, model.backgroundColor, true); - - if (twoPaneView) { - getChildRouter(detailContainer).setRoot(RouterTransaction.with(controller)); - } else { - getRouter().pushController(RouterTransaction.with(controller) - .pushChangeHandler(new HorizontalChangeHandler()) - .popChangeHandler(new HorizontalChangeHandler())); + protected void onDestroyView(@NonNull View view) { + if (isTwoPanesMode()) { + getDetailRouter().removeChangeListener(changeListener); } + super.onDestroyView(view); } - class DetailItemAdapter extends RecyclerView.Adapter { - - private final LayoutInflater inflater; - private final DetailItemModel[] items; - - public DetailItemAdapter(LayoutInflater inflater, DetailItemModel[] items) { - this.inflater = inflater; - this.items = items; - } - - @Override - public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { - return new ViewHolder(inflater.inflate(R.layout.row_detail_item, parent, false)); - } - - @Override - public void onBindViewHolder(ViewHolder holder, int position) { - holder.bind(items[position], position); - } - - @Override - public int getItemCount() { - return items.length; - } - - class ViewHolder extends RecyclerView.ViewHolder { - - @BindView(R.id.row_root) View root; - @BindView(R.id.tv_title) TextView tvTitle; - private int position; - - public ViewHolder(View itemView) { - super(itemView); - ButterKnife.bind(this, itemView); - } - - void bind(DetailItemModel item, int position) { - tvTitle.setText(item.title); - this.position = position; - - if (twoPaneView && position == selectedIndex) { - root.setBackgroundColor(ContextCompat.getColor(root.getContext(), R.color.grey_400)); - } else { - root.setBackgroundColor(ContextCompat.getColor(root.getContext(), android.R.color.transparent)); - } - } - - @OnClick(R.id.row_root) - void onRowClick() { - onRowSelected(position); - notifyDataSetChanged(); - } - + private void onDetailChange() { + if (tvDetailEmpty != null) { + tvDetailEmpty.setVisibility(!getDetailRouter().hasRootController() ? View.VISIBLE : View.GONE); } } - } diff --git a/demo/src/main/java/com/bluelinelabs/conductor/demo/controllers/NavigationDemoController.java b/demo/src/main/java/com/bluelinelabs/conductor/demo/controllers/NavigationDemoController.java index 3330b1d4..9d917232 100644 --- a/demo/src/main/java/com/bluelinelabs/conductor/demo/controllers/NavigationDemoController.java +++ b/demo/src/main/java/com/bluelinelabs/conductor/demo/controllers/NavigationDemoController.java @@ -59,6 +59,10 @@ public NavigationDemoController(Bundle args) { displayUpMode = DisplayUpMode.values()[args.getInt(KEY_DISPLAY_UP_MODE)]; } + public int getIndex() { + return index; + } + @NonNull @Override protected View inflateView(@NonNull LayoutInflater inflater, @NonNull ViewGroup container) { diff --git a/demo/src/main/java/com/bluelinelabs/conductor/demo/controllers/base/BaseController.java b/demo/src/main/java/com/bluelinelabs/conductor/demo/controllers/base/BaseController.java index 32b4d720..bff09ce7 100644 --- a/demo/src/main/java/com/bluelinelabs/conductor/demo/controllers/base/BaseController.java +++ b/demo/src/main/java/com/bluelinelabs/conductor/demo/controllers/base/BaseController.java @@ -30,6 +30,9 @@ protected void onAttach(@NonNull View view) { } protected void setTitle() { + if (isDetail()) { + return; + } Controller parentController = getParentController(); while (parentController != null) { if (parentController instanceof BaseController && ((BaseController)parentController).getTitle() != null) { diff --git a/demo/src/main/java/com/bluelinelabs/conductor/demo/controllers/base/BaseMasterDetailController.java b/demo/src/main/java/com/bluelinelabs/conductor/demo/controllers/base/BaseMasterDetailController.java new file mode 100644 index 00000000..a68f258e --- /dev/null +++ b/demo/src/main/java/com/bluelinelabs/conductor/demo/controllers/base/BaseMasterDetailController.java @@ -0,0 +1,103 @@ +package com.bluelinelabs.conductor.demo.controllers.base; + +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.appcompat.app.ActionBar; + +import com.bluelinelabs.conductor.Controller; +import com.bluelinelabs.conductor.ControllerChangeHandler; +import com.bluelinelabs.conductor.ControllerChangeType; +import com.bluelinelabs.conductor.MasterDetailController; +import com.bluelinelabs.conductor.demo.ActionBarProvider; +import com.bluelinelabs.conductor.demo.DemoApplication; + +import butterknife.ButterKnife; +import butterknife.Unbinder; + +public abstract class BaseMasterDetailController extends MasterDetailController { + + private Unbinder unbinder; + private boolean hasExited; + + protected BaseMasterDetailController() { super(); } + protected BaseMasterDetailController(Bundle args) { + super(args); + } + + @Override + protected void onAttach(@NonNull View view) { + setTitle(); + super.onAttach(view); + } + + protected abstract View inflateView(@NonNull LayoutInflater inflater, @NonNull ViewGroup container); + + @NonNull + @Override + protected View onCreateView(@NonNull LayoutInflater inflater, @NonNull ViewGroup container) { + View view = inflateView(inflater, container); + unbinder = ButterKnife.bind(this, view); + onViewBound(view); + return view; + } + + protected void onViewBound(@NonNull View view) { } + + @Override + protected void onDestroyView(@NonNull View view) { + super.onDestroyView(view); + unbinder.unbind(); + unbinder = null; + } + + @Override + protected void onChangeEnded(@NonNull ControllerChangeHandler changeHandler, @NonNull ControllerChangeType changeType) { + super.onChangeEnded(changeHandler, changeType); + + hasExited = !changeType.isEnter; + if (isDestroyed()) { + DemoApplication.refWatcher.watch(this); + } + } + + @Override + public void onDestroy() { + super.onDestroy(); + + if (hasExited) { + DemoApplication.refWatcher.watch(this); + } + } + + // Note: This is just a quick demo of how an ActionBar *can* be accessed, not necessarily how it *should* + // be accessed. In a production app, this would use Dagger instead. + protected ActionBar getActionBar() { + ActionBarProvider actionBarProvider = ((ActionBarProvider)getActivity()); + return actionBarProvider != null ? actionBarProvider.getSupportActionBar() : null; + } + + protected void setTitle() { + Controller parentController = getParentController(); + while (parentController != null) { + if (parentController instanceof BaseController && ((BaseController)parentController).getTitle() != null) { + return; + } + parentController = parentController.getParentController(); + } + + String title = getTitle(); + ActionBar actionBar = getActionBar(); + if (title != null && actionBar != null) { + actionBar.setTitle(title); + } + } + + protected String getTitle() { + return null; + } + +} diff --git a/demo/src/main/java/com/bluelinelabs/conductor/demo/util/ObjectUtils.java b/demo/src/main/java/com/bluelinelabs/conductor/demo/util/ObjectUtils.java new file mode 100644 index 00000000..1aa51c75 --- /dev/null +++ b/demo/src/main/java/com/bluelinelabs/conductor/demo/util/ObjectUtils.java @@ -0,0 +1,8 @@ +package com.bluelinelabs.conductor.demo.util; + +public class ObjectUtils { + + public static boolean equals(Object a, Object b) { + return (a == b) || (a != null && a.equals(b)); + } +} diff --git a/demo/src/main/res/drawable/ic_chevron_right_black_24dp.xml b/demo/src/main/res/drawable/ic_chevron_right_black_24dp.xml new file mode 100644 index 00000000..24835127 --- /dev/null +++ b/demo/src/main/res/drawable/ic_chevron_right_black_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/demo/src/main/res/layout-land/controller_master_detail_list.xml b/demo/src/main/res/layout-land/controller_master_detail_list.xml deleted file mode 100644 index 6792e4da..00000000 --- a/demo/src/main/res/layout-land/controller_master_detail_list.xml +++ /dev/null @@ -1,23 +0,0 @@ - - - - - - - - diff --git a/demo/src/main/res/layout-land/controller_master_details.xml b/demo/src/main/res/layout-land/controller_master_details.xml new file mode 100644 index 00000000..48861f0a --- /dev/null +++ b/demo/src/main/res/layout-land/controller_master_details.xml @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/demo/src/main/res/layout/controller_master_detail_list.xml b/demo/src/main/res/layout/controller_list.xml similarity index 94% rename from demo/src/main/res/layout/controller_master_detail_list.xml rename to demo/src/main/res/layout/controller_list.xml index e66e9d13..b71bb088 100644 --- a/demo/src/main/res/layout/controller_master_detail_list.xml +++ b/demo/src/main/res/layout/controller_list.xml @@ -6,6 +6,7 @@ android:orientation="vertical" > + diff --git a/demo/src/main/res/layout/row_detail_item.xml b/demo/src/main/res/layout/row_detail_item.xml index 0bf13483..b3666923 100644 --- a/demo/src/main/res/layout/row_detail_item.xml +++ b/demo/src/main/res/layout/row_detail_item.xml @@ -1,18 +1,30 @@ - + android:padding="24dp" + xmlns:app="http://schemas.android.com/apk/res-auto" + xmlns:tools="http://schemas.android.com/tools"> + tools:text="Item 1"/> + + + + diff --git a/demo/src/main/res/values/strings.xml b/demo/src/main/res/values/strings.xml index f359294d..a8f4433c 100644 --- a/demo/src/main/res/values/strings.xml +++ b/demo/src/main/res/values/strings.xml @@ -37,6 +37,7 @@ Rotate your device for a two pane view. + No item selected yet! transition.fab