/* * Copyright 2013 Square Inc. * * 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 flow; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.Iterator; import java.util.List; import java.util.ListIterator; import java.util.Locale; 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> { private final List<Object> history; @NonNull public static Builder emptyBuilder() { return new Builder(Collections.emptyList()); } /** Create a history that contains a single key. */ @NonNull public static History single(@NonNull Object key) { return emptyBuilder().push(key).build(); } private History(List<Object> history) { checkArgument(history != null && !history.isEmpty(), "History may not be empty"); this.history = history; } @NonNull public <T> Iterator<T> reverseIterator() { return new ReadStateIterator<>(history.iterator()); } @NonNull @Override public Iterator<Object> iterator() { return new ReadStateIterator<>(new ReverseIterator<>(history)); } public int size() { return history.size(); } @NonNull public <T> T top() { return peek(0); } /** Returns the app state at the provided index in history. 0 is the newest entry. */ @NonNull public <T> T peek(int index) { //noinspection unchecked return (T) history.get(history.size() - index - 1); } @NonNull List<Object> asList() { final ArrayList<Object> copy = new ArrayList<>(history); return unmodifiableList(copy); } /** * Get a builder to modify a copy of this history. * <p> * The builder returned will retain all internal information related to the keys in the * history, including their states. It is safe to remove keys from the builder and push them back * on; nothing will be lost in those operations. */ @NonNull public Builder buildUpon() { return new Builder(history); } @Override public String toString() { return Arrays.deepToString(history.toArray()); } public static final class Builder { private final List<Object> history; private Builder(Collection<Object> history) { this.history = new ArrayList<>(history); } /** * Removes all keys from this builder. But note that if this builder was created * via {@link #buildUpon()}, any state associated with the cleared * keys will be preserved and will be restored if they are {@link #push pushed} * back on. */ @NonNull public Builder clear() { // Clear by popping everything (rather than just calling history.clear()) to // fill up entryMemory. Otherwise we drop view state on the floor. while (!isEmpty()) { pop(); } return this; } /** * Adds a key to the builder. If this builder was created via {@link #buildUpon()}, * and the pushed key was previously {@link #pop() popped} or {@link #clear cleared} * from the builder, the key's associated state will be restored. */ @NonNull public Builder push(@NonNull Object key) { history.add(key); return this; } /** * {@link #push Pushes} all of the keys in the collection onto this builder. */ @NonNull public Builder pushAll(@NonNull Collection<?> c) { for (Object key : c) { //noinspection CheckResult push(key); } return this; } /** @return null if the history is empty. */ @Nullable public Object peek() { return history.isEmpty() ? null : history.get(history.size() - 1); } @NonNull public boolean isEmpty() { return history.isEmpty(); } /** * Removes the last state added. Note that if this builder was created * via {@link #buildUpon()}, any view state associated with the popped * state will be preserved, and restored if it is {@link #push pushed} * back in. * * @throws IllegalStateException if empty */ public Object pop() { if (isEmpty()) { throw new IllegalStateException("Cannot pop from an empty builder"); } return history.remove(history.size() - 1); } /** * Pops the history until the given state is at the top. * * @throws IllegalArgumentException if the given state isn't in the history. */ @NonNull public Builder popTo(@NonNull Object state) { //noinspection ConstantConditions while (!isEmpty() && !peek().equals(state)) { pop(); } checkArgument(!isEmpty(), String.format("%s not found in history", state)); return this; } @NonNull public Builder pop(int count) { final int size = history.size(); checkArgument(count <= size, String.format((Locale) null, "Cannot pop %d elements, history only has %d", count, size)); while (count-- > 0) { pop(); } return this; } @NonNull public History build() { return new History(history); } @Override public String toString() { return Arrays.deepToString(history.toArray()); } } private static class ReverseIterator<T> implements Iterator<T> { private final ListIterator<T> wrapped; ReverseIterator(List<T> list) { wrapped = list.listIterator(list.size()); } @Override public boolean hasNext() { return wrapped.hasPrevious(); } @Override public T next() { return wrapped.previous(); } @Override public void remove() { wrapped.remove(); } } private static class ReadStateIterator<T> implements Iterator<T> { private final Iterator<Object> iterator; ReadStateIterator(Iterator<Object> iterator) { this.iterator = iterator; } @Override public boolean hasNext() { return iterator.hasNext(); } @Override public T next() { //noinspection unchecked return (T) iterator.next(); } @Override public void remove() { throw new UnsupportedOperationException(); } } }