diff options
author | nileshagrawal@chromium.org <nileshagrawal@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2013-03-01 21:35:34 +0000 |
---|---|---|
committer | nileshagrawal@chromium.org <nileshagrawal@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2013-03-01 21:35:34 +0000 |
commit | 2afee1373c1233260f32bccb86483b53a3a280cc (patch) | |
tree | 5fb6229161a97bb75f576a6f13c9f45bcd57dea3 /base | |
parent | 644f4fba5a0fd2b13ef21e7f76e419ae3dc9db56 (diff) | |
download | chromium_src-2afee1373c1233260f32bccb86483b53a3a280cc.zip chromium_src-2afee1373c1233260f32bccb86483b53a3a280cc.tar.gz chromium_src-2afee1373c1233260f32bccb86483b53a3a280cc.tar.bz2 |
Android: Add a java version of ObserverList.
Provides a safe container to maintain observer/listener lists which can be modified during iteration.
This is heavily based on the C++ version.
TBR=jam@chromium.org
BUG=
Review URL: https://codereview.chromium.org/12378004
git-svn-id: svn://svn.chromium.org/chrome/trunk/src@185595 0039d316-1c4b-4281-b951-d872f2087c98
Diffstat (limited to 'base')
-rw-r--r-- | base/android/java/src/org/chromium/base/ObserverList.java | 177 | ||||
-rw-r--r-- | base/android/javatests/src/org/chromium/base/ObserverListTest.java | 180 | ||||
-rw-r--r-- | base/base.gyp | 15 |
3 files changed, 372 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"', { |