package architect; import android.content.Context; import android.os.Parcelable; import android.util.SparseArray; import android.view.View; import java.util.ArrayList; import java.util.List; import architect.view.HandlesViewTransition; import architect.view.HasPresenter; import mortar.MortarScope; import mortar.ViewPresenter; /** * @author Lukasz Piliszczuk - lukasz.pili@gmail.com */ class Presenter { private final Transitions transitions; private NavigatorView view; private Dispatcher.Callback dispatchingCallback; private boolean active; /** * Track the session * Each session lives during the lifespan of a navigation view instance * When a new view is provided, new unique session id is set * * id starts at 1 * negative value means there is no session (like during config changes) */ private int sessionId = 0; // private final List<History.Entry> entriesInView = new ArrayList<>(); // private final List<MortarScope> scopesInView = new ArrayList<>(); // private final List<String> presentedModalScopes = new ArrayList<>(); Presenter(Transitions transitions) { this.transitions = transitions; } void newSession() { Preconditions.checkArgument(sessionId <= 0, "New session while session id is valid"); sessionId *= -1; sessionId++; } void invalidateSession() { Preconditions.checkArgument(sessionId > 0, "Invalidate session while session id is invalid"); sessionId *= -1; } void attach(NavigatorView view) { Preconditions.checkNotNull(view, "Cannot attach null navigator view"); Preconditions.checkNull(this.view, "Current navigator view not null, did you forget to detach the previous view?"); Preconditions.checkArgument(!active, "Navigator view must be inactive before attaching"); Preconditions.checkNull(dispatchingCallback, "Dispatching callback must be null before attaching"); newSession(); this.view = view; this.view.sessionId = sessionId; } void detach() { Preconditions.checkNotNull(view, "Cannot detach null navigator view"); Preconditions.checkArgument(!active, "Navigator view must be inactive before detaching"); Preconditions.checkNull(dispatchingCallback, "Dispatching callback must be null before detaching"); invalidateSession(); view = null; } void activate() { Preconditions.checkNotNull(view, "Navigator view cannot be null, did you forget to attach?"); Preconditions.checkNull(dispatchingCallback, "Dispatching callback must be null before activating"); active = true; } void desactivate() { Preconditions.checkNotNull(view, "Navigator view cannot be null, did you forget to attach?"); active = false; completeDispatchingCallback(); } void restore(List<Dispatcher.Dispatch> dispatches) { Preconditions.checkNotNull(view, "Container view cannot be null"); Preconditions.checkArgument(view.getChildCount() == 0, "Restore requires view with no children"); if (dispatches.isEmpty()) { return; } Dispatcher.Dispatch dispatch; View child; for (int i = 0; i < dispatches.size(); i++) { dispatch = dispatches.get(i); Logger.d("Restore scope: %s", dispatch.entry.scopeName); child = dispatch.entry.path.createView(dispatch.scope.createContext(view.getContext()), view); if (dispatch.entry.state != null) { view.restoreHierarchyState(dispatch.entry.state); } view.addView(child); if (child instanceof HandlesViewTransition) { ((HandlesViewTransition) child).onViewTransition(null); } } } /** * Present several modals that will all show/hide in parallel * Once all view transitions have been executed, the dispatcher callback will complete */ void presentModals(List<Dispatcher.Dispatch> modals, final Dispatcher.Callback callback) { Preconditions.checkNotNull(view, "Container view cannot be null"); Preconditions.checkArgument(view.hasCurrentView(), "Container view must have current view"); Preconditions.checkNull(dispatchingCallback, "Previous dispatching callback not completed"); Logger.d("Present modals: %d", modals.size()); // set and track the callback from dispatcher // dispatcher is waiting for the onComplete call // either when present is done, or when presenter is desactivated dispatchingCallback = callback; // first, build the views for new modals, or reuse old ones for existing modals final List<NavigatorView.Presentation> presentations = new ArrayList<>(modals.size()); Dispatcher.Dispatch dispatch; ViewTransition viewTransition; View newView; ViewTransitionDirection direction; for (int i = 0; i < modals.size(); i++) { dispatch = modals.get(i); Logger.d("%s : %s", dispatch.entry.scopeName, dispatch.entry.dead ? "DEAD" : "ALIVE"); boolean addView; if (dispatch.entry.dead) { newView = view.getChildAt(0); addView = false; direction = ViewTransitionDirection.BACKWARD; Logger.d("Reuse view"); } else { newView = dispatch.entry.path.createView(dispatch.scope.createContext(view.getContext()), view); addView = true; direction = ViewTransitionDirection.FORWARD; Logger.d("Create new view"); } viewTransition = transitions.findTransition(view.getCurrentView(), newView, direction); presentations.add(new NavigatorView.Presentation(newView, addView, !addView, direction, viewTransition)); } // show/hide them all at once // keep track of view transitions that are finished // and complete dispatching callback one all are done Logger.d("Show presentations: %d", presentations.size()); view.showModals(presentations, new PresentationCallback() { @Override public void onPresentationFinished(int sessionId) { if (isCurrentSession(sessionId)) { completeDispatchingCallback(); } } }); } void present(final Dispatcher.Dispatch newDispatch, final History.Entry previousEntry, final ViewTransitionDirection direction, final Dispatcher.Callback callback) { Preconditions.checkNotNull(view, "Container view cannot be null"); Preconditions.checkNull(dispatchingCallback, "Previous dispatching callback not completed"); Logger.d("Present new dispatch: %s - with direction: %s", newDispatch.entry.scopeName, direction); Logger.d("Previous entry: %s", previousEntry.scopeName); // set and track the callback from dispatcher // dispatcher is waiting for the onComplete call // either when present is done, or when presenter is desactivated dispatchingCallback = callback; if (!previousEntry.dead) { // save previous view state Logger.d("Save view state for: %s", previousEntry.scopeName); previousEntry.state = getCurrentViewState(); } // create or reuse view View newView; boolean addNewView; if (!previousEntry.dead || (previousEntry.dead && !previousEntry.isModal())) { // direction == Dispatcher.Direction.FORWARD || // (direction == Dispatcher.Direction.BACKWARD && !previousEntry.isModal())) { // create new view when forward and replace // or when backward if previous entry is not modal Logger.d("Create new view for %s", newDispatch.entry.scopeName); Context context = newDispatch.scope.createContext(view.getContext()); newView = newDispatch.entry.path.createView(context, view); addNewView = true; } else { Logger.d("Reuse previous view for %s", newDispatch.entry.scopeName); newView = view.getChildAt(view.getChildCount() - 2); addNewView = false; } // find transition ViewTransition transition; if (view.hasCurrentView()) { transition = transitions.findTransition(view.getCurrentView(), newView, direction); } else { transition = null; } // restore state if it exists if (newDispatch.entry.state != null) { Logger.d("Restore view state for: %s", newDispatch.entry.scopeName); newView.restoreHierarchyState(newDispatch.entry.state); } if (newDispatch.entry.receivedResult != null) { if (newView instanceof HasPresenter) { // put result ViewPresenter viewPresenter = ((HasPresenter) newView).getPresenter(); if (viewPresenter instanceof ReceivesResult) { ((ReceivesResult) viewPresenter).onReceivedResult(newDispatch.entry.receivedResult); } } newDispatch.entry.receivedResult = null; } // boolean keepPreviousView = direction == Dispatcher.Direction.FORWARD && newDispatch.entry.isModal(); boolean keepPreviousView = !previousEntry.dead && newDispatch.entry.isModal(); Logger.d("Keep previous view: %b", keepPreviousView); view.show(new NavigatorView.Presentation(newView, addNewView, !keepPreviousView, direction, transition), new PresentationCallback() { @Override public void onPresentationFinished(int sessionId) { if (isCurrentSession(sessionId)) { completeDispatchingCallback(); } } }); } MortarScope getCurrentScope() { return view.hasCurrentView() ? MortarScope.getScope(view.getCurrentView().getContext()) : null; } boolean containerViewOnBackPressed() { return view != null && view.onBackPressed(); } private SparseArray<Parcelable> getCurrentViewState() { Preconditions.checkNotNull(view, "Container view cannot be null"); Preconditions.checkArgument(view.hasCurrentView(), "Save view state requires current view"); View view = this.view.getCurrentView(); SparseArray<Parcelable> state = new SparseArray<>(); view.saveHierarchyState(state); return state; } boolean isActive() { return view != null && active; } boolean isCurrentSession(int sessionId) { return this.sessionId == sessionId; } private void completeDispatchingCallback() { if (dispatchingCallback == null) { return; } Dispatcher.Callback callback = dispatchingCallback; dispatchingCallback = null; // onComplete redispatches direclty so we need to remove ref now callback.onComplete(); } interface PresentationCallback { void onPresentationFinished(int sessionId); } }