Skip to content

Commit

Permalink
Safer History building
Browse files Browse the repository at this point in the history
State no longer lives in the History instance. This means building a
history is no longer unsafe. State is instead maintained by the
KeyManager, and History is just a list of keys.

States are created in the KeyManager on-demand to support traversals.

When the traversal queue is flushed we purge any stale/unused states
from the key manager.

Loading and saving state is no longer done via the History, but instead
handled directly by InternalLifecycleIntegration which pulls keys from
the current History and their States from the KeyManager; when loading,
it builds a History and pushes States into the KeyManager.

There are no longer any public APIs for loading and saving history, it's
all managed internally.

Fixes square#148
  • Loading branch information
loganj committed Feb 17, 2016
1 parent a3fb82d commit 1af06fd
Show file tree
Hide file tree
Showing 8 changed files with 133 additions and 165 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ final class BasicDispatcher implements Flow.Dispatcher {

if (traversal.origin != null) {
if (frame.getChildCount() > 0) {
traversal.origin.topSaveState().save(frame.getChildAt(0));
traversal.getState(traversal.origin.top()).save(frame.getChildAt(0));
frame.removeAllViews();
}
}
Expand All @@ -58,7 +58,7 @@ final class BasicDispatcher implements Flow.Dispatcher {
.inflate(layout, frame, false);

frame.addView(incomingView);
traversal.destination.topSaveState().restore(incomingView);
traversal.getState(traversal.destination.top()).restore(incomingView);

callback.onTraversalCompleted();
}
Expand Down
11 changes: 7 additions & 4 deletions flow/src/main/java/flow/Flow.java
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,6 @@ public final class Flow {
}
};

static final String HISTORY_KEY = InternalLifecycleIntegration.class.getSimpleName() + "_history";

