Bug 1553354 - Add org.mozilla.gecko.MultiMap. r=snorp

Differential Revision: https://phabricator.services.mozilla.com/D57510

--HG--
extra : moz-landing-system : lando
This commit is contained in:
Agi Sferro 2019-12-17 23:24:00 +00:00
Родитель dafbe96c98
Коммит f70dd054b3
3 изменённых файлов: 422 добавлений и 38 удалений

Просмотреть файл

@ -0,0 +1,212 @@
package org.mozilla.geckoview.test;
import android.support.test.filters.MediumTest;
import android.support.test.runner.AndroidJUnit4;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mozilla.gecko.MultiMap;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Set;
import static org.junit.Assert.assertThat;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.CoreMatchers.nullValue;
@RunWith(AndroidJUnit4.class)
@MediumTest
public class MultiMapTest {
@Test
public void emptyMap() {
final MultiMap<String, String> map = new MultiMap<>();
assertThat(map.get("not-present").isEmpty(), is(true));
assertThat(map.containsKey("not-present"), is(false));
assertThat(map.containsEntry("not-present", "nope"), is(false));
assertThat(map.size(), is(0));
assertThat(map.asMap().size(), is(0));
assertThat(map.remove("not-present"), nullValue());
assertThat(map.remove("not-present", "nope"), is(false));
assertThat(map.keySet().size(), is(0));
map.clear();
}
@Test
public void emptyMapWithCapacity() {
final MultiMap<String, String> map = new MultiMap<>(10);
assertThat(map.get("not-present").isEmpty(), is(true));
assertThat(map.containsKey("not-present"), is(false));
assertThat(map.containsEntry("not-present", "nope"), is(false));
assertThat(map.size(), is(0));
assertThat(map.asMap().size(), is(0));
assertThat(map.remove("not-present"), nullValue());
assertThat(map.remove("not-present", "nope"), is(false));
assertThat(map.keySet().size(), is(0));
map.clear();
}
@Test
public void addMultipleValues() {
final MultiMap<String, String> map = new MultiMap<>();
map.add("test", "value1");
map.add("test", "value2");
map.add("test2", "value3");
assertThat(map.containsEntry("test", "value1"), is(true));
assertThat(map.containsEntry("test", "value2"), is(true));
assertThat(map.containsEntry("test2", "value3"), is(true));
assertThat(map.containsEntry("test3", "value1"), is(false));
assertThat(map.containsEntry("test", "value3"), is(false));
List<String> values = map.get("test");
assertThat(values.contains("value1"), is(true));
assertThat(values.contains("value2"), is(true));
assertThat(values.contains("value3"), is(false));
assertThat(values.size(), is(2));
List<String> values2 = map.get("test2");
assertThat(values2.contains("value1"), is(false));
assertThat(values2.contains("value2"), is(false));
assertThat(values2.contains("value3"), is(true));
assertThat(values2.size(), is(1));
assertThat(map.size(), is(2));
}
@Test
public void remove() {
final MultiMap<String, String> map = new MultiMap<>();
map.add("test", "value1");
map.add("test", "value2");
map.add("test2", "value3");
assertThat(map.size(), is(2));
List<String> values = map.remove("test");
assertThat(values.size(), is(2));
assertThat(values.contains("value1"), is(true));
assertThat(values.contains("value2"), is(true));
assertThat(map.size(), is(1));
assertThat(map.containsKey("test"), is(false));
assertThat(map.containsEntry("test", "value1"), is(false));
assertThat(map.containsEntry("test", "value2"), is(false));
assertThat(map.get("test").size(), is(0));
assertThat(map.get("test2").size(), is(1));
assertThat(map.get("test2").contains("value3"), is(true));
assertThat(map.containsEntry("test2", "value3"), is(true));
}
@Test
public void removeAllValuesRemovesKey() {
final MultiMap<String, String> map = new MultiMap<>();
map.add("test", "value1");
map.add("test", "value2");
map.add("test2", "value3");
assertThat(map.remove("test", "value1"), is(true));
assertThat(map.containsEntry("test", "value1"), is(false));
assertThat(map.containsEntry("test", "value2"), is(true));
assertThat(map.get("test").size(), is(1));
assertThat(map.get("test").contains("value2"), is(true));
assertThat(map.remove("test", "value2"), is(true));
assertThat(map.remove("test", "value3"), is(false));
assertThat(map.remove("test2", "value4"), is(false));
assertThat(map.containsKey("test"), is(false));
assertThat(map.containsKey("test2"), is(true));
}
@Test
public void keySet() {
final MultiMap<String, String> map = new MultiMap<>();
map.add("test", "value1");
map.add("test", "value2");
map.add("test2", "value3");
Set<String> keys = map.keySet();
assertThat(keys.size(), is(2));
assertThat(keys.contains("test"), is(true));
assertThat(keys.contains("test2"), is(true));
}
@Test
public void clear() {
final MultiMap<String, String> map = new MultiMap<>();
map.add("test", "value1");
map.add("test", "value2");
map.add("test2", "value3");
assertThat(map.size(), is(2));
map.clear();
assertThat(map.size(), is(0));
assertThat(map.containsKey("test"), is(false));
assertThat(map.containsKey("test2"), is(false));
assertThat(map.containsEntry("test", "value1"), is(false));
assertThat(map.containsEntry("test", "value2"), is(false));
assertThat(map.containsEntry("test2", "value3"), is(false));
}
@Test
public void asMap() {
final MultiMap<String, String> map = new MultiMap<>();
map.add("test", "value1");
map.add("test", "value2");
map.add("test2", "value3");
final Map<String, List<String>> asMap = map.asMap();
assertThat(asMap.size(), is(2));
assertThat(asMap.get("test").size(), is(2));
assertThat(asMap.get("test").contains("value1"), is(true));
assertThat(asMap.get("test").contains("value2"), is(true));
assertThat(asMap.get("test2").size(), is(1));
assertThat(asMap.get("test2").contains("value3"), is(true));
}
@Test
public void addAll() {
final MultiMap<String, String> map = new MultiMap<>();
map.add("test", "value1");
assertThat(map.get("test").size(), is(1));
// Existing key test
List<String> values = map.addAll("test", Arrays.asList("value2", "value3"));
assertThat(values.size(), is(3));
assertThat(values.contains("value1"), is(true));
assertThat(values.contains("value2"), is(true));
assertThat(values.contains("value3"), is(true));
assertThat(map.containsEntry("test", "value1"), is(true));
assertThat(map.containsEntry("test", "value2"), is(true));
assertThat(map.containsEntry("test", "value3"), is(true));
// New key test
List<String> values2 = map.addAll("test2", Arrays.asList("value4", "value5"));
assertThat(values2.size(), is(2));
assertThat(values2.contains("value4"), is(true));
assertThat(values2.contains("value5"), is(true));
assertThat(map.containsEntry("test2", "value4"), is(true));
assertThat(map.containsEntry("test2", "value5"), is(true));
}
}

Просмотреть файл

@ -22,9 +22,7 @@ import android.util.Log;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CopyOnWriteArrayList;
@RobocopTarget
@ -44,12 +42,12 @@ public final class EventDispatcher extends JNIObject {
private static final int DEFAULT_BACKGROUND_EVENTS_COUNT = 64; // Empirically measured
// GeckoBundle-based events.
private final Map<String, List<BundleEventListener>> mGeckoThreadListeners =
new HashMap<String, List<BundleEventListener>>(DEFAULT_GECKO_EVENTS_COUNT);
private final Map<String, List<BundleEventListener>> mUiThreadListeners =
new HashMap<String, List<BundleEventListener>>(DEFAULT_UI_EVENTS_COUNT);
private final Map<String, List<BundleEventListener>> mBackgroundThreadListeners =
new HashMap<String, List<BundleEventListener>>(DEFAULT_BACKGROUND_EVENTS_COUNT);
private final MultiMap<String, BundleEventListener> mGeckoThreadListeners =
new MultiMap<>(DEFAULT_GECKO_EVENTS_COUNT);
private final MultiMap<String, BundleEventListener> mUiThreadListeners =
new MultiMap<>(DEFAULT_UI_EVENTS_COUNT);
private final MultiMap<String, BundleEventListener> mBackgroundThreadListeners =
new MultiMap<>(DEFAULT_BACKGROUND_EVENTS_COUNT);
private boolean mAttachedToGecko;
private final NativeQueue mNativeQueue;
@ -104,7 +102,7 @@ public final class EventDispatcher extends JNIObject {
}
private <T> void registerListener(final Class<?> listType,
final Map<String, List<T>> listenersMap,
final MultiMap<String, T> listenersMap,
final T listener,
final String[] events) {
try {
@ -113,18 +111,10 @@ public final class EventDispatcher extends JNIObject {
if (event == null) {
continue;
}
List<T> listeners = listenersMap.get(event);
if (listeners == null) {
// Java doesn't let us put Class<? extends List<T>> as the type for listType.
@SuppressWarnings("unchecked")
final Class<? extends List<T>> type = (Class) listType;
listeners = type.newInstance();
listenersMap.put(event, listeners);
}
if (!BuildConfig.RELEASE_OR_BETA && listeners.contains(listener)) {
if (!BuildConfig.RELEASE_OR_BETA && listenersMap.containsEntry(event, listener)) {
throw new IllegalStateException("Already registered " + event);
}
listeners.add(listener);
listenersMap.add(event, listener);
}
}
} catch (final Exception e) {
@ -132,14 +122,14 @@ public final class EventDispatcher extends JNIObject {
}
}
private void checkNotRegisteredElsewhere(final Map<String, ?> allowedMap,
private void checkNotRegisteredElsewhere(final MultiMap<String, ?> allowedMap,
final String[] events) {
if (BuildConfig.RELEASE_OR_BETA) {
// for performance reasons, we only check for
// already-registered listeners in non-release builds.
return;
}
for (final Map<String, ?> listenersMap : Arrays.asList(mGeckoThreadListeners,
for (final MultiMap<String, ?> listenersMap : Arrays.asList(mGeckoThreadListeners,
mUiThreadListeners,
mBackgroundThreadListeners)) {
if (listenersMap == allowedMap) {
@ -147,7 +137,7 @@ public final class EventDispatcher extends JNIObject {
}
synchronized (listenersMap) {
for (final String event : events) {
if (listenersMap.get(event) != null) {
if (listenersMap.containsKey(event)) {
throw new IllegalStateException(
"Already registered " + event + " under a different type");
}
@ -156,7 +146,7 @@ public final class EventDispatcher extends JNIObject {
}
}
private <T> void unregisterListener(final Map<String, List<T>> listenersMap,
private <T> void unregisterListener(final MultiMap<String, T> listenersMap,
final T listener,
final String[] events) {
synchronized (listenersMap) {
@ -164,9 +154,8 @@ public final class EventDispatcher extends JNIObject {
if (event == null) {
continue;
}
List<T> listeners = listenersMap.get(event);
if ((listeners == null ||
!listeners.remove(listener)) && !BuildConfig.RELEASE_OR_BETA) {
if (!listenersMap.remove(event, listener) && !BuildConfig.RELEASE_OR_BETA) {
throw new IllegalArgumentException(event + " was not registered");
}
}
@ -272,7 +261,7 @@ public final class EventDispatcher extends JNIObject {
synchronized (mGeckoThreadListeners) {
geckoListeners = mGeckoThreadListeners.get(type);
}
if (geckoListeners != null && !geckoListeners.isEmpty()) {
if (!geckoListeners.isEmpty()) {
final boolean onGeckoThread = ThreadUtils.isOnGeckoThread();
final EventCallback wrappedCallback = JavaCallbackDelegate.wrap(callback);
@ -329,11 +318,11 @@ public final class EventDispatcher extends JNIObject {
@WrapForJNI
public boolean hasListener(final String event) {
for (final Map<String, ?> listenersMap : Arrays.asList(mGeckoThreadListeners,
for (final MultiMap<String, ?> listenersMap : Arrays.asList(mGeckoThreadListeners,
mUiThreadListeners,
mBackgroundThreadListeners)) {
synchronized (listenersMap) {
if (listenersMap.get(event) != null) {
if (listenersMap.containsKey(event)) {
return true;
}
}
@ -345,20 +334,14 @@ public final class EventDispatcher extends JNIObject {
private boolean dispatchToThread(final String type,
final GeckoBundle message,
final EventCallback callback,
final Map<String, List<BundleEventListener>> listenersMap,
final MultiMap<String, BundleEventListener> listenersMap,
final Handler thread) {
// We need to hold the lock throughout dispatching, to ensure the listeners list
// is consistent, while we iterate over it. We don't have to worry about listeners
// running for a long time while we have the lock, because the listeners will run
// on a separate thread.
synchronized (listenersMap) {
final List<BundleEventListener> listeners = listenersMap.get(type);
if (listeners == null) {
return false;
}
if (listeners.isEmpty()) {
// There were native listeners, and they're gone.
if (!listenersMap.containsKey(type)) {
return false;
}
@ -366,7 +349,7 @@ public final class EventDispatcher extends JNIObject {
final EventCallback wrappedCallback = JavaCallbackDelegate.wrap(callback);
// Event listeners will call | callback.sendError | if applicable.
for (final BundleEventListener listener : listeners) {
for (final BundleEventListener listener : listenersMap.get(type)) {
thread.post(new Runnable() {
@Override
public void run() {

Просмотреть файл

@ -0,0 +1,189 @@
package org.mozilla.gecko;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* Defines a map that holds a collection of values against each key.
*
* @param <K> Key type
* @param <T> Value type
*/
public class MultiMap<K, T> {
private HashMap<K, List<T>> mMap;
private final List<T> mEmptyList = Collections.unmodifiableList(new ArrayList<>());
/**
* Creates a MultiMap with specified initial capacity.
*
* @param count Initial capacity
*/
public MultiMap(final int count) {
mMap = new HashMap<>(count);
}
/**
* Creates a MultiMap with default initial capacity.
*/
public MultiMap() {
mMap = new HashMap<>();
}
private void ensure(final K key) {
if (!mMap.containsKey(key)) {
mMap.put(key, new ArrayList<>());
}
}
/**
* @return A map of key to the list of values associated to it
*/
public Map<K, List<T>> asMap() {
return mMap;
}
/**
* @return The number of keys present in this map
*/
public int size() {
return mMap.size();
}
/**
* @return whether this map is empty or not
*/
public boolean isEmpty() {
return mMap.isEmpty();
}
/**
* Checks if a key is present in this map.
*
* @param key the key to check
* @return True if the map contains this key, false otherwise.
*/
public boolean containsKey(final @Nullable K key) {
return mMap.containsKey(key);
}
/**
* Checks if a (key, value) pair is present in this map.
*
* @param key the key to check
* @param value the value to check
* @return true if there is a value associated to the given key, false otherwise
*/
public boolean containsEntry(final @Nullable K key, final @Nullable T value) {
if (!mMap.containsKey(key)) {
return false;
}
return mMap.get(key).contains(value);
}
/**
* Gets the values associated with the given key.
*
* @param key the key to check
* @return the list of values associated with keys, an empty list if no values are associated
* with key.
*/
@NonNull
public List<T> get(final @Nullable K key) {
if (!mMap.containsKey(key)) {
return mEmptyList;
}
return mMap.get(key);
}
/**
* Add a (key, value) mapping to this map.
*
* @param key the key to add
* @param value the value to add
*/
@Nullable
public void add(final @NonNull K key, final @NonNull T value) {
ensure(key);
mMap.get(key).add(value);
}
/**
* Add a list of values to the given key.
*
* @param key the key to add
* @param values the list of values to add
* @return the final list of values or null if no value was added
*/
@Nullable
public List<T> addAll(final @NonNull K key, final @NonNull List<T> values) {
if (values == null || values.isEmpty()) {
return null;
}
ensure(key);
final List<T> result = mMap.get(key);
result.addAll(values);
return result;
}
/**
* Remove all mappings for the given key.
*
* @param key the key
*
* @return values associated with the key or null if no values was present.
*/
@Nullable
public List<T> remove(final @Nullable K key) {
return mMap.remove(key);
}
/**
* Remove a (key, value) mapping from this map
*
* @param key the key to remove
* @param value the value to remove
*
* @return true if the (key, value) mapping was present, false otherwise
*/
@Nullable
public boolean remove(final @Nullable K key, final @Nullable T value) {
if (!mMap.containsKey(key)) {
return false;
}
List<T> values = mMap.get(key);
boolean wasPresent = values.remove(value);
if (values.isEmpty()) {
mMap.remove(key);
}
return wasPresent;
}
/**
* Remove all mappings from this map.
*/
public void clear() {
mMap.clear();
}
/**
* @return a set with all the keys for this map.
*/
@NonNull
public Set<K> keySet() {
return mMap.keySet();
}
}