/* * Copyright 2016 Kejun Xia * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.shipdream.lib.android.mvc; import com.shipdream.lib.poke.exception.PokeException; import com.shipdream.lib.poke.exception.ProviderMissingException; import org.jetbrains.annotations.NotNull; import java.lang.annotation.Annotation; import java.util.ArrayList; import java.util.List; /** * A navigator consists of data for a navigation.It is created by {@link NavigationManager#navigate(Object)} */ public class Navigator { /** * The callback when the navigation is settled. Since Android Fragment doesn't invoke its call * back like onCreated, onCreateView and etc after a fragment manager commits fragment transaction, * if something needs to be done after the fragment being navigated to is ready to show * (MvcFragment.onViewReady is called), put the actions in here. */ public interface OnSettled { void run(); } private static class PendingReleaseInstance<T> { private Class<T> type; private Annotation qualifier; private T instance; } private final Object sender; private OnSettled onSettled; private NavigationManager navigationManager; private Object navigateEvent; private List<PendingReleaseInstance> pendingReleaseInstances; /** * Construct a {@link Navigator} * * @param sender Who wants to navigate * @param navigationManager The navigation manager */ Navigator(Object sender, NavigationManager navigationManager) { this.sender = sender; this.navigationManager = navigationManager; } /** * Who wants to navigate * * @return the sender */ public Object getSender() { return sender; } /** * Prepare the instance subject to being injected with no qualifier for the fragment being * navigated to. This instance will be not be released until the navigation is settled. To * config the instance try {@link #with(Class, Preparer)} or {@link #with(Class, Annotation, Preparer)} * * @param type The class type of the instance needs to be prepared * @return This navigator * @throws MvcGraphException Raised when the required injectable object cannot be injected */ public <T> Navigator with(Class<T> type) throws MvcGraphException { with(type, null, null); return this; } /** * Prepare the instance subject to being injected with no qualifier for the fragment being * navigated to. It's an equivalent way to pass arguments to the next fragment.For example, when * next fragment needs to have a pre set page title name, the controller referenced by the * fragment can be prepared here and set the title in the controller's model. Then in the * MvcFragment.onViewReady bind the value of the page title from the controller's model to the * fragment. * <p/> * <p>Example:</p> * To initialize the timer of a TimerFragment which counts down seconds,sets the initial value * of its controller by this with method. * <pre> * class TimerFragment { * @Inject * TimerController timerController; * } * * interface TimerController { * void setInitialValue(long howManySeconds); * } * * navigationManager.navigate(this).with(TimerController.class, new Preparer<TimerController>() { * @Override * public void prepare(TimerController instance) { * long fiveMinutes = 60 * 5; * instance.setInitialValue(fiveMinutes); * * //Then the value set to the controller will be guaranteed to be retained when * //TimerFragment is ready to show * } * }).to(TimerFragment.class.getName()); * </pre> * * @param type The class type of the instance needs to be prepared * @param preparer The preparer in which the injected instance will be prepared * @return This navigator * @throws MvcGraphException Raised when the required injectable object cannot be injected */ public <T> Navigator with(Class<T> type, Preparer<T> preparer) throws MvcGraphException { with(type, null, preparer); return this; } /** * Prepare the instance subject to being injected for the fragment being navigated to. It's an * equivalent way to pass arguments to the next fragment.For example, when next fragment needs * to have a pre set page title name, the controller referenced by the fragment can be prepared * here and set the title in the controller's model. Then in the MvcFragment.onViewReady bind * the value of the page title from the controller's model to the fragment. * <p/> * <p>Example:</p> * To initialize the timer of a TimerFragment which counts down seconds,sets the initial value * of its controller by this with method. * <pre> * class TimerFragment { * @Inject * TimerController timerController; * } * * interface TimerController { * void setInitialValue(long howManySeconds); * } * * navigationManager.navigate(this).with(TimerController.class, null, new Preparer<TimerController>() { * @Override * public void prepare(TimerController instance) { * long fiveMinutes = 60 * 5; * instance.setInitialValue(fiveMinutes); * * //Then the value set to the controller will be guaranteed to be retained when * //TimerFragment is ready to show * } * }).to(TimerFragment.class.getName()); * </pre> * * @param type The class type of the instance needs to be prepared * @param qualifier The qualifier * @param preparer The preparer in which the injected instance will be prepared * @return This navigator * @throws MvcGraphException Raised when the required injectable object cannot be injected */ public <T> Navigator with(Class<T> type, Annotation qualifier, Preparer<T> preparer) throws MvcGraphException { T instance; try { instance = Mvc.graph().reference(type, qualifier); } catch (PokeException e) { throw new MvcGraphException(e.getMessage(), e); } if (preparer != null) { preparer.prepare(instance); } if (pendingReleaseInstances == null) { pendingReleaseInstances = new ArrayList<>(); } PendingReleaseInstance pendingReleaseInstance = new PendingReleaseInstance(); pendingReleaseInstance.instance = instance; pendingReleaseInstance.type = type; pendingReleaseInstance.qualifier = qualifier; pendingReleaseInstances.add(pendingReleaseInstance); return this; } /** * Navigate to the location represented by the controller. Navigation only takes effect when the * given locationId is different from the current location and raises {@link NavigationManager.Event.OnLocationForward} * <p/> * <p> * To set argument for the next location navigating to, use {@link #with(Class, Annotation, Preparer)} * to prepare the controller injecting into the next fragment. * </p> * * @param controllerClass The controller of which screen the app is navigating to. */ public void to(@NotNull Class<? extends Controller> controllerClass) { doNavigateTo(controllerClass, null); go(); } /** * Navigate to the location represented by the controller. Navigation only takes effect when the * given locationId is different from the current location and raises {@link NavigationManager.Event.OnLocationForward} * <p/> * <p> * To set argument for the next location navigating to, use {@link #with(Class, Annotation, Preparer)} * to prepare the controller injecting into the next fragment. * </p> * * @param controllerClass The controller class type. * @param forwarder The configuration by {@link Forwarder} of the forward navigation. */ public void to(@NotNull Class<? extends Controller> controllerClass, @NotNull Forwarder forwarder) { doNavigateTo(controllerClass, forwarder); go(); } private void doNavigateTo(@NotNull Class<? extends Controller> controllerClass, Forwarder forwarder) { boolean clearTop = false; String clearToLocationId = null; if (forwarder != null) { clearTop = forwarder.clearHistory; clearToLocationId = forwarder.clearToLocationId; } NavLocation clearedTopToLocation = null; if (clearTop) { if (clearToLocationId != null) { //find out the top most location in the history stack with clearTopToLocationId NavLocation currentLoc = navigationManager.getModel().getCurrentLocation(); while (currentLoc != null) { if (clearToLocationId.equals(currentLoc.getLocationId())) { //Reverse the history to this location clearedTopToLocation = currentLoc; break; } currentLoc = currentLoc.getPreviousLocation(); } if (clearedTopToLocation == null) { //The location to clear up to is not found. Disable clear top. clearTop = false; } } else { clearedTopToLocation = null; } } NavLocation lastLoc = navigationManager.getModel().getCurrentLocation(); boolean locationChanged = false; String locationId = controllerClass == null ? null : controllerClass.getName(); if (clearTop) { locationChanged = true; } else { if (locationId != null) { if (lastLoc == null) { locationChanged = true; } else if (!locationId.equals(lastLoc.getLocationId())) { locationChanged = true; } } } if (locationChanged) { NavLocation currentLoc = new NavLocation(); currentLoc._setLocationId(locationId); if (forwarder != null) { currentLoc._setInterim(forwarder.interim); } if (!clearTop) { //Remember last location as previous location currentLoc._setPreviousLocation(lastLoc); } else { //Remember clear top location location as the previous location currentLoc._setPreviousLocation(clearedTopToLocation); } navigationManager.getModel().setCurrentLocation(currentLoc); navigateEvent = new NavigationManager.Event.OnLocationForward(sender, lastLoc, currentLoc, clearTop, clearedTopToLocation, this); } } /** * <p> * Navigates one step back. However, if the previous location is an interim location, it keeps * seeking backward to the first non-interim navigation location. Interim locations are set when * forward navigate to them by * * <pre> * navigationManager.navigate(this).to(SomeController.class, new Forwarder().setInterim(true)); * </pre> * </p> * * <p> * If current location is null it doesn't take any effect otherwise * raises a {@link NavigationManager.Event.OnLocationBack} event when there is a previous * location. * </p> */ public void back() { NavLocation currentLoc = navigationManager.getModel().getCurrentLocation(); if (currentLoc == null) { navigationManager.logger.warn("Current location should never be null before navigating backwards."); return; } NavLocation previousLoc = currentLoc.getPreviousLocation(); boolean needFastRewind = false; while (previousLoc != null && previousLoc.isInterim()) { previousLoc = previousLoc.getPreviousLocation(); needFastRewind = true; } if (needFastRewind) { navigateBackToLoc(previousLoc == null ? null : previousLoc.getLocationId()); } else { navigationManager.getModel().setCurrentLocation(previousLoc); navigateEvent = new NavigationManager.Event.OnLocationBack(sender, currentLoc, previousLoc, false, this); go(); } } /** * Navigates back. If current location is null it doesn't take any effect. When controllerClass * is null, navigate to the very first location and clear all history prior to it, otherwise * navigate to location with given locationId and clear history prior to it. Then a * {@link NavigationManager.Event.OnLocationBack} event will be raised. * * @param controllerClass the controller class type */ public void back(Class<? extends Controller> controllerClass) { String toLocationId = controllerClass == null ? null : controllerClass.getName(); navigateBackToLoc(toLocationId); } private void navigateBackToLoc(String toLocationId) { NavLocation currentLoc = navigationManager.getModel().getCurrentLocation(); if (currentLoc == null) { navigationManager.logger.warn("Current location should never be null before navigating backwards."); return; } if (currentLoc.getPreviousLocation() == null) { //Has already been the first location, don't do anything return; } boolean success = false; NavLocation previousLoc = currentLoc; if (toLocationId == null) { success = true; } while (currentLoc != null) { if (toLocationId != null) { if (toLocationId.equals(currentLoc.getLocationId())) { success = true; break; } } else { if (currentLoc.getPreviousLocation() == null) { break; } } currentLoc = currentLoc.getPreviousLocation(); } if (success) { navigationManager.getModel().setCurrentLocation(currentLoc); navigateEvent = new NavigationManager.Event.OnLocationBack(sender, previousLoc, currentLoc, true, this); } go(); } /** * Sets the call back when fragment being navigated to is ready to show(MvcFragment.onViewReady * is called). * * @param onSettled {@link OnSettled} call back * @return The navigator itself */ public Navigator onSettled(OnSettled onSettled) { this.onSettled = onSettled; return this; } /** * Sends out the navigation event to execute the navigation */ private void go() { if (navigateEvent != null) { navigationManager.postEvent2C(navigateEvent); if (navigationManager.logger.isTraceEnabled()) { if (navigateEvent instanceof NavigationManager.Event.OnLocationForward) { NavigationManager.Event.OnLocationForward event = (NavigationManager.Event.OnLocationForward) navigateEvent; String lastLocId = event.getLastValue() == null ? null : event.getLastValue().getLocationId(); navigationManager.logger.trace("Nav Manager: Forward: {} -> {}", lastLocId, event.getCurrentValue().getLocationId()); } } if (navigateEvent instanceof NavigationManager.Event.OnLocationBack) { if (navigationManager.logger.isTraceEnabled()) { NavigationManager.Event.OnLocationBack event = (NavigationManager.Event.OnLocationBack) navigateEvent; NavLocation lastLoc = event.getLastValue(); NavLocation currentLoc = event.getCurrentValue(); navigationManager.logger.trace("Nav Manager: Backward: {} -> {}", lastLoc.getLocationId(), currentLoc == null ? "null" : currentLoc.getLocationId()); } checkAppExit(sender); } } dumpHistory(); } /** * Internal use. Don't do it in your app. */ void destroy() { if (onSettled != null) { onSettled.run(); } if (pendingReleaseInstances != null) { for (PendingReleaseInstance i : pendingReleaseInstances) { try { Mvc.graph().dereference(i.instance, i.type, i.qualifier); } catch (ProviderMissingException e) { //should not happen //in case this happens just logs it navigationManager.logger.warn("Failed to auto release {} after navigation settled", i.type.getName()); } } } } /** * Check the app is exiting * * @param sender The sender */ private void checkAppExit(Object sender) { NavLocation curLocation = navigationManager.getModel().getCurrentLocation(); if (curLocation == null) { navigationManager.postEvent2C(new NavigationManager.Event.OnAppExit(sender)); } } /** * Prints navigation history */ private void dumpHistory() { if (navigationManager.dumpHistoryOnLocationChange) { navigationManager.logger.trace(""); navigationManager.logger.trace("Nav Controller: dump: begin ---------------------------------------------->"); NavLocation curLoc = navigationManager.getModel().getCurrentLocation(); while (curLoc != null) { navigationManager.logger.trace("Nav Controller: dump: {}({})", curLoc.getLocationId()); curLoc = curLoc.getPreviousLocation(); } navigationManager.logger.trace("Nav Controller: dump: end ---------------------------------------------->"); navigationManager.logger.trace(""); } } }