public static Flow get(View view) {
return get(view.getContext());
}
Expand Down Expand Up @@ -71,7 +69,7 @@ public static Installer configure(Context baseContext, Activity activity) {

/** Adds a history as an extra to an Intent. */
public static void addHistory(Intent intent, History history, KeyParceler parceler) {
intent.putExtra(HISTORY_KEY, history.getParcelable(parceler));
InternalLifecycleIntegration.addHistoryToIntent(intent, history, parceler);
}

/**
Expand All @@ -81,7 +79,7 @@ public static void addHistory(Intent intent, History history, KeyParceler parcel
*/
public static boolean onNewIntent(Intent intent, Activity activity) {
checkArgument(intent != null, "intent may not be null");
if (intent.hasExtra(HISTORY_KEY)) {
if (intent.hasExtra(InternalLifecycleIntegration.INTENT_KEY)) {
InternalLifecycleIntegration.find(activity).onNewIntent(intent);
return true;
}
Expand Down Expand Up @@ -126,6 +124,10 @@ private Traversal(@Nullable History from, History to, Direction direction,
public Context createContext(Object key, Context baseContext) {
return new FlowContextWrapper(keyManager.findServices(key), baseContext);
}

public State getState(Object key) {
return keyManager.getState(key);
}
}

public interface Dispatcher {
Expand Down Expand Up @@ -376,6 +378,7 @@ void enqueue(PendingTraversal pendingTraversal) {
keyManager.tearDown(it.next());
it.remove();
}
keyManager.clearStatesExcept(history.asList());
} else if (dispatcher != null) {
pendingTraversal.execute();
}
Expand Down
107 changes: 17 additions & 90 deletions flow/src/main/java/flow/History.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,92 +16,35 @@

package flow;

import android.os.Bundle;
import android.os.Parcelable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.ListIterator;
import java.util.Locale;
import java.util.Map;

import static flow.Preconditions.checkArgument;
import static java.util.Collections.unmodifiableList;

/**
* Describes the history of a {@link Flow} at a specific point in time.
*/
public final class History implements Iterable<Object> {
public interface Filter {
boolean apply(Object key);
}

/** Restore a saved history from a {@link Parcelable} using the supplied {@link KeyParceler}. */
public static History from(Parcelable parcelable, KeyParceler parceler) {
Bundle bundle = (Bundle) parcelable;
ArrayList<Bundle> entryBundles = bundle.getParcelableArrayList("ENTRIES");
if (entryBundles == null) throw new AssertionError("Parcelable does not contain history");
List<State> entries = new ArrayList<>(entryBundles.size());
for (Bundle entryBundle : entryBundles) {
entries.add(State.fromBundle(entryBundle, parceler));
}
return new History(entries);
}

private final List<State> history;

/** Get a {@link Parcelable} of this history using the supplied {@link KeyParceler}. */
public Parcelable getParcelable(KeyParceler parceler) {
Bundle historyBundle = new Bundle();
ArrayList<Bundle> entryBundles = new ArrayList<>(history.size());
for (State entry : history) {
entryBundles.add(entry.toBundle(parceler));
}
if (entryBundles.isEmpty()) {
return null;
}
historyBundle.putParcelableArrayList("ENTRIES", entryBundles);
return historyBundle;
}

/**
* Get a {@link Parcelable} of this history using the supplied {@link KeyParceler}, filtered
* by the supplied {@link Filter}.
*
* The filter is invoked on each key in the stack in reverse order
*
* @return null if all keys are filtered out.
*/
public Parcelable getParcelable(KeyParceler parceler, Filter filter) {
Bundle historyBundle = new Bundle();
ArrayList<Bundle> entryBundles = new ArrayList<>(history.size());
ListIterator<State> it = history.listIterator();
while (it.hasNext()) {
State entry = it.next();
if (filter.apply(entry.getKey())) {
entryBundles.add(entry.toBundle(parceler));
}
}
if (entryBundles.isEmpty()) {
return null;
}
historyBundle.putParcelableArrayList("ENTRIES", entryBundles);
return historyBundle;
}
private final List<Object> history;

public static Builder emptyBuilder() {
return new Builder(Collections.<State>emptyList());
return new Builder(Collections.emptyList());
}

/** Create a history that contains a single key. */
public static History single(Object key) {
return emptyBuilder().push(key).build();
}

private History(List<State> history) {
private History(List<Object> history) {
checkArgument(history != null && !history.isEmpty(), "History may not be empty");
this.history = history;
}
Expand All @@ -119,25 +62,18 @@ public int size() {
}

public <T> T top() {
//noinspection unchecked
return (T) peekSaveState(0).getKey();
return peek(0);
}

/** Returns the app state at the provided index in history. 0 is the newest entry. */
public <T> T peek(int index) {
//noinspection unchecked
return (T) peekSaveState(index).getKey();
return (T) history.get(history.size() - index - 1);
}

/**
* Returns the {@link State} at the provided index in history. 0 is the newest entry.
*/
public State peekSaveState(int index) {
return history.get(history.size() - index - 1);
}

public State topSaveState() {
return peekSaveState(0);
List<Object> asList() {
final ArrayList<Object> copy = new ArrayList<>(history);
return unmodifiableList(copy);
}

/**
Expand All @@ -156,10 +92,9 @@ public Builder buildUpon() {
}

public static final class Builder {
private final List<State> history;
private final Map<Object, State> entryMemory = new LinkedHashMap<>();
private final List<Object> history;

private Builder(Collection<State> history) {
private Builder(Collection<Object> history) {
this.history = new ArrayList<>(history);
}

Expand All @@ -185,13 +120,7 @@ public Builder clear() {
* from the builder, the key's associated state will be restored.
*/
public Builder push(Object key) {
State entry = entryMemory.get(key);
if (entry == null) {
final Object key1 = key;
entry = new State(key1);
}
history.add(entry);
entryMemory.remove(key);
history.add(key);
return this;
}

Expand All @@ -206,7 +135,7 @@ public Builder pushAll(Collection<?> c) {
}

public Object peek() {
return history.isEmpty() ? null : history.get(history.size() - 1).getKey();
return history.isEmpty() ? null : history.get(history.size() - 1);
}

public boolean isEmpty() {
Expand All @@ -225,9 +154,7 @@ public Object pop() {
if (isEmpty()) {
throw new IllegalStateException("Cannot pop from an empty builder");
}
State entry = history.remove(history.size() - 1);
entryMemory.put(entry.getKey(), entry);
return entry.getKey();
return history.remove(history.size() - 1);
}

/**
Expand Down Expand Up @@ -284,9 +211,9 @@ private static class ReverseIterator<T> implements Iterator<T> {
}

private static class ReadStateIterator<T> implements Iterator<T> {
private final Iterator<State> iterator;
private final Iterator<Object> iterator;

ReadStateIterator(Iterator<State> iterator) {
ReadStateIterator(Iterator<Object> iterator) {
this.iterator = iterator;
}

Expand All @@ -296,7 +223,7 @@ private static class ReadStateIterator<T> implements Iterator<T> {

@Override public T next() {
//noinspection unchecked
return (T) iterator.next().getKey();
return (T) iterator.next();
}

@Override public void remove() {
Expand Down
86 changes: 66 additions & 20 deletions flow/src/main/java/flow/InternalLifecycleIntegration.java
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@
import android.os.Bundle;
import android.os.Parcelable;
import android.support.annotation.Nullable;
import java.util.ArrayList;
import java.util.Iterator;

import static flow.Preconditions.checkArgument;
import static flow.Preconditions.checkNotNull;
Expand All @@ -32,6 +34,9 @@
*/
public final class InternalLifecycleIntegration extends Fragment {
static final String TAG = "flow-lifecycle-integration";
static final String PERSISTENCE_KEY =
InternalLifecycleIntegration.class.getSimpleName() + "_state";
static final String INTENT_KEY = InternalLifecycleIntegration.class.getSimpleName() + "_history";

static InternalLifecycleIntegration find(Activity activity) {
return (InternalLifecycleIntegration) activity.getFragmentManager().findFragmentByTag(TAG);
Expand Down Expand Up @@ -98,24 +103,40 @@ public InternalLifecycleIntegration() {
setRetainInstance(true);
}

static void addHistoryToIntent(Intent intent, History history, KeyParceler parceler) {
Bundle bundle = new Bundle();
ArrayList<Parcelable> parcelables = new ArrayList<>(history.size());
final Iterator<Object> keys = history.reverseIterator();
while (keys.hasNext()) {
Object key = keys.next();
parcelables.add(State.empty(key).toBundle(parceler));
}
bundle.putParcelableArrayList("FLOW_STATE", parcelables);
intent.putExtra(INTENT_KEY, bundle);
}

void onNewIntent(Intent intent) {
if (intent.hasExtra(Flow.HISTORY_KEY)) {
History history = History.from(intent.getParcelableExtra(Flow.HISTORY_KEY),
checkNotNull(parceler,
"Intent has a Flow history extra, but Flow was not installed with a KeyParceler"));
flow.setHistory(history, Flow.Direction.REPLACE);
if (intent.hasExtra(INTENT_KEY)) {
checkNotNull(parceler,
"Intent has a Flow history extra, but Flow was not installed with a KeyParceler");
History.Builder builder = History.emptyBuilder();
load((Bundle) intent.getParcelableExtra(INTENT_KEY), parceler, builder, keyManager);
flow.setHistory(builder.build(), Flow.Direction.REPLACE);
}
}

@Override public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
if (flow == null) {
History savedHistory = null;
if (savedInstanceState != null && savedInstanceState.containsKey(Flow.HISTORY_KEY)) {
savedHistory = History.from(savedInstanceState.getParcelable(Flow.HISTORY_KEY),
checkNotNull(parceler, "no KeyParceler installed"));
if (savedInstanceState != null && savedInstanceState.containsKey(INTENT_KEY)) {
checkNotNull(parceler, "no KeyParceler installed");
History.Builder builder = History.emptyBuilder();
Bundle bundle = savedInstanceState.getParcelable(INTENT_KEY);
load(bundle, parceler, builder, keyManager);
savedHistory = builder.build();
}
History history = selectHistory(intent, savedHistory, defaultHistory, parceler);
History history = selectHistory(intent, savedHistory, defaultHistory, parceler, keyManager);
flow = new Flow(keyManager, history);
}
flow.setDispatcher(dispatcher);
Expand Down Expand Up @@ -148,27 +169,52 @@ void onNewIntent(Intent intent) {
return;
}

Parcelable parcelable = flow.getHistory().getParcelable(parceler, new History.Filter() {
@Override public boolean apply(Object state) {
return !state.getClass().isAnnotationPresent(NotPersistent.class);
}
});
if (parcelable != null) {
//noinspection ConstantConditions
outState.putParcelable(Flow.HISTORY_KEY, parcelable);
Bundle bundle = new Bundle();
save(bundle, parceler, flow.getHistory(), keyManager);
if (!bundle.isEmpty()) {
outState.putParcelable(INTENT_KEY, bundle);
}
}

private static History selectHistory(Intent intent, History saved, History defaultHistory,
@Nullable KeyParceler parceler) {
@Nullable KeyParceler parceler, KeyManager keyManager) {
if (saved != null) {
return saved;
}
if (intent != null && intent.hasExtra(Flow.HISTORY_KEY)) {
if (intent != null && intent.hasExtra(INTENT_KEY)) {
checkNotNull(parceler,
"Intent has a Flow history extra, but Flow was not installed with a KeyParceler");
return History.from(intent.getParcelableExtra(Flow.HISTORY_KEY), parceler);
History.Builder history = History.emptyBuilder();
load(intent.<Bundle>getParcelableExtra(INTENT_KEY), parceler, history, keyManager);
return history.build();
}
return defaultHistory;
}

private static void save(Bundle bundle, KeyParceler parceler, History history, KeyManager keyManager) {
ArrayList<Parcelable> parcelables = new ArrayList<>(history.size());
final Iterator<Object> keys = history.reverseIterator();
while (keys.hasNext()) {
Object key = keys.next();
if (!key.getClass().isAnnotationPresent(NotPersistent.class)) {
parcelables.add(keyManager.getState(key).toBundle(parceler));
}
}
bundle.putParcelableArrayList(PERSISTENCE_KEY, parcelables);
}

private static void load(Bundle bundle, KeyParceler parceler, History.Builder builder,
KeyManager keyManager) {
if (!bundle.containsKey(PERSISTENCE_KEY)) return;
ArrayList<Parcelable> stateBundles = bundle.getParcelableArrayList(PERSISTENCE_KEY);
//noinspection ConstantConditions
for (Parcelable stateBundle : stateBundles) {
State state = State.fromBundle((Bundle) stateBundle, parceler);
builder.push(state.getKey());
if (!keyManager.hasState(state.getKey())) {
keyManager.addState(state);
}
}
}

}
Loading

0 comments on commit 1af06fd

Please sign in to comment.