package architect; import android.os.Bundle; import android.os.Parcelable; import android.util.SparseArray; import java.util.ArrayList; import java.util.List; /** * An history of scopes * * History restoration from instance state bundle is only used during app process kill * During simple configuration change, the navigator scope is preserved, and thus the history also * See NavigatorLifecycleDelegate.onCreate() to see implementation details * * History also persists its ScopeNamer instance, in order to preserve scope names * between state restoration (process kill) * * @author Lukasz Piliszczuk - lukasz.pili@gmail.com */ public class History { private static final String ENTRIES_KEY = "ENTRIES"; private static final String PATH_KEY = "PATH"; private static final String SCOPE_KEY = "SCOPE_STATE"; private static final String STATE_KEY = "VIEW_STATE"; private static final String SCOPES_NAMER_KEY = "SCOPES_IDS"; private static final String NAV_TYPE_KEY = "NAV_TYPE"; static final int NAV_TYPE_PUSH = 1; static final int NAV_TYPE_MODAL = 2; private final StackableParceler parceler; private List<Entry> entries; private ScopeNamer scopeNamer; History(StackableParceler parceler) { this.parceler = parceler; } void init(Bundle bundle) { scopeNamer = bundle.getParcelable(SCOPES_NAMER_KEY); ArrayList<Bundle> entryBundles = bundle.getParcelableArrayList(ENTRIES_KEY); if (entryBundles != null) { entries = new ArrayList<>(entryBundles.size()); if (!entryBundles.isEmpty()) { for (int i = 0; i < entryBundles.size(); i++) { entries.add(Entry.fromBundle(entryBundles.get(i), parceler)); } } } } void init(StackablePath... paths) { entries = new ArrayList<>(); scopeNamer = new ScopeNamer(); for (int i = 0; i < paths.length; i++) { add(paths[i], NAV_TYPE_PUSH); } } Bundle toBundle() { Bundle historyBundle = new Bundle(); historyBundle.putParcelable(SCOPES_NAMER_KEY, scopeNamer); if (parceler != null) { ArrayList<Bundle> entryBundles = new ArrayList<>(entries.size()); for (Entry entry : entries) { Bundle entryBundle = entry.toBundle(parceler); if (entryBundle != null) { entryBundles.add(entryBundle); } } historyBundle.putParcelableArrayList(ENTRIES_KEY, entryBundles); } return historyBundle; } boolean isEmpty() { return entries.isEmpty(); } boolean shouldInit() { return entries == null; } Entry add(StackablePath path, int navType) { if (navType == NAV_TYPE_MODAL) { // modal Preconditions.checkArgument(!entries.isEmpty(), "Cannot add modal on empty history"); } else { // push and history not empty Entry lastAlive = getLastAlive(); Preconditions.checkArgument(lastAlive == null || !lastAlive.isModal(), "Cannot push new path on modal"); } Entry entry = new Entry(scopeNamer.getName(path), path, navType); Preconditions.checkArgument(!entries.contains(entry), "An entry with the same navigation path is already present in history"); entries.add(entry); return entry; } /** * At least 2 alive entries */ boolean canKill() { if (entries.size() < 2) { return false; } int notdead = 0; for (int i = entries.size() - 1; i >= 0; i--) { if (!entries.get(i).dead) { notdead++; } if (notdead > 1) { return true; } } return false; } /** * Kill the latest alive entry * * @return the killed entry */ Entry kill() { Entry entry; for (int i = entries.size() - 1; i >= 0; i--) { entry = entries.get(i); if (!entry.dead) { entry.dead = true; return entry; } } throw new IllegalStateException("There is no entry to kill"); } /** * Kill all, including root or not * The returned entries don't include the root entry though * * @return the killed entries, in the historical order */ List<Entry> killAll(boolean rootIncluded) { List<Entry> killed = new ArrayList<>(rootIncluded ? entries.size() : entries.size() - 1); Entry entry; for (int i = entries.size() - 1; i > (rootIncluded ? -1 : 0); i--) { entry = entries.get(i); // entry can be killed from another dispatch still in process // ignore killed entries if (entry.dead) continue; entry.dead = true; if (i != 0) { // never include root killed.add(entry); } } return killed; } Entry getLastAlive() { if (entries.isEmpty()) { return null; } Entry entry; for (int i = entries.size() - 1; i >= 0; i--) { entry = entries.get(i); if (!entry.dead) { return entry; } } return null; } void remove(Entry entry) { boolean removed = entries.remove(entry); Preconditions.checkArgument(removed, "Entry to remove does not exist"); } List<Entry> removeAllDead() { if (entries.isEmpty()) { return null; } List<Entry> dead = new ArrayList<>(entries.size()); Entry entry; for (int i = 0; i < entries.size(); i++) { entry = entries.get(i); if (entry.dead) { dead.add(entry); entries.remove(i); } } return dead; } Entry getLeftOf(Entry entry) { int index = entries.indexOf(entry); Preconditions.checkArgument(index >= 0, "Get left of an entry that does not exist in history"); if (index == 0) { return null; } return entries.get(index - 1); } Entry getRoot() { Preconditions.checkArgument(entries.size() > 0, "Cannot get root on empty history"); return entries.get(0); } int indexOf(Entry entry) { return entries.indexOf(entry); } boolean existInHistory(Entry entry) { return entries.contains(entry); } List<Entry> getPreviousOfModal(Entry entry) { int index = entries.indexOf(entry); Preconditions.checkArgument(index > 0, "Invalid entry modal index in history"); List<Entry> previous = new ArrayList<>(entries.size() - index); Entry e; for (int i = index - 1; i >= 0; i--) { e = entries.get(i); previous.add(e); if (!e.isModal()) { // when we encounter non modal, return the previous stack return previous; } } throw new IllegalStateException("Invalid reach"); } static class Entry { final String scopeName; final StackablePath path; final int navType; SparseArray<Parcelable> state; boolean dead; Object returnsResult; Object receivedResult; ViewTransitionDirection direction; public Entry(String scopeName, StackablePath path, int navType) { Preconditions.checkArgument(scopeName != null && !scopeName.isEmpty(), "Scope name cannot be null nor empty"); Preconditions.checkNotNull(path, "Path cannot be null"); Preconditions.checkArgument(navType == NAV_TYPE_PUSH || navType == NAV_TYPE_MODAL, "Nav type invalid"); this.scopeName = scopeName; this.path = path; this.navType = navType; } boolean isModal() { return navType == NAV_TYPE_MODAL; } private Bundle toBundle(StackableParceler parceler) { if (dead) { // don't save dead entry // its scope will be destroyed anyway return null; } Bundle bundle = new Bundle(); bundle.putString(SCOPE_KEY, scopeName); bundle.putParcelable(PATH_KEY, parceler.wrap(path)); bundle.putSparseParcelableArray(STATE_KEY, state); bundle.putInt(NAV_TYPE_KEY, navType); return bundle; } private static Entry fromBundle(Bundle bundle, StackableParceler parceler) { StackablePath path = parceler.unwrap(bundle.getParcelable(PATH_KEY)); // new entry with new scope instance, but preserving previous scope name Entry entry = new Entry(bundle.getString(SCOPE_KEY), path, bundle.getInt(NAV_TYPE_KEY)); entry.state = bundle.getSparseParcelableArray(STATE_KEY); return entry; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; Entry entry = (Entry) o; return scopeName.equals(entry.scopeName); } @Override public int hashCode() { return scopeName.hashCode(); } } }