package com.airbnb.epoxy;
import android.support.annotation.NonNull;
import java.util.AbstractList;
import java.util.ArrayList;
import java.util.Collection;
import java.util.ConcurrentModificationException;
import java.util.Iterator;
import java.util.List;
import java.util.ListIterator;
import java.util.NoSuchElementException;
/**
* Used by our {@link EpoxyAdapter} to track models. It simply wraps ArrayList and notifies an
* observer when remove or insertion operations are done on the list. This allows us to optimize
* diffing since we have a knowledge of what changed in the list.
*/
class ModelList extends ArrayList<EpoxyModel<?>> {
ModelList(int expectedModelCount) {
super(expectedModelCount);
}
ModelList() {
}
interface ModelListObserver {
void onItemRangeInserted(int positionStart, int itemCount);
void onItemRangeRemoved(int positionStart, int itemCount);
}
private boolean notificationsPaused;
private ModelListObserver observer;
void pauseNotifications() {
if (notificationsPaused) {
throw new IllegalStateException("Notifications already paused");
}
notificationsPaused = true;
}
void resumeNotifications() {
if (!notificationsPaused) {
throw new IllegalStateException("Notifications already resumed");
}
notificationsPaused = false;
}
void setObserver(ModelListObserver observer) {
this.observer = observer;
}
private void notifyInsertion(int positionStart, int itemCount) {
if (!notificationsPaused && observer != null) {
observer.onItemRangeInserted(positionStart, itemCount);
}
}
private void notifyRemoval(int positionStart, int itemCount) {
if (!notificationsPaused && observer != null) {
observer.onItemRangeRemoved(positionStart, itemCount);
}
}
@Override
public EpoxyModel<?> set(int index, EpoxyModel<?> element) {
EpoxyModel<?> previousModel = super.set(index, element);
if (previousModel.id() != element.id()) {
notifyRemoval(index, 1);
notifyInsertion(index, 1);
}
return previousModel;
}
@Override
public boolean add(EpoxyModel<?> epoxyModel) {
notifyInsertion(size(), 1);
return super.add(epoxyModel);
}
@Override
public void add(int index, EpoxyModel<?> element) {
notifyInsertion(index, 1);
super.add(index, element);
}
@Override
public boolean addAll(Collection<? extends EpoxyModel<?>> c) {
notifyInsertion(size(), c.size());
return super.addAll(c);
}
@Override
public boolean addAll(int index, Collection<? extends EpoxyModel<?>> c) {
notifyInsertion(index, c.size());
return super.addAll(index, c);
}
@Override
public EpoxyModel<?> remove(int index) {
notifyRemoval(index, 1);
return super.remove(index);
}
@Override
public boolean remove(Object o) {
int index = indexOf(o);
if (index == -1) {
return false;
}
notifyRemoval(index, 1);
super.remove(index);
return true;
}
@Override
public void clear() {
if (!isEmpty()) {
notifyRemoval(0, size());
super.clear();
}
}
@Override
protected void removeRange(int fromIndex, int toIndex) {
if (fromIndex == toIndex) {
return;
}
notifyRemoval(fromIndex, toIndex - fromIndex);
super.removeRange(fromIndex, toIndex);
}
@Override
public boolean removeAll(Collection<?> collection) {
// Using this implementation from the Android ArrayList since the Java 1.8 ArrayList
// doesn't call through to remove. Calling through to remove lets us leverage the notification
// done there
boolean result = false;
Iterator<?> it = iterator();
while (it.hasNext()) {
if (collection.contains(it.next())) {
it.remove();
result = true;
}
}
return result;
}
@Override
public boolean retainAll(Collection<?> collection) {
// Using this implementation from the Android ArrayList since the Java 1.8 ArrayList
// doesn't call through to remove. Calling through to remove lets us leverage the notification
// done there
boolean result = false;
Iterator<?> it = iterator();
while (it.hasNext()) {
if (!collection.contains(it.next())) {
it.remove();
result = true;
}
}
return result;
}
@NonNull
@Override
public Iterator<EpoxyModel<?>> iterator() {
return new Itr();
}
/**
* An Iterator implementation that calls through to the parent list's methods for modification.
* Some implementations, like the Android ArrayList.ArrayListIterator class, modify the list data
* directly instead of calling into the parent list's methods. We need the implementation to call
* the parent methods so that the proper notifications are done.
*/
private class Itr implements Iterator<EpoxyModel<?>> {
int cursor; // index of next element to return
int lastRet = -1; // index of last element returned; -1 if no such
int expectedModCount = modCount;
public boolean hasNext() {
return cursor != size();
}
@SuppressWarnings("unchecked")
public EpoxyModel<?> next() {
checkForComodification();
int i = cursor;
cursor = i + 1;
lastRet = i;
return ModelList.this.get(i);
}
public void remove() {
if (lastRet < 0) {
throw new IllegalStateException();
}
checkForComodification();
try {
ModelList.this.remove(lastRet);
cursor = lastRet;
lastRet = -1;
expectedModCount = modCount;
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}
final void checkForComodification() {
if (modCount != expectedModCount) {
throw new ConcurrentModificationException();
}
}
}
@NonNull
@Override
public ListIterator<EpoxyModel<?>> listIterator() {
return new ListItr(0);
}
@NonNull
@Override
public ListIterator<EpoxyModel<?>> listIterator(int index) {
return new ListItr(index);
}
/**
* A ListIterator implementation that calls through to the parent list's methods for modification.
* Some implementations may modify the list data directly instead of calling into the parent
* list's methods. We need the implementation to call the parent methods so that the proper
* notifications are done.
*/
private class ListItr extends Itr implements ListIterator<EpoxyModel<?>> {
ListItr(int index) {
cursor = index;
}
public boolean hasPrevious() {
return cursor != 0;
}
public int nextIndex() {
return cursor;
}
public int previousIndex() {
return cursor - 1;
}
@SuppressWarnings("unchecked")
public EpoxyModel<?> previous() {
checkForComodification();
int i = cursor - 1;
if (i < 0) {
throw new NoSuchElementException();
}
cursor = i;
lastRet = i;
return ModelList.this.get(i);
}
public void set(EpoxyModel<?> e) {
if (lastRet < 0) {
throw new IllegalStateException();
}
checkForComodification();
try {
ModelList.this.set(lastRet, e);
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}
public void add(EpoxyModel<?> e) {
checkForComodification();
try {
int i = cursor;
ModelList.this.add(i, e);
cursor = i + 1;
lastRet = -1;
expectedModCount = modCount;
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}
}
@NonNull
@Override
public List<EpoxyModel<?>> subList(int start, int end) {
if (start >= 0 && end <= size()) {
if (start <= end) {
return new SubList(this, start, end);
}
throw new IllegalArgumentException();
}
throw new IndexOutOfBoundsException();
}
/**
* A SubList implementation from Android's AbstractList class. It's copied here to make sure the
* implementation doesn't change, since some implementations, like the Java 1.8 ArrayList.SubList
* class, modify the list data directly instead of calling into the parent list's methods. We need
* the implementation to call the parent methods so that the proper notifications are done.
*/
private static class SubList extends AbstractList<EpoxyModel<?>> {
private final ModelList fullList;
private int offset;
private int size;
private static final class SubListIterator implements ListIterator<EpoxyModel<?>> {
private final SubList subList;
private final ListIterator<EpoxyModel<?>> iterator;
private int start;
private int end;
SubListIterator(ListIterator<EpoxyModel<?>> it, SubList list, int offset, int length) {
iterator = it;
subList = list;
start = offset;
end = start + length;
}
public void add(EpoxyModel<?> object) {
iterator.add(object);
subList.sizeChanged(true);
end++;
}
public boolean hasNext() {
return iterator.nextIndex() < end;
}
public boolean hasPrevious() {
return iterator.previousIndex() >= start;
}
public EpoxyModel<?> next() {
if (iterator.nextIndex() < end) {
return iterator.next();
}
throw new NoSuchElementException();
}
public int nextIndex() {
return iterator.nextIndex() - start;
}
public EpoxyModel<?> previous() {
if (iterator.previousIndex() >= start) {
return iterator.previous();
}
throw new NoSuchElementException();
}
public int previousIndex() {
int previous = iterator.previousIndex();
if (previous >= start) {
return previous - start;
}
return -1;
}
public void remove() {
iterator.remove();
subList.sizeChanged(false);
end--;
}
public void set(EpoxyModel<?> object) {
iterator.set(object);
}
}
SubList(ModelList list, int start, int end) {
fullList = list;
modCount = fullList.modCount;
offset = start;
size = end - start;
}
@Override
public void add(int location, EpoxyModel<?> object) {
if (modCount == fullList.modCount) {
if (location >= 0 && location <= size) {
fullList.add(location + offset, object);
size++;
modCount = fullList.modCount;
} else {
throw new IndexOutOfBoundsException();
}
} else {
throw new ConcurrentModificationException();
}
}
@Override
public boolean addAll(int location, Collection<? extends EpoxyModel<?>> collection) {
if (modCount == fullList.modCount) {
if (location >= 0 && location <= size) {
boolean result = fullList.addAll(location + offset, collection);
if (result) {
size += collection.size();
modCount = fullList.modCount;
}
return result;
}
throw new IndexOutOfBoundsException();
}
throw new ConcurrentModificationException();
}
@Override
public boolean addAll(@NonNull Collection<? extends EpoxyModel<?>> collection) {
if (modCount == fullList.modCount) {
boolean result = fullList.addAll(offset + size, collection);
if (result) {
size += collection.size();
modCount = fullList.modCount;
}
return result;
}
throw new ConcurrentModificationException();
}
@Override
public EpoxyModel<?> get(int location) {
if (modCount == fullList.modCount) {
if (location >= 0 && location < size) {
return fullList.get(location + offset);
}
throw new IndexOutOfBoundsException();
}
throw new ConcurrentModificationException();
}
@NonNull
@Override
public Iterator<EpoxyModel<?>> iterator() {
return listIterator(0);
}
@NonNull
@Override
public ListIterator<EpoxyModel<?>> listIterator(int location) {
if (modCount == fullList.modCount) {
if (location >= 0 && location <= size) {
return new SubListIterator(fullList.listIterator(location + offset), this, offset, size);
}
throw new IndexOutOfBoundsException();
}
throw new ConcurrentModificationException();
}
@Override
public EpoxyModel<?> remove(int location) {
if (modCount == fullList.modCount) {
if (location >= 0 && location < size) {
EpoxyModel<?> result = fullList.remove(location + offset);
size--;
modCount = fullList.modCount;
return result;
}
throw new IndexOutOfBoundsException();
}
throw new ConcurrentModificationException();
}
@Override
protected void removeRange(int start, int end) {
if (start != end) {
if (modCount == fullList.modCount) {
fullList.removeRange(start + offset, end + offset);
size -= end - start;
modCount = fullList.modCount;
} else {
throw new ConcurrentModificationException();
}
}
}
@Override
public EpoxyModel<?> set(int location, EpoxyModel<?> object) {
if (modCount == fullList.modCount) {
if (location >= 0 && location < size) {
return fullList.set(location + offset, object);
}
throw new IndexOutOfBoundsException();
}
throw new ConcurrentModificationException();
}
@Override
public int size() {
if (modCount == fullList.modCount) {
return size;
}
throw new ConcurrentModificationException();
}
void sizeChanged(boolean increment) {
if (increment) {
size++;
} else {
size--;
}
modCount = fullList.modCount;
}
}
}