summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--base/android/java/src/org/chromium/base/ObserverList.java177
-rw-r--r--base/android/javatests/src/org/chromium/base/ObserverListTest.java180
-rw-r--r--base/base.gyp15
-rw-r--r--content/content_tests.gypi1
4 files changed, 373 insertions, 0 deletions
diff --git a/base/android/java/src/org/chromium/base/ObserverList.java b/base/android/java/src/org/chromium/base/ObserverList.java
new file mode 100644
index 0000000..13a81c5
--- /dev/null
+++ b/base/android/java/src/org/chromium/base/ObserverList.java
@@ -0,0 +1,177 @@
+// Copyright 2013 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package org.chromium.base;
+
+import java.lang.Iterable;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+import java.util.NoSuchElementException;
+
+import javax.annotation.concurrent.NotThreadSafe;
+
+/**
+ * A container for a list of observers.
+ * <p/>
+ * This container can be modified during iteration without invalidating the iterator.
+ * So, it safely handles the case of an observer removing itself or other observers from the list
+ * while observers are being notified.
+ * <p/>
+ * The implementation (and the interface) is heavily influenced by the C++ ObserverList.
+ * Notable differences:
+ * - The iterator implements NOTIFY_EXISTING_ONLY.
+ * - The FOR_EACH_OBSERVER closure is left to the clients to implement in terms of iterator().
+ * <p/>
+ * This class is not threadsafe. Observers MUST be added, removed and will be notified on the same
+ * thread this is created.
+ */
+@NotThreadSafe
+public class ObserverList<E> implements Iterable<E> {
+ public final List<E> mObservers = new ArrayList<E>();
+ private int mIterationDepth = 0;
+
+ public ObserverList() {}
+
+ /**
+ * Add an observer to the list.
+ * <p/>
+ * An observer should not be added to the same list more than once. If an iteration is already
+ * in progress, this observer will be not be visible during that iteration.
+ */
+ public void addObserver(E obs) {
+ // Avoid adding null elements to the list as they may be removed on a compaction.
+ if (obs == null || mObservers.contains(obs)) {
+ assert false;
+ return;
+ }
+
+ // Structurally modifying the underlying list here. This means we
+ // cannot use the underlying list's iterator to iterate over the list.
+ mObservers.add(obs);
+ }
+
+ /**
+ * Remove an observer from the list if it is in the list.
+ */
+ public void removeObserver(E obs) {
+ int index = mObservers.indexOf(obs);
+
+ if (index == -1)
+ return;
+
+ if (mIterationDepth == 0) {
+ // No one is iterating over the list.
+ mObservers.remove(obs);
+ } else {
+ mObservers.set(index, null);
+ }
+ }
+
+ public boolean hasObserver(E obs) {
+ return mObservers.contains(obs);
+ }
+
+ public void clear() {
+ if (mIterationDepth == 0) {
+ mObservers.clear();
+ return;
+ }
+
+ int size = mObservers.size();
+ for (int i = 0; i < size; i++)
+ mObservers.set(i, null);
+ }
+
+ @Override
+ public Iterator<E> iterator() {
+ return new ObserverListIterator();
+ }
+
+ /**
+ * Compact the underlying list be removing null elements.
+ * <p/>
+ * Should only be called when mIterationDepth is zero.
+ */
+ private void compact() {
+ assert mIterationDepth == 0;
+ // Safe to use the underlying list's iterator, as we know that no-one else
+ // is iterating over the list.
+ Iterator<E> it = mObservers.iterator();
+ while (it.hasNext()) {
+ E el = it.next();
+ if (el == null)
+ it.remove();
+ }
+ }
+
+ private void incrementIterationDepth() {
+ mIterationDepth++;
+ }
+
+ private void decrementIterationDepthAndCompactIfNeeded() {
+ mIterationDepth--;
+ assert mIterationDepth >= 0;
+ if (mIterationDepth == 0)
+ compact();
+ }
+
+ private int getSize() {
+ return mObservers.size();
+ }
+
+ private E getObserverAt(int index) {
+ return mObservers.get(index);
+ }
+
+ private class ObserverListIterator implements Iterator<E> {
+ private final int mListEndMarker;
+ private int mIndex = 0;
+ private boolean mIsExhausted = false;
+
+ private ObserverListIterator() {
+ ObserverList.this.incrementIterationDepth();
+ mListEndMarker = ObserverList.this.getSize();
+ }
+
+ @Override
+ public boolean hasNext() {
+ int lookupIndex = mIndex;
+ while (lookupIndex < mListEndMarker &&
+ ObserverList.this.getObserverAt(lookupIndex) == null)
+ lookupIndex++;
+ if (lookupIndex < mListEndMarker)
+ return true;
+
+ // We have reached the end of the list, allow for compaction.
+ compactListIfNeeded();
+ return false;
+ }
+
+ @Override
+ public E next() {
+ // Advance if the current element is null.
+ while (mIndex < mListEndMarker && ObserverList.this.getObserverAt(mIndex) == null)
+ mIndex++;
+ if (mIndex < mListEndMarker)
+ return ObserverList.this.getObserverAt(mIndex++);
+
+ // We have reached the end of the list, allow for compaction.
+ compactListIfNeeded();
+ throw new NoSuchElementException();
+ }
+
+ @Override
+ public void remove() {
+ throw new UnsupportedOperationException();
+ }
+
+ private void compactListIfNeeded() {
+ if (!mIsExhausted) {
+ mIsExhausted = true;
+ ObserverList.this.decrementIterationDepthAndCompactIfNeeded();
+ }
+ }
+ }
+}
diff --git a/base/android/javatests/src/org/chromium/base/ObserverListTest.java b/base/android/javatests/src/org/chromium/base/ObserverListTest.java
new file mode 100644
index 0000000..10c898c
--- /dev/null
+++ b/base/android/javatests/src/org/chromium/base/ObserverListTest.java
@@ -0,0 +1,180 @@
+// Copyright 2013 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package org.chromium.base;
+
+import android.test.InstrumentationTestCase;
+import android.test.suitebuilder.annotation.SmallTest;
+
+import org.chromium.base.test.util.Feature;
+
+import java.lang.Iterable;
+import java.util.Iterator;
+import java.util.NoSuchElementException;
+
+/**
+ * Tests for (@link ObserverList}.
+ */
+public class ObserverListTest extends InstrumentationTestCase {
+ interface Observer {
+ void observe(int x);
+ }
+
+ private static class Foo implements Observer {
+ private final int mScalar;
+ private int mTotal = 0;
+
+ Foo(int scalar) {
+ mScalar = scalar;
+ }
+
+ @Override
+ public void observe(int x) {
+ mTotal += x * mScalar;
+ }
+ }
+
+ /**
+ * An observer which add a given Observer object to the list when observe is called.
+ */
+ private static class FooAdder implements Observer {
+ private final ObserverList<Observer> mList;
+ private final Observer mLucky;
+
+ FooAdder(ObserverList<Observer> list, Observer oblivious) {
+ mList = list;
+ mLucky = oblivious;
+ }
+
+ @Override
+ public void observe(int x) {
+ mList.addObserver(mLucky);
+ }
+ }
+
+ /**
+ * An observer which removes a given Observer object from the list when observe is called.
+ */
+ private static class FooRemover implements Observer {
+ private final ObserverList<Observer> mList;
+ private final Observer mDoomed;
+
+ FooRemover(ObserverList<Observer> list, Observer innocent) {
+ mList = list;
+ mDoomed = innocent;
+ }
+
+ @Override
+ public void observe(int x) {
+ mList.removeObserver(mDoomed);
+ }
+ }
+
+ private static <T> int getSizeOfIterable(Iterable<T> iterable) {
+ int num = 0;
+ for (T el : iterable)
+ num++;
+ return num;
+ }
+
+ @SmallTest
+ @Feature({"Android-AppBase"})
+ public void testRemoveWhileIteration() {
+ ObserverList<Observer> observerList = new ObserverList<Observer>();
+ Foo a = new Foo(1);
+ Foo b = new Foo(-1);
+ Foo c = new Foo(1);
+ Foo d = new Foo(-1);
+ Foo e = new Foo(-1);
+ FooRemover evil = new FooRemover(observerList, c);
+
+ observerList.addObserver(a);
+ observerList.addObserver(b);
+
+ for (Observer obs : observerList)
+ obs.observe(10);
+
+ // Removing an observer not in the list should do nothing.
+ observerList.removeObserver(e);
+
+ observerList.addObserver(evil);
+ observerList.addObserver(c);
+ observerList.addObserver(d);
+
+ for (Observer obs : observerList)
+ obs.observe(10);
+
+ // observe should be called twice on a.
+ assertEquals(20, a.mTotal);
+ // observe should be called twice on b.
+ assertEquals(-20, b.mTotal);
+ // evil removed c from the observerList before it got any callbacks.
+ assertEquals(0, c.mTotal);
+ // observe should be called once on d.
+ assertEquals(-10, d.mTotal);
+ // e was never added to the list, observe should not be called.
+ assertEquals(0, e.mTotal);
+ }
+
+ @SmallTest
+ @Feature({"Android-AppBase"})
+ public void testAddWhileIteration() {
+ ObserverList<Observer> observerList = new ObserverList<Observer>();
+ Foo a = new Foo(1);
+ Foo b = new Foo(-1);
+ Foo c = new Foo(1);
+ FooAdder evil = new FooAdder(observerList, c);
+
+ observerList.addObserver(evil);
+ observerList.addObserver(a);
+ observerList.addObserver(b);
+
+ for (Observer obs : observerList)
+ obs.observe(10);
+
+ assertTrue(observerList.hasObserver(c));
+ assertEquals(10, a.mTotal);
+ assertEquals(-10, b.mTotal);
+ assertEquals(0, c.mTotal);
+ }
+
+ @SmallTest
+ @Feature({"Android-AppBase"})
+ public void testIterator() {
+ ObserverList<Integer> observerList = new ObserverList<Integer>();
+ observerList.addObserver(5);
+ observerList.addObserver(10);
+ observerList.addObserver(15);
+ assertEquals(3, getSizeOfIterable(observerList));
+
+ observerList.removeObserver(10);
+ assertEquals(2, getSizeOfIterable(observerList));
+
+ Iterator<Integer> it = observerList.iterator();
+ assertTrue(it.hasNext());
+ assertTrue(5 == it.next());
+ assertTrue(it.hasNext());
+ assertTrue(15 == it.next());
+ assertFalse(it.hasNext());
+
+ boolean removeExceptionThrown = false;
+ try {
+ it.remove();
+ fail("Expecting UnsupportedOperationException to be thrown here.");
+ } catch (UnsupportedOperationException e) {
+ removeExceptionThrown = true;
+ }
+ assertTrue(removeExceptionThrown);
+ assertEquals(2, getSizeOfIterable(observerList));
+
+ boolean noElementExceptionThrown = false;
+ try {
+ it.next();
+ fail("Expecting NoSuchElementException to be thrown here.");
+ } catch (NoSuchElementException e) {
+ noElementExceptionThrown = true;
+ }
+ assertTrue(noElementExceptionThrown);
+ }
+}
diff --git a/base/base.gyp b/base/base.gyp
index 2dd072a..aa753c0 100644
--- a/base/base.gyp
+++ b/base/base.gyp
@@ -1119,6 +1119,9 @@
{
'target_name': 'base_java',
'type': 'none',
+ 'dependencies': [
+ '../third_party/jsr-305/jsr-305.gyp:jsr_305_javalib',
+ ],
'variables': {
'java_in_dir': '../base/android/java',
},
@@ -1135,6 +1138,18 @@
},
'includes': [ '../build/java.gypi' ],
},
+ {
+ 'target_name': 'base_javatests',
+ 'type': 'none',
+ 'dependencies': [
+ 'base_java',
+ 'base_java_test_support',
+ ],
+ 'variables': {
+ 'java_in_dir': '../base/android/javatests',
+ },
+ 'includes': [ '../build/java.gypi' ],
+ },
],
}],
['OS == "win"', {
diff --git a/content/content_tests.gypi b/content/content_tests.gypi
index 009332b..e632f8a 100644
--- a/content/content_tests.gypi
+++ b/content/content_tests.gypi
@@ -1041,6 +1041,7 @@
'content_java_test_support',
'content_shell_apk_java',
'../base/base.gyp:base_java',
+ '../base/base.gyp:base_javatests',
'../base/base.gyp:base_java_test_support',
'../media/media.gyp:media_java',
'../media/media.gyp:media_test_support',