summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--services/core/java/com/android/server/utils/Watchable.java51
-rw-r--r--services/core/java/com/android/server/utils/WatchableImpl.java87
-rw-r--r--services/core/java/com/android/server/utils/WatchedArrayMap.java389
-rw-r--r--services/core/java/com/android/server/utils/WatchedSparseArray.java401
-rw-r--r--services/core/java/com/android/server/utils/WatchedSparseBooleanArray.java236
-rw-r--r--services/core/java/com/android/server/utils/Watcher.java35
-rw-r--r--services/tests/servicestests/src/com/android/server/utils/WatcherTest.java304
7 files changed, 1503 insertions, 0 deletions
diff --git a/services/core/java/com/android/server/utils/Watchable.java b/services/core/java/com/android/server/utils/Watchable.java
new file mode 100644
index 000000000000..7c99274f3df2
--- /dev/null
+++ b/services/core/java/com/android/server/utils/Watchable.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * 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 com.android.server.utils;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+
+/**
+ * Notify registered {@link Watcher}s when the content changes.
+ */
+public interface Watchable {
+
+ /**
+ * Ensures an observer is in the list, exactly once. The observer cannot be null. The
+ * function quietly returns if the observer is already in the list.
+ *
+ * @param observer The {@link Watcher} to be notified when the {@link Watchable} changes.
+ */
+ public void registerObserver(@NonNull Watcher observer);
+
+ /**
+ * Ensures an observer is not in the list. The observer must not be null. The function
+ * quietly returns if the objserver is not in the list.
+ *
+ * @param observer The {@link Watcher} that should not be in the notification list.
+ */
+ public void unregisterObserver(@NonNull Watcher observer);
+
+ /**
+ * Invokes {@link Watcher#onChange} on each registered observer. The method can be called
+ * with the {@link Watchable} that generated the event. In a tree of {@link Watchable}s, this
+ * is generally the first (deepest) {@link Watchable} to detect a change.
+ *
+ * @param what The {@link Watchable} that generated the event.
+ */
+ public void dispatchChange(@Nullable Watchable what);
+}
diff --git a/services/core/java/com/android/server/utils/WatchableImpl.java b/services/core/java/com/android/server/utils/WatchableImpl.java
new file mode 100644
index 000000000000..94ab1d49807f
--- /dev/null
+++ b/services/core/java/com/android/server/utils/WatchableImpl.java
@@ -0,0 +1,87 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * 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 com.android.server.utils;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+
+import java.util.ArrayList;
+import java.util.Objects;
+
+/**
+ * A concrete implementation of {@link Watchable}
+ */
+public class WatchableImpl implements Watchable {
+ /**
+ * The list of observers.
+ */
+ protected final ArrayList<Watcher> mObservers = new ArrayList<>();
+
+ /**
+ * Ensure the observer is the list. The observer cannot be null but it is okay if it
+ * is already in the list.
+ *
+ * @param observer The {@link} Watcher to be added to the notification list.
+ */
+ @Override
+ public void registerObserver(@NonNull Watcher observer) {
+ Objects.requireNonNull(observer, "observer may not be null");
+ synchronized (mObservers) {
+ if (!mObservers.contains(observer)) {
+ mObservers.add(observer);
+ }
+ }
+ }
+
+ /**
+ * Removes a previously registered observer. The observer must not be null and it
+ * must already have been registered.
+ *
+ * @param observer The {@link} Watcher to be removed from the notification list.
+ */
+ @Override
+ public void unregisterObserver(@NonNull Watcher observer) {
+ Objects.requireNonNull(observer, "observer may not be null");
+ synchronized (mObservers) {
+ mObservers.remove(observer);
+ }
+ }
+
+ /**
+ * Return the number of registered observers.
+ *
+ * @return The number of registered observers.
+ */
+ public int registeredObserverCount() {
+ return mObservers.size();
+ }
+
+ /**
+ * Invokes {@link Watcher#onChange} on each observer.
+ *
+ * @param what The {@link Watchable} that generated the event
+ */
+ @Override
+ public void dispatchChange(@Nullable Watchable what) {
+ synchronized (mObservers) {
+ final int end = mObservers.size();
+ for (int i = 0; i < end; i++) {
+ mObservers.get(i).onChange(what);
+ }
+ }
+ }
+}
diff --git a/services/core/java/com/android/server/utils/WatchedArrayMap.java b/services/core/java/com/android/server/utils/WatchedArrayMap.java
new file mode 100644
index 000000000000..7b3298086aba
--- /dev/null
+++ b/services/core/java/com/android/server/utils/WatchedArrayMap.java
@@ -0,0 +1,389 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * 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 com.android.server.utils;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+
+import android.util.ArrayMap;
+import android.util.Log;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * WatchedArrayMap is an {@link android.util.ArrayMap} that can report changes to itself. If its
+ * values are {@link Watchable} then the WatchedArrayMap will also report changes to the values.
+ * A {@link Watchable} is notified only once, no matter how many times it is stored in the array.
+ */
+public class WatchedArrayMap<K, V> extends WatchableImpl implements Map<K, V> {
+
+ // The storage
+ private final ArrayMap<K, V> mStorage;
+
+ // If true, the array is watching its children
+ private boolean mWatching = false;
+
+ // The local observer
+ private final Watcher mObserver = new Watcher() {
+ @Override
+ public void onChange(@Nullable Watchable what) {
+ WatchedArrayMap.this.dispatchChange(what);
+ }
+ };
+
+ /**
+ * A convenience function called when the elements are added to or removed from the storage.
+ * The watchable is always {@link this}.
+ */
+ private void onChanged() {
+ dispatchChange(this);
+ }
+
+ /**
+ * A convenience function. Register the object if it is {@link Watchable} and if the
+ * array is currently watching. Note that the watching flag must be true if this
+ * function is to succeed. Also note that if this is called with the same object
+ * twice, <this> is only registered once.
+ */
+ private void registerChild(Object o) {
+ if (mWatching && o instanceof Watchable) {
+ ((Watchable) o).registerObserver(mObserver);
+ }
+ }
+
+ /**
+ * A convenience function. Unregister the object if it is {@link Watchable} and if the
+ * array is currently watching. This unconditionally removes the object from the
+ * registered list.
+ */
+ private void unregisterChild(Object o) {
+ if (mWatching && o instanceof Watchable) {
+ ((Watchable) o).unregisterObserver(mObserver);
+ }
+ }
+
+ /**
+ * A convenience function. Unregister the object if it is {@link Watchable}, if the
+ * array is currently watching, and if there are no other instances of this object in
+ * the storage. Note that the watching flag must be true if this function is to
+ * succeed. The object must already have been removed from the storage before this
+ * method is called.
+ */
+ private void unregisterChildIf(Object o) {
+ if (mWatching && o instanceof Watchable) {
+ if (!mStorage.containsValue(o)) {
+ ((Watchable) o).unregisterObserver(mObserver);
+ }
+ }
+ }
+
+ /**
+ * Register a {@link Watcher} with the array. If this is the first Watcher than any
+ * array values that are {@link Watchable} are registered to the array itself.
+ */
+ @Override
+ public void registerObserver(@NonNull Watcher observer) {
+ super.registerObserver(observer);
+ if (registeredObserverCount() == 1) {
+ // The watching flag must be set true before any children are registered.
+ mWatching = true;
+ final int end = mStorage.size();
+ for (int i = 0; i < end; i++) {
+ registerChild(mStorage.valueAt(i));
+ }
+ }
+ }
+
+ /**
+ * Unregister a {@link Watcher} from the array. If this is the last Watcher than any
+ * array values that are {@link Watchable} are unregistered to the array itself.
+ */
+ @Override
+ public void unregisterObserver(@NonNull Watcher observer) {
+ super.unregisterObserver(observer);
+ if (registeredObserverCount() == 0) {
+ final int end = mStorage.size();
+ for (int i = 0; i < end; i++) {
+ unregisterChild(mStorage.valueAt(i));
+ }
+ // The watching flag must be true while children are unregistered.
+ mWatching = false;
+ }
+ }
+
+ /**
+ * Create a new empty {@link WatchedArrayMap}. The default capacity of an array map
+ * is 0, and will grow once items are added to it.
+ */
+ public WatchedArrayMap() {
+ this(0, false);
+ }
+
+ /**
+ * Create a new {@link WatchedArrayMap} with a given initial capacity.
+ */
+ public WatchedArrayMap(int capacity) {
+ this(capacity, false);
+ }
+
+ /** {@hide} */
+ public WatchedArrayMap(int capacity, boolean identityHashCode) {
+ mStorage = new ArrayMap<K, V>(capacity, identityHashCode);
+ }
+
+ /**
+ * Create a new {@link WatchedArrayMap} with the mappings from the given {@link Map}.
+ */
+ public WatchedArrayMap(@Nullable Map<? extends K, ? extends V> map) {
+ mStorage = new ArrayMap<K, V>();
+ if (map != null) {
+ putAll(map);
+ }
+ }
+
+ /**
+ * Return the underlying storage. This breaks the wrapper but is necessary when
+ * passing the array to distant methods.
+ */
+ public ArrayMap untrackedMap() {
+ return mStorage;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void clear() {
+ // The storage cannot be simply cleared. Each element in the storage must be
+ // unregistered. Deregistration is only needed if the array is actually
+ // watching.
+ if (mWatching) {
+ final int end = mStorage.size();
+ for (int i = 0; i < end; i++) {
+ unregisterChild(mStorage.valueAt(i));
+ }
+ }
+ mStorage.clear();
+ onChanged();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public boolean containsKey(Object key) {
+ return mStorage.containsKey(key);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public boolean containsValue(Object value) {
+ return mStorage.containsValue(value);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public Set<Map.Entry<K, V>> entrySet() {
+ return Collections.unmodifiableSet(mStorage.entrySet());
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public boolean equals(Object o) {
+ if (o instanceof WatchedArrayMap) {
+ WatchedArrayMap w = (WatchedArrayMap) o;
+ return mStorage.equals(w.mStorage);
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public V get(Object key) {
+ return mStorage.get(key);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public int hashCode() {
+ return mStorage.hashCode();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public boolean isEmpty() {
+ return mStorage.isEmpty();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public Set<K> keySet() {
+ return Collections.unmodifiableSet(mStorage.keySet());
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public V put(K key, V value) {
+ final V result = mStorage.put(key, value);
+ registerChild(value);
+ onChanged();
+ return result;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void putAll(@NonNull Map<? extends K, ? extends V> map) {
+ for (Map.Entry<? extends K, ? extends V> element : map.entrySet()) {
+ put(element.getKey(), element.getValue());
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public V remove(@NonNull Object key) {
+ final V result = mStorage.remove(key);
+ unregisterChildIf(result);
+ onChanged();
+ return result;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public int size() {
+ return mStorage.size();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public Collection<V> values() {
+ return Collections.unmodifiableCollection(mStorage.values());
+ }
+
+ // Methods supported by ArrayMap that are not part of Map
+
+ /**
+ * Return the key at the given index in the array.
+ *
+ * <p>For indices outside of the range <code>0...size()-1</code>, an
+ * {@link ArrayIndexOutOfBoundsException} is thrown.</p>
+ *
+ * @param index The desired index, must be between 0 and {@link #size()}-1.
+ * @return Returns the key stored at the given index.
+ */
+ public K keyAt(int index) {
+ return mStorage.keyAt(index);
+ }
+
+ /**
+ * Return the value at the given index in the array.
+ *
+ * <p>For indices outside of the range <code>0...size()-1</code>, an
+ * {@link ArrayIndexOutOfBoundsException} is thrown.</p>
+ *
+ * @param index The desired index, must be between 0 and {@link #size()}-1.
+ * @return Returns the value stored at the given index.
+ */
+ public V valueAt(int index) {
+ return mStorage.valueAt(index);
+ }
+
+ /**
+ * Remove an existing key from the array map.
+ * @param key The key of the mapping to remove.
+ * @return Returns the value that was stored under the key, or null if there
+ * was no such key.
+ */
+ public int indexOfKey(K key) {
+ return mStorage.indexOfKey(key);
+ }
+
+ /**
+ * Returns an index for which {@link #valueAt} would return the
+ * specified value, or a negative number if no keys map to the
+ * specified value.
+ * Beware that this is a linear search, unlike lookups by key,
+ * and that multiple keys can map to the same value and this will
+ * find only one of them.
+ */
+ public int indexOfValue(V value) {
+ return mStorage.indexOfValue(value);
+ }
+
+ /**
+ * Set the value at a given index in the array.
+ *
+ * <p>For indices outside of the range <code>0...size()-1</code>, an
+ * {@link ArrayIndexOutOfBoundsException} is thrown.</p>
+ *
+ * @param index The desired index, must be between 0 and {@link #size()}-1.
+ * @param value The new value to store at this index.
+ * @return Returns the previous value at the given index.
+ */
+ public V setValueAt(int index, V value) {
+ final V result = mStorage.setValueAt(index, value);
+ if (value != result) {
+ unregisterChildIf(result);
+ registerChild(value);
+ onChanged();
+ }
+ return result;
+ }
+
+ /**
+ * Remove the key/value mapping at the given index.
+ *
+ * <p>For indices outside of the range <code>0...size()-1</code>, an
+ * {@link ArrayIndexOutOfBoundsException} is thrown.</p>
+ *
+ * @param index The desired index, must be between 0 and {@link #size()}-1.
+ * @return Returns the value that was stored at this index.
+ */
+ public V removeAt(int index) {
+ final V result = mStorage.removeAt(index);
+ unregisterChildIf(result);
+ onChanged();
+ return result;
+ }
+}
diff --git a/services/core/java/com/android/server/utils/WatchedSparseArray.java b/services/core/java/com/android/server/utils/WatchedSparseArray.java
new file mode 100644
index 000000000000..4d43b682e113
--- /dev/null
+++ b/services/core/java/com/android/server/utils/WatchedSparseArray.java
@@ -0,0 +1,401 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * 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 com.android.server.utils;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+
+import android.util.SparseArray;
+
+import java.util.ArrayList;
+
+/**
+ * A watched variant of SparseArray. If a {@link Watchable} is stored in the array, the
+ * array registers with the {@link Watchable}. The array registers only once with each
+ * {@link Watchable} no matter how many times the {@link Watchable} is stored in the
+ * array.
+ */
+public class WatchedSparseArray<E> extends WatchableImpl {
+
+ // The storage
+ private final SparseArray<E> mStorage;
+
+ // If true, the array is watching its children
+ private boolean mWatching = false;
+
+ // The local observer
+ private final Watcher mObserver = new Watcher() {
+ @Override
+ public void onChange(@Nullable Watchable o) {
+ WatchedSparseArray.this.dispatchChange(o);
+ }
+ };
+
+ /**
+ * A private convenience function that notifies registered listeners that an element
+ * has been added to or removed from the array. The what parameter is the array itself.
+ */
+ private void onChanged() {
+ dispatchChange(this);
+ }
+
+ /**
+ * A convenience function. Register the object if it is {@link Watchable} and if the
+ * array is currently watching. Note that the watching flag must be true if this
+ * function is to succeed.
+ */
+ private void registerChild(Object o) {
+ if (mWatching && o instanceof Watchable) {
+ ((Watchable) o).registerObserver(mObserver);
+ }
+ }
+
+ /**
+ * A convenience function. Unregister the object if it is {@link Watchable} and if
+ * the array is currently watching. Note that the watching flag must be true if this
+ * function is to succeed.
+ */
+ private void unregisterChild(Object o) {
+ if (mWatching && o instanceof Watchable) {
+ ((Watchable) o).unregisterObserver(mObserver);
+ }
+ }
+
+ /**
+ * A convenience function. Unregister the object if it is {@link Watchable}, if the array is
+ * currently watching, and if the storage does not contain the object. Note that the watching
+ * flag must be true if this function is to succeed. This must be called after an object has
+ * been removed from the storage.
+ */
+ private void unregisterChildIf(Object o) {
+ if (mWatching && o instanceof Watchable) {
+ if (mStorage.indexOfValue((E) o) == -1) {
+ ((Watchable) o).unregisterObserver(mObserver);
+ }
+ }
+ }
+
+ /**
+ * Register a {@link Watcher} with the array. If this is the first Watcher than any
+ * array values that are {@link Watchable} are registered to the array itself.
+ */
+ @Override
+ public void registerObserver(@NonNull Watcher observer) {
+ super.registerObserver(observer);
+ if (registeredObserverCount() == 1) {
+ // The watching flag must be set true before any children are registered.
+ mWatching = true;
+ final int end = mStorage.size();
+ for (int i = 0; i < end; i++) {
+ registerChild(mStorage.valueAt(i));
+ }
+ }
+ }
+
+ /**
+ * Unregister a {@link Watcher} from the array. If this is the last Watcher than any
+ * array values that are {@link Watchable} are unregistered to the array itself.
+ */
+ @Override
+ public void unregisterObserver(@NonNull Watcher observer) {
+ super.unregisterObserver(observer);
+ if (registeredObserverCount() == 0) {
+ final int end = mStorage.size();
+ for (int i = 0; i < end; i++) {
+ unregisterChild(mStorage.valueAt(i));
+ }
+ // The watching flag must be true while children are unregistered.
+ mWatching = false;
+ }
+ }
+
+ /**
+ * Creates a new WatchedSparseArray containing no mappings.
+ */
+ public WatchedSparseArray() {
+ mStorage = new SparseArray();
+ }
+
+ /**
+ * Creates a new WatchedSparseArray containing no mappings that
+ * will not require any additional memory allocation to store the
+ * specified number of mappings. If you supply an initial capacity of
+ * 0, the sparse array will be initialized with a light-weight
+ * representation not requiring any additional array allocations.
+ */
+ public WatchedSparseArray(int initialCapacity) {
+ mStorage = new SparseArray(initialCapacity);
+ }
+
+ /**
+ * The copy constructor does not copy the watcher data.
+ */
+ public WatchedSparseArray(@NonNull WatchedSparseArray<E> r) {
+ mStorage = r.mStorage.clone();
+ }
+
+ /**
+ * Returns true if the key exists in the array. This is equivalent to
+ * {@link #indexOfKey(int)} >= 0.
+ *
+ * @param key Potential key in the mapping
+ * @return true if the key is defined in the mapping
+ */
+ public boolean contains(int key) {
+ return mStorage.contains(key);
+ }
+
+ /**
+ * Gets the Object mapped from the specified key, or <code>null</code>
+ * if no such mapping has been made.
+ */
+ public E get(int key) {
+ return mStorage.get(key);
+ }
+
+ /**
+ * Gets the Object mapped from the specified key, or the specified Object
+ * if no such mapping has been made.
+ */
+ public E get(int key, E valueIfKeyNotFound) {
+ return mStorage.get(key, valueIfKeyNotFound);
+ }
+
+ /**
+ * Removes the mapping from the specified key, if there was any.
+ */
+ public void delete(int key) {
+ final E child = mStorage.get(key);
+ mStorage.delete(key);
+ unregisterChildIf(child);
+ onChanged();
+ }
+
+ /**
+ * @hide
+ * Removes the mapping from the specified key, if there was any, returning the old value.
+ */
+ public E removeReturnOld(int key) {
+ final E result = mStorage.removeReturnOld(key);
+ unregisterChildIf(result);
+ return result;
+ }
+
+ /**
+ * Alias for {@link #delete(int)}.
+ */
+ public void remove(int key) {
+ delete(key);
+ }
+
+ /**
+ * Removes the mapping at the specified index.
+ *
+ * <p>For indices outside of the range <code>0...size()-1</code>, an
+ * {@link ArrayIndexOutOfBoundsException} is thrown.</p>
+ */
+ public void removeAt(int index) {
+ final E child = mStorage.valueAt(index);
+ mStorage.removeAt(index);
+ unregisterChildIf(child);
+ onChanged();
+ }
+
+ /**
+ * Remove a range of mappings as a batch.
+ *
+ * @param index Index to begin at
+ * @param size Number of mappings to remove
+ *
+ * <p>For indices outside of the range <code>0...size()-1</code>,
+ * the behavior is undefined.</p>
+ */
+ public void removeAtRange(int index, int size) {
+ final ArrayList<E> children = new ArrayList<>();
+ try {
+ for (int i = 0; i < size; i++) {
+ children.add(mStorage.valueAt(i + index));
+ }
+ } catch (Exception e) {
+ // Ignore any exception and proceed with removal.
+ }
+ try {
+ mStorage.removeAtRange(index, size);
+ } finally {
+ // Even on exception, make sure to deregister children that have been
+ // removed.
+ for (int i = 0; i < size; i++) {
+ unregisterChildIf(children.get(i));
+ }
+ }
+ onChanged();
+ }
+
+ /**
+ * Adds a mapping from the specified key to the specified value,
+ * replacing the previous mapping from the specified key if there
+ * was one.
+ */
+ public void put(int key, E value) {
+ final E old = mStorage.get(key);
+ mStorage.put(key, value);
+ unregisterChildIf(old);
+ registerChild(value);
+ onChanged();
+ }
+
+ /**
+ * Returns the number of key-value mappings that this SparseArray
+ * currently stores.
+ */
+ public int size() {
+ return mStorage.size();
+ }
+
+ /**
+ * Given an index in the range <code>0...size()-1</code>, returns
+ * the key from the <code>index</code>th key-value mapping that this
+ * SparseArray stores.
+ *
+ * <p>The keys corresponding to indices in ascending order are guaranteed to
+ * be in ascending order, e.g., <code>keyAt(0)</code> will return the
+ * smallest key and <code>keyAt(size()-1)</code> will return the largest
+ * key.</p>
+ *
+ * <p>For indices outside of the range <code>0...size()-1</code>,
+ * the behavior is undefined for apps targeting {@link android.os.Build.VERSION_CODES#P} and
+ * earlier, and an {@link ArrayIndexOutOfBoundsException} is thrown for apps targeting
+ * {@link android.os.Build.VERSION_CODES#Q} and later.</p>
+ */
+ public int keyAt(int index) {
+ return mStorage.keyAt(index);
+ }
+
+ /**
+ * Given an index in the range <code>0...size()-1</code>, returns
+ * the value from the <code>index</code>th key-value mapping that this
+ * SparseArray stores.
+ *
+ * <p>The values corresponding to indices in ascending order are guaranteed
+ * to be associated with keys in ascending order, e.g.,
+ * <code>valueAt(0)</code> will return the value associated with the
+ * smallest key and <code>valueAt(size()-1)</code> will return the value
+ * associated with the largest key.</p>
+ *
+ * <p>For indices outside of the range <code>0...size()-1</code>,
+ * the behavior is undefined for apps targeting {@link android.os.Build.VERSION_CODES#P} and
+ * earlier, and an {@link ArrayIndexOutOfBoundsException} is thrown for apps targeting
+ * {@link android.os.Build.VERSION_CODES#Q} and later.</p>
+ */
+ public E valueAt(int index) {
+ return mStorage.valueAt(index);
+ }
+
+ /**
+ * Given an index in the range <code>0...size()-1</code>, sets a new
+ * value for the <code>index</code>th key-value mapping that this
+ * SparseArray stores.
+ *
+ * <p>For indices outside of the range <code>0...size()-1</code>, the behavior is undefined for
+ * apps targeting {@link android.os.Build.VERSION_CODES#P} and earlier, and an
+ * {@link ArrayIndexOutOfBoundsException} is thrown for apps targeting
+ * {@link android.os.Build.VERSION_CODES#Q} and later.</p>
+ */
+ public void setValueAt(int index, E value) {
+ final E old = mStorage.valueAt(index);
+ mStorage.setValueAt(index, value);
+ if (value != old) {
+ unregisterChildIf(old);
+ registerChild(value);
+ onChanged();
+ }
+ }
+
+ /**
+ * Returns the index for which {@link #keyAt} would return the
+ * specified key, or a negative number if the specified
+ * key is not mapped.
+ */
+ public int indexOfKey(int key) {
+ return mStorage.indexOfKey(key);
+ }
+
+ /**
+ * Returns an index for which {@link #valueAt} would return the
+ * specified value, or a negative number if no keys map to the
+ * specified value.
+ * <p>Beware that this is a linear search, unlike lookups by key,
+ * and that multiple keys can map to the same value and this will
+ * find only one of them.
+ * <p>Note also that unlike most collections' {@code indexOf} methods,
+ * this method compares values using {@code ==} rather than {@code equals}.
+ */
+ public int indexOfValue(E value) {
+ return mStorage.indexOfValue(value);
+ }
+
+ /**
+ * Returns an index for which {@link #valueAt} would return the
+ * specified value, or a negative number if no keys map to the
+ * specified value.
+ * <p>Beware that this is a linear search, unlike lookups by key,
+ * and that multiple keys can map to the same value and this will
+ * find only one of them.
+ * <p>Note also that this method uses {@code equals} unlike {@code indexOfValue}.
+ * @hide
+ */
+ public int indexOfValueByValue(E value) {
+ return mStorage.indexOfValueByValue(value);
+ }
+
+ /**
+ * Removes all key-value mappings from this SparseArray.
+ */
+ public void clear() {
+ // The storage cannot be simply cleared. Each element in the storage must be
+ // unregistered. Deregistration is only needed if the array is actually
+ // watching.
+ if (mWatching) {
+ final int end = mStorage.size();
+ for (int i = 0; i < end; i++) {
+ unregisterChild(mStorage.valueAt(i));
+ }
+ }
+ mStorage.clear();
+ onChanged();
+ }
+
+ /**
+ * Puts a key/value pair into the array, optimizing for the case where
+ * the key is greater than all existing keys in the array.
+ */
+ public void append(int key, E value) {
+ mStorage.append(key, value);
+ registerChild(value);
+ onChanged();
+ }
+
+ /**
+ * <p>This implementation composes a string by iterating over its mappings. If
+ * this map contains itself as a value, the string "(this Map)"
+ * will appear in its place.
+ */
+ @Override
+ public String toString() {
+ return mStorage.toString();
+ }
+}
diff --git a/services/core/java/com/android/server/utils/WatchedSparseBooleanArray.java b/services/core/java/com/android/server/utils/WatchedSparseBooleanArray.java
new file mode 100644
index 000000000000..edf7d27b61dd
--- /dev/null
+++ b/services/core/java/com/android/server/utils/WatchedSparseBooleanArray.java
@@ -0,0 +1,236 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * 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 com.android.server.utils;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+
+import android.util.SparseBooleanArray;
+
+/**
+ * A watched variant of SparseBooleanArray. Changes to the array are notified to
+ * registered {@link Watcher}s.
+ */
+public class WatchedSparseBooleanArray extends WatchableImpl {
+
+ // The storage
+ private final SparseBooleanArray mStorage;
+
+ // A private convenience function
+ private void dispatchChange() {
+ dispatchChange(this);
+ }
+
+ /**
+ * Creates a new WatchedSparseBooleanArray containing no mappings.
+ */
+ public WatchedSparseBooleanArray() {
+ mStorage = new SparseBooleanArray();
+ }
+
+ /**
+ * Creates a new WatchedSparseBooleanArray containing no mappings that
+ * will not require any additional memory allocation to store the
+ * specified number of mappings. If you supply an initial capacity of
+ * 0, the sparse array will be initialized with a light-weight
+ * representation not requiring any additional array allocations.
+ */
+ public WatchedSparseBooleanArray(int initialCapacity) {
+ mStorage = new SparseBooleanArray(initialCapacity);
+ }
+
+ /**
+ * The copy constructor does not copy the watcher data.
+ */
+ public WatchedSparseBooleanArray(@NonNull WatchedSparseBooleanArray r) {
+ mStorage = r.mStorage.clone();
+ }
+
+ /**
+ * Gets the boolean mapped from the specified key, or <code>false</code>
+ * if no such mapping has been made.
+ */
+ public boolean get(int key) {
+ return mStorage.get(key);
+ }
+
+ /**
+ * Gets the boolean mapped from the specified key, or the specified value
+ * if no such mapping has been made.
+ */
+ public boolean get(int key, boolean valueIfKeyNotFound) {
+ return mStorage.get(key, valueIfKeyNotFound);
+ }
+
+ /**
+ * Removes the mapping from the specified key, if there was any.
+ */
+ public void delete(int key) {
+ mStorage.delete(key);
+ dispatchChange();
+ }
+
+ /**
+ * Removes the mapping at the specified index.
+ * <p>
+ * For indices outside of the range {@code 0...size()-1}, the behavior is undefined.
+ */
+ public void removeAt(int index) {
+ mStorage.removeAt(index);
+ dispatchChange();
+ }
+
+ /**
+ * Adds a mapping from the specified key to the specified value,
+ * replacing the previous mapping from the specified key if there
+ * was one.
+ */
+ public void put(int key, boolean value) {
+ if (mStorage.get(key) != value) {
+ mStorage.put(key, value);
+ dispatchChange();
+ }
+ }
+
+ /**
+ * Returns the number of key-value mappings that this SparseBooleanArray
+ * currently stores.
+ */
+ public int size() {
+ return mStorage.size();
+ }
+
+ /**
+ * Given an index in the range <code>0...size()-1</code>, returns
+ * the key from the <code>index</code>th key-value mapping that this
+ * SparseBooleanArray stores.
+ *
+ * <p>The keys corresponding to indices in ascending order are guaranteed to
+ * be in ascending order, e.g., <code>keyAt(0)</code> will return the
+ * smallest key and <code>keyAt(size()-1)</code> will return the largest
+ * key.</p>
+ *
+ * <p>For indices outside of the range <code>0...size()-1</code>, the behavior is undefined for
+ * apps targeting {@link android.os.Build.VERSION_CODES#P} and earlier, and an
+ * {@link ArrayIndexOutOfBoundsException} is thrown for apps targeting
+ * {@link android.os.Build.VERSION_CODES#Q} and later.</p>
+ */
+ public int keyAt(int index) {
+ return mStorage.keyAt(index);
+ }
+
+ /**
+ * Given an index in the range <code>0...size()-1</code>, returns
+ * the value from the <code>index</code>th key-value mapping that this
+ * SparseBooleanArray stores.
+ *
+ * <p>The values corresponding to indices in ascending order are guaranteed
+ * to be associated with keys in ascending order, e.g.,
+ * <code>valueAt(0)</code> will return the value associated with the
+ * smallest key and <code>valueAt(size()-1)</code> will return the value
+ * associated with the largest key.</p>
+ *
+ * <p>For indices outside of the range <code>0...size()-1</code>, the behavior is undefined for
+ * apps targeting {@link android.os.Build.VERSION_CODES#P} and earlier, and an
+ * {@link ArrayIndexOutOfBoundsException} is thrown for apps targeting
+ * {@link android.os.Build.VERSION_CODES#Q} and later.</p>
+ */
+ public boolean valueAt(int index) {
+ return mStorage.valueAt(index);
+ }
+
+ /**
+ * Directly set the value at a particular index.
+ *
+ * <p>For indices outside of the range <code>0...size()-1</code>, the behavior is undefined for
+ * apps targeting {@link android.os.Build.VERSION_CODES#P} and earlier, and an
+ * {@link ArrayIndexOutOfBoundsException} is thrown for apps targeting
+ * {@link android.os.Build.VERSION_CODES#Q} and later.</p>
+ */
+ public void setValueAt(int index, boolean value) {
+ if (mStorage.valueAt(index) != value) {
+ mStorage.setValueAt(index, value);
+ dispatchChange();
+ }
+ }
+
+ /** @hide */
+ public void setKeyAt(int index, int key) {
+ if (mStorage.keyAt(index) != key) {
+ mStorage.setKeyAt(index, key);
+ dispatchChange();
+ }
+ }
+
+ /**
+ * Returns the index for which {@link #keyAt} would return the
+ * specified key, or a negative number if the specified
+ * key is not mapped.
+ */
+ public int indexOfKey(int key) {
+ return mStorage.indexOfKey(key);
+ }
+
+ /**
+ * Returns an index for which {@link #valueAt} would return the
+ * specified key, or a negative number if no keys map to the
+ * specified value.
+ * Beware that this is a linear search, unlike lookups by key,
+ * and that multiple keys can map to the same value and this will
+ * find only one of them.
+ */
+ public int indexOfValue(boolean value) {
+ return mStorage.indexOfValue(value);
+ }
+
+ /**
+ * Removes all key-value mappings from this SparseBooleanArray.
+ */
+ public void clear() {
+ mStorage.clear();
+ dispatchChange();
+ }
+
+ /**
+ * Puts a key/value pair into the array, optimizing for the case where
+ * the key is greater than all existing keys in the array.
+ */
+ public void append(int key, boolean value) {
+ mStorage.append(key, value);
+ dispatchChange();
+ }
+
+ @Override
+ public int hashCode() {
+ return mStorage.hashCode();
+ }
+
+ @Override
+ public boolean equals(Object that) {
+ return this == that || mStorage.equals(that);
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * <p>This implementation composes a string by iterating over its mappings.
+ */
+ @Override
+ public String toString() {
+ return mStorage.toString();
+ }
+}
diff --git a/services/core/java/com/android/server/utils/Watcher.java b/services/core/java/com/android/server/utils/Watcher.java
new file mode 100644
index 000000000000..a9bfbfa0c7dc
--- /dev/null
+++ b/services/core/java/com/android/server/utils/Watcher.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * 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 com.android.server.utils;
+
+import android.annotation.Nullable;
+
+/**
+ * This class receives notifications when watched objects detect changes.
+ * Must be implemented by objects which are registered to a {@link Watchable}.
+ */
+public abstract class Watcher {
+
+ /**
+ * This method is called when {@link Watchable} detects a change. The <what>
+ * parameter indicates what changed, but it may be null. The parameter is intended
+ * for debugging.
+ *
+ * @param what The {@link Watchable} that changed.
+ */
+ public abstract void onChange(@Nullable Watchable what);
+}
diff --git a/services/tests/servicestests/src/com/android/server/utils/WatcherTest.java b/services/tests/servicestests/src/com/android/server/utils/WatcherTest.java
new file mode 100644
index 000000000000..40575e4cf16f
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/utils/WatcherTest.java
@@ -0,0 +1,304 @@
+/**
+ * Copyright (c) 2020, The Android Open Source Project
+ *
+ * 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 com.android.server.utils;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import android.content.Context;
+import android.platform.test.annotations.Presubmit;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.internal.util.Preconditions;
+import com.android.internal.util.TraceBuffer;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * Test class for {@link Watcher}, {@link Watchable}, {@link WatchableImpl},
+ * {@link WatchedArrayMap}, {@link WatchedSparseArray}, and
+ * {@link WatchedSparseBooleanArray}.
+ *
+ * Build/Install/Run:
+ * atest FrameworksServicesTests:WatcherTest
+ */
+@SmallTest
+public class WatcherTest {
+
+ // A small Watchable leaf node
+ private class Leaf extends WatchableImpl {
+ private int datum = 0;
+ void set(int i) {
+ if (datum != i) {
+ datum = i;
+ dispatchChange(this);
+ }
+ }
+ void tick() {
+ set(datum + 1);
+ }
+ }
+
+ // A top-most watcher. It counts the number of notifications that it receives.
+ private class Tester extends Watcher {
+ // The count of changes.
+ public int changes = 0;
+
+ // The single Watchable that this monitors.
+ public final Watchable mWatched;
+
+ // The key, used for messages
+ public String mKey;
+
+ // Create the Tester with a Watcher
+ public Tester(Watchable w, String k) {
+ mWatched = w;
+ mKey = k;
+ }
+
+ // Listen for events
+ public void register() {
+ mWatched.registerObserver(this);
+ }
+
+ // Stop listening for events
+ public void unregister() {
+ mWatched.unregisterObserver(this);
+ }
+
+ // Count the number of notifications received.
+ @Override
+ public void onChange(Watchable what) {
+ changes++;
+ }
+
+ // Verify the count.
+ public void verify(int want, String msg) {
+ assertEquals(mKey + " " + msg, want, changes);
+ }
+ }
+
+ @Before
+ public void setUp() throws Exception {
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ }
+
+ @Test
+ public void test_notify() {
+
+ Tester tester;
+
+ // Create a few leaves
+ Leaf a = new Leaf();
+ Leaf b = new Leaf();
+ Leaf c = new Leaf();
+ Leaf d = new Leaf();
+
+ // Basic test. Create a leaf and verify that changes to the leaf get notified to
+ // the tester.
+ tester = new Tester(a, "Leaf");
+ tester.verify(0, "Initial leaf - no registration");
+ a.tick();
+ tester.verify(0, "Updates with no registration");
+ tester.register();
+ a.tick();
+ tester.verify(1, "Updates with registration");
+ a.tick();
+ a.tick();
+ tester.verify(3, "Updates with registration");
+
+ // Add the same leaf to more than one tester. Verify that a change to the leaf is seen by
+ // all registered listeners.
+ Tester buddy1 = new Tester(a, "Leaf2");
+ Tester buddy2 = new Tester(a, "Leaf3");
+ buddy1.verify(0, "Initial leaf - no registration");
+ buddy2.verify(0, "Initial leaf - no registration");
+ a.tick();
+ tester.verify(4, "Updates with buddies");
+ buddy1.verify(0, "Updates - no registration");
+ buddy2.verify(0, "Updates - no registration");
+ buddy1.register();
+ buddy2.register();
+ buddy1.verify(0, "No updates - registered");
+ buddy2.verify(0, "No updates - registered");
+ a.tick();
+ buddy1.verify(1, "First update");
+ buddy2.verify(1, "First update");
+ buddy1.unregister();
+ a.tick();
+ buddy1.verify(1, "Second update - unregistered");
+ buddy2.verify(2, "Second update");
+
+ buddy1 = null;
+ buddy2 = null;
+
+ final int INDEX_A = 1;
+ final int INDEX_B = 2;
+ final int INDEX_C = 3;
+ final int INDEX_D = 4;
+
+ // Test WatchedArrayMap
+ WatchedArrayMap<Integer, Leaf> am = new WatchedArrayMap<>();
+ am.put(INDEX_A, a);
+ am.put(INDEX_B, b);
+ tester = new Tester(am, "WatchedArrayMap");
+ tester.verify(0, "Initial array - no registration");
+ a.tick();
+ tester.verify(0, "Updates with no registration");
+ tester.register();
+ tester.verify(0, "Updates with no registration");
+ a.tick();
+ tester.verify(1, "Updates with registration");
+ b.tick();
+ tester.verify(2, "Updates with registration");
+ am.remove(INDEX_B);
+ tester.verify(3, "Removed b");
+ b.tick();
+ tester.verify(3, "Updates with b not watched");
+ am.put(INDEX_B, b);
+ am.put(INDEX_C, b);
+ tester.verify(5, "Added b twice");
+ b.tick();
+ tester.verify(6, "Changed b - single notification");
+ am.remove(INDEX_C);
+ tester.verify(7, "Removed first b");
+ b.tick();
+ tester.verify(8, "Changed b - single notification");
+ am.remove(INDEX_B);
+ tester.verify(9, "Removed second b");
+ b.tick();
+ tester.verify(9, "Updated b - no change");
+ am.clear();
+ tester.verify(10, "Cleared array");
+ b.tick();
+ tester.verify(10, "Change to b not in array");
+
+ // Special methods
+ am.put(INDEX_C, c);
+ tester.verify(11, "Added c");
+ c.tick();
+ tester.verify(12, "Ticked c");
+ am.setValueAt(am.indexOfKey(INDEX_C), d);
+ tester.verify(13, "Replaced c with d");
+ c.tick();
+ d.tick();
+ tester.verify(14, "Ticked d and c (c not registered)");
+
+ am = null;
+
+ // Test WatchedSparseArray
+ WatchedSparseArray<Leaf> sa = new WatchedSparseArray<>();
+ sa.put(INDEX_A, a);
+ sa.put(INDEX_B, b);
+ tester = new Tester(sa, "WatchedSparseArray");
+ tester.verify(0, "Initial array - no registration");
+ a.tick();
+ tester.verify(0, "Updates with no registration");
+ tester.register();
+ tester.verify(0, "Updates with no registration");
+ a.tick();
+ tester.verify(1, "Updates with registration");
+ b.tick();
+ tester.verify(2, "Updates with registration");
+ sa.remove(INDEX_B);
+ tester.verify(3, "Removed b");
+ b.tick();
+ tester.verify(3, "Updates with b not watched");
+ sa.put(INDEX_B, b);
+ sa.put(INDEX_C, b);
+ tester.verify(5, "Added b twice");
+ b.tick();
+ tester.verify(6, "Changed b - single notification");
+ sa.remove(INDEX_C);
+ tester.verify(7, "Removed first b");
+ b.tick();
+ tester.verify(8, "Changed b - single notification");
+ sa.remove(INDEX_B);
+ tester.verify(9, "Removed second b");
+ b.tick();
+ tester.verify(9, "Updated b - no change");
+ sa.clear();
+ tester.verify(10, "Cleared array");
+ b.tick();
+ tester.verify(10, "Change to b not in array");
+
+ // Special methods
+ sa.put(INDEX_A, a);
+ sa.put(INDEX_B, b);
+ sa.put(INDEX_C, c);
+ tester.verify(13, "Added c");
+ c.tick();
+ tester.verify(14, "Ticked c");
+ sa.setValueAt(sa.indexOfKey(INDEX_C), d);
+ tester.verify(15, "Replaced c with d");
+ c.tick();
+ d.tick();
+ tester.verify(16, "Ticked d and c (c not registered)");
+ sa.append(INDEX_D, c);
+ tester.verify(17, "Append c");
+ c.tick();
+ d.tick();
+ tester.verify(19, "Ticked d and c");
+ assertEquals("Verify four elements", 4, sa.size());
+ // Figure out which elements are at which indices.
+ Leaf[] x = new Leaf[4];
+ for (int i = 0; i < 4; i++) {
+ x[i] = sa.valueAt(i);
+ }
+ sa.removeAtRange(0, 2);
+ tester.verify(20, "Removed two elements in one operation");
+ x[0].tick();
+ x[1].tick();
+ tester.verify(20, "Ticked two removed elements");
+ x[2].tick();
+ x[3].tick();
+ tester.verify(22, "Ticked two remaining elements");
+
+ sa = null;
+
+ // Test WatchedSparseBooleanArray
+ WatchedSparseBooleanArray sb = new WatchedSparseBooleanArray();
+ tester = new Tester(sb, "WatchedSparseBooleanArray");
+ tester.verify(0, "Initial array - no registration");
+ sb.put(INDEX_A, true);
+ tester.verify(0, "Updates with no registration");
+ tester.register();
+ tester.verify(0, "Updates with no registration");
+ sb.put(INDEX_B, true);
+ tester.verify(1, "Updates with registration");
+ sb.put(INDEX_B, true);
+ tester.verify(1, "Null update");
+ sb.put(INDEX_B, false);
+ sb.put(INDEX_C, true);
+ tester.verify(3, "Updates with registration");
+ // Special methods
+ sb.put(INDEX_C, true);
+ tester.verify(3, "Added true, no change");
+ sb.setValueAt(sb.indexOfKey(INDEX_C), false);
+ tester.verify(4, "Replaced true with false");
+ sb.append(INDEX_D, true);
+ tester.verify(5, "Append true");
+
+ sb = null;
+ }
+}