blob: 33b08d41c07def39dc20ae2e94850a86414c3be3 [file] [log] [blame]
/*
* 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.location.listeners;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.os.Build;
import android.util.ArrayMap;
import android.util.ArraySet;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.listeners.ListenerExecutor.ListenerOperation;
import com.android.internal.util.Preconditions;
import java.io.FileDescriptor;
import java.io.PrintWriter;
import java.util.AbstractMap;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Map.Entry;
import java.util.Objects;
import java.util.function.Function;
import java.util.function.Predicate;
/**
* A base class to multiplex client listener registrations within system server. Every listener is
* represented by a registration object which stores all required state for a listener. Keys are
* used to uniquely identify every registration. Listener operations may be executed on
* registrations in order to invoke the represented listener.
*
* Registrations are divided into two categories, active registrations and inactive registrations,
* as defined by {@link #isActive(ListenerRegistration)}. If a registration's active state changes,
* {@link #updateRegistrations(Predicate)} must be invoked and return true for any registration
* whose active state may have changed. Listeners will only be invoked for active registrations.
*
* The set of active registrations is combined into a single merged registration, which is submitted
* to the backing service when necessary in order to register the service. The merged registration
* is updated whenever the set of active registration changes.
*
* Callbacks invoked for various changes will always be ordered according to this lifecycle list:
*
* <ul>
* <li>{@link #onRegister()}</li>
* <li>{@link ListenerRegistration#onRegister(Object)}</li>
* <li>{@link #onRegistrationAdded(Object, ListenerRegistration)}</li>
* <li>{@link #onRegistrationReplaced(Object, ListenerRegistration, ListenerRegistration)} (only
* invoked if this registration is replacing a prior registration)</li>
* <li>{@link #onActive()}</li>
* <li>{@link ListenerRegistration#onActive()}</li>
* <li>{@link ListenerRegistration#onInactive()}</li>
* <li>{@link #onInactive()}</li>
* <li>{@link #onRegistrationRemoved(Object, ListenerRegistration)}</li>
* <li>{@link ListenerRegistration#onUnregister()}</li>
* <li>{@link #onUnregister()}</li>
* </ul>
*
* Adding registrations is not allowed to be called re-entrantly (ie, while in the middle of some
* other operation or callback. Removal is allowed re-entrantly, however only via
* {@link #removeRegistration(Object, ListenerRegistration)}, not via any other removal method. This
* ensures re-entrant removal does not accidentally remove the incorrect registration.
*
* @param <TKey> key type
* @param <TListener> listener type
* @param <TRegistration> registration type
* @param <TMergedRegistration> merged registration type
*/
public abstract class ListenerMultiplexer<TKey, TListener,
TRegistration extends ListenerRegistration<TListener>, TMergedRegistration> {
@GuardedBy("mRegistrations")
private final ArrayMap<TKey, TRegistration> mRegistrations = new ArrayMap<>();
@GuardedBy("mRegistrations")
private final UpdateServiceBuffer mUpdateServiceBuffer = new UpdateServiceBuffer();
@GuardedBy("mRegistrations")
private final ReentrancyGuard mReentrancyGuard = new ReentrancyGuard();
@GuardedBy("mRegistrations")
private int mActiveRegistrationsCount = 0;
@GuardedBy("mRegistrations")
private boolean mServiceRegistered = false;
@GuardedBy("mRegistrations")
@Nullable private TMergedRegistration mMerged;
/**
* Should be implemented to return a unique identifying tag that may be used for logging, etc...
*/
public abstract @NonNull String getTag();
/**
* Should be implemented to register with the backing service with the given merged
* registration, and should return true if a matching call to {@link #unregisterWithService()}
* is required to unregister (ie, if registration succeeds). The set of registrations passed in
* is the same set passed into {@link #mergeRegistrations(Collection)} to generate the merged
* registration.
*
* <p class="note">It may seem redundant to pass in the set of active registrations when they
* have already been used to generate the merged request, and indeed, for many implementations
* this parameter can likely simply be ignored. However, some implementations may require access
* to the set of registrations used to generate the merged requestion for further logic even
* after the merged registration has been generated.
*
* @see #mergeRegistrations(Collection)
* @see #reregisterWithService(Object, Object, Collection)
*/
protected abstract boolean registerWithService(TMergedRegistration merged,
@NonNull Collection<TRegistration> registrations);
/**
* Invoked when the service has already been registered with some merged registration, and is
* now being registered with a different merged registration. The default implementation simply
* invokes {@link #registerWithService(Object, Collection)}.
*
* @see #registerWithService(Object, Collection)
*/
protected boolean reregisterWithService(TMergedRegistration oldMerged,
TMergedRegistration newMerged, @NonNull Collection<TRegistration> registrations) {
return registerWithService(newMerged, registrations);
}
/**
* Should be implemented to unregister from the backing service.
*/
protected abstract void unregisterWithService();
/**
* Defines whether a registration is currently active or not. Only active registrations will be
* forwarded to {@link #registerWithService(Object, Collection)}, and listener invocations will
* only be delivered to active requests. If a registration's active state changes,
* {@link #updateRegistrations(Predicate)} must be invoked with a function that returns true for
* any registrations that may have changed their active state.
*/
protected abstract boolean isActive(@NonNull TRegistration registration);
/**
* Called in order to generate a merged registration from the given set of active registrations.
* The list of registrations will never be empty. If the resulting merged registration is equal
* to the currently registered merged registration, nothing further will happen. If the merged
* registration differs, {@link #registerWithService(Object, Collection)} or
* {@link #reregisterWithService(Object, Object, Collection)} will be invoked with the new
* merged registration so that the backing service can be updated.
*/
protected abstract @Nullable TMergedRegistration mergeRegistrations(
@NonNull Collection<TRegistration> registrations);
/**
* Invoked when the multiplexer goes from having no registrations to having some registrations.
* This is a convenient entry point for registering listeners, etc, which only need to be
* present while there are any registrations. Invoked while holding the multiplexer's internal
* lock.
*/
protected void onRegister() {}
/**
* Invoked when the multiplexer goes from having some registrations to having no registrations.
* This is a convenient entry point for unregistering listeners, etc, which only need to be
* present while there are any registrations. Invoked while holding the multiplexer's internal
* lock.
*/
protected void onUnregister() {}
/**
* Invoked when a registration is added. Invoked while holding the multiplexer's internal lock.
*/
protected void onRegistrationAdded(@NonNull TKey key, @NonNull TRegistration registration) {}
/**
* Invoked instead of {@link #onRegistrationAdded(Object, ListenerRegistration)} if a
* registration is replacing an old registration. The old registration will have already been
* unregistered. Invoked while holding the multiplexer's internal lock. The default behavior is
* simply to call into {@link #onRegistrationAdded(Object, ListenerRegistration)}.
*/
protected void onRegistrationReplaced(@NonNull TKey key, @NonNull TRegistration oldRegistration,
@NonNull TRegistration newRegistration) {
onRegistrationAdded(key, newRegistration);
}
/**
* Invoked when a registration is removed. Invoked while holding the multiplexer's internal
* lock.
*/
protected void onRegistrationRemoved(@NonNull TKey key, @NonNull TRegistration registration) {}
/**
* Invoked when the multiplexer goes from having no active registrations to having some active
* registrations. This is a convenient entry point for registering listeners, etc, which only
* need to be present while there are active registrations. Invoked while holding the
* multiplexer's internal lock.
*/
protected void onActive() {}
/**
* Invoked when the multiplexer goes from having some active registrations to having no active
* registrations. This is a convenient entry point for unregistering listeners, etc, which only
* need to be present while there are active registrations. Invoked while holding the
* multiplexer's internal lock.
*/
protected void onInactive() {}
/**
* Puts a new registration with the given key, replacing any previous registration under the
* same key. This method cannot be called to put a registration re-entrantly.
*/
protected final void putRegistration(@NonNull TKey key, @NonNull TRegistration registration) {
replaceRegistration(key, key, registration);
}
/**
* Atomically removes the registration with the old key and adds a new registration with the
* given key. If there was a registration for the old key,
* {@link #onRegistrationReplaced(Object, ListenerRegistration, ListenerRegistration)} will be
* invoked for the new registration and key instead of
* {@link #onRegistrationAdded(Object, ListenerRegistration)}, even though they may not share
* the same key. The old key may be the same value as the new key, in which case this function
* is equivalent to {@link #putRegistration(Object, ListenerRegistration)}. This method cannot
* be called to add a registration re-entrantly.
*/
protected final void replaceRegistration(@NonNull TKey oldKey, @NonNull TKey key,
@NonNull TRegistration registration) {
Objects.requireNonNull(oldKey);
Objects.requireNonNull(key);
Objects.requireNonNull(registration);
synchronized (mRegistrations) {
// adding listeners reentrantly is not supported
Preconditions.checkState(!mReentrancyGuard.isReentrant());
// new key may only have a prior registration if the oldKey is the same as the key
Preconditions.checkArgument(oldKey == key || !mRegistrations.containsKey(key));
// since adding a registration can invoke a variety of callbacks, we need to ensure
// those callbacks themselves do not re-enter, as this could lead to out-of-order
// callbacks. further, we buffer service updates since adding a registration may
// involve removing a prior registration. note that try-with-resources ordering is
// meaningful here as well. we want to close the reentrancy guard first, as this may
// generate additional service updates, then close the update service buffer.
try (UpdateServiceBuffer ignored1 = mUpdateServiceBuffer.acquire();
ReentrancyGuard ignored2 = mReentrancyGuard.acquire()) {
boolean wasEmpty = mRegistrations.isEmpty();
TRegistration oldRegistration = null;
int index = mRegistrations.indexOfKey(oldKey);
if (index >= 0) {
oldRegistration = removeRegistration(index, oldKey != key);
}
if (oldKey == key && index >= 0) {
mRegistrations.setValueAt(index, registration);
} else {
mRegistrations.put(key, registration);
}
if (wasEmpty) {
onRegister();
}
registration.onRegister(key);
if (oldRegistration == null) {
onRegistrationAdded(key, registration);
} else {
onRegistrationReplaced(key, oldRegistration, registration);
}
onRegistrationActiveChanged(registration);
}
}
}
/**
* Removes the registration with the given key. This method cannot be called to remove a
* registration re-entrantly.
*/
protected final void removeRegistration(@NonNull Object key) {
synchronized (mRegistrations) {
// this method does not support removing listeners reentrantly
Preconditions.checkState(!mReentrancyGuard.isReentrant());
int index = mRegistrations.indexOfKey(key);
if (index < 0) {
return;
}
removeRegistration(index, true);
}
}
/**
* Removes all registrations with keys that satisfy the given predicate. This method cannot be
* called to remove a registration re-entrantly.
*/
protected final void removeRegistrationIf(@NonNull Predicate<TKey> predicate) {
synchronized (mRegistrations) {
// this method does not support removing listeners reentrantly
Preconditions.checkState(!mReentrancyGuard.isReentrant());
// since removing a registration can invoke a variety of callbacks, we need to ensure
// those callbacks themselves do not re-enter, as this could lead to out-of-order
// callbacks. further, we buffer service updates since chains of removeLater()
// invocations could result in multiple service updates. note that try-with-resources
// ordering is meaningful here as well. we want to close the reentrancy guard first, as
// this may generate additional service updates, then close the update service buffer.
try (UpdateServiceBuffer ignored1 = mUpdateServiceBuffer.acquire();
ReentrancyGuard ignored2 = mReentrancyGuard.acquire()) {
final int size = mRegistrations.size();
for (int i = 0; i < size; i++) {
TKey key = mRegistrations.keyAt(i);
if (predicate.test(key)) {
removeRegistration(key, mRegistrations.valueAt(i));
}
}
}
}
}
/**
* Removes the given registration with the given key. If the given key has a different
* registration at the time this method is called, nothing happens. This method allows for
* re-entrancy, and may be called to remove a registration re-entrantly.
*/
protected final void removeRegistration(@NonNull Object key,
@NonNull ListenerRegistration<?> registration) {
synchronized (mRegistrations) {
int index = mRegistrations.indexOfKey(key);
if (index < 0) {
return;
}
TRegistration typedRegistration = mRegistrations.valueAt(index);
if (typedRegistration != registration) {
return;
}
if (mReentrancyGuard.isReentrant()) {
unregister(typedRegistration);
mReentrancyGuard.markForRemoval(key, typedRegistration);
} else {
removeRegistration(index, true);
}
}
}
@GuardedBy("mRegistrations")
private TRegistration removeRegistration(int index, boolean removeEntry) {
if (Build.IS_DEBUGGABLE) {
Preconditions.checkState(Thread.holdsLock(mRegistrations));
}
TKey key = mRegistrations.keyAt(index);
TRegistration registration = mRegistrations.valueAt(index);
// since removing a registration can invoke a variety of callbacks, we need to ensure those
// callbacks themselves do not re-enter, as this could lead to out-of-order callbacks.
// further, we buffer service updates since chains of removeLater() invocations could result
// in multiple service updates. note that try-with-resources ordering is meaningful here as
// well. we want to close the reentrancy guard first, as this may generate additional
// service updates, then close the update service buffer.
try (UpdateServiceBuffer ignored1 = mUpdateServiceBuffer.acquire();
ReentrancyGuard ignored2 = mReentrancyGuard.acquire()) {
unregister(registration);
onRegistrationRemoved(key, registration);
registration.onUnregister();
if (removeEntry) {
mRegistrations.removeAt(index);
if (mRegistrations.isEmpty()) {
onUnregister();
}
}
}
return registration;
}
/**
* Forces a re-evalution of the merged request for all active registrations and updates service
* registration accordingly.
*/
protected final void updateService() {
synchronized (mRegistrations) {
if (mUpdateServiceBuffer.isBuffered()) {
mUpdateServiceBuffer.markUpdateServiceRequired();
return;
}
ArrayList<TRegistration> actives = new ArrayList<>(mRegistrations.size());
final int size = mRegistrations.size();
for (int i = 0; i < size; i++) {
TRegistration registration = mRegistrations.valueAt(i);
if (registration.isActive()) {
actives.add(registration);
}
}
if (actives.isEmpty()) {
if (mServiceRegistered) {
mMerged = null;
mServiceRegistered = false;
unregisterWithService();
}
return;
}
TMergedRegistration merged = mergeRegistrations(actives);
if (!mServiceRegistered || !Objects.equals(merged, mMerged)) {
if (mServiceRegistered) {
mServiceRegistered = reregisterWithService(mMerged, merged, actives);
} else {
mServiceRegistered = registerWithService(merged, actives);
}
mMerged = mServiceRegistered ? merged : null;
}
}
}
/**
* If the service is currently registered, unregisters it and then calls
* {@link #updateService()} so that {@link #registerWithService(Object, Collection)} will be
* re-invoked. This is useful, for instance, if the backing service has crashed or otherwise
* lost state, and needs to be re-initialized. Because this unregisters first, this is safe to
* use even if there is a possibility the backing server has not crashed, or has already been
* reinitialized.
*/
protected final void resetService() {
synchronized (mRegistrations) {
if (mServiceRegistered) {
mMerged = null;
mServiceRegistered = false;
unregisterWithService();
updateService();
}
}
}
/**
* Begins buffering calls to {@link #updateService()} until {@link UpdateServiceLock#close()}
* is called. This is useful to prevent extra work when combining multiple calls (for example,
* buffering {@code updateService()} until after multiple adds/removes/updates occur.
*/
public UpdateServiceLock newUpdateServiceLock() {
return new UpdateServiceLock(mUpdateServiceBuffer.acquire());
}
/**
* Evaluates the predicate on all registrations. The predicate should return true if the active
* state of the registration may have changed as a result. If the active state of any
* registration has changed, {@link #updateService()} will automatically be invoked to handle
* the resulting changes.
*/
protected final void updateRegistrations(@NonNull Predicate<TRegistration> predicate) {
synchronized (mRegistrations) {
// since updating a registration can invoke a variety of callbacks, we need to ensure
// those callbacks themselves do not re-enter, as this could lead to out-of-order
// callbacks. note that try-with-resources ordering is meaningful here as well. we want
// to close the reentrancy guard first, as this may generate additional service updates,
// then close the update service buffer.
try (UpdateServiceBuffer ignored1 = mUpdateServiceBuffer.acquire();
ReentrancyGuard ignored2 = mReentrancyGuard.acquire()) {
final int size = mRegistrations.size();
for (int i = 0; i < size; i++) {
TRegistration registration = mRegistrations.valueAt(i);
if (predicate.test(registration)) {
onRegistrationActiveChanged(registration);
}
}
}
}
}
/**
* Evaluates the predicate on a registration with the given key. The predicate should return
* true if the active state of the registration may have changed as a result. If the active
* state of the registration has changed, {@link #updateService()} will automatically be invoked
* to handle the resulting changes. Returns true if there is a registration with the given key
* (and thus the predicate was invoked), and false otherwise.
*/
protected final boolean updateRegistration(@NonNull Object key,
@NonNull Predicate<TRegistration> predicate) {
synchronized (mRegistrations) {
// since updating a registration can invoke a variety of callbacks, we need to ensure
// those callbacks themselves do not re-enter, as this could lead to out-of-order
// callbacks. note that try-with-resources ordering is meaningful here as well. we want
// to close the reentrancy guard first, as this may generate additional service updates,
// then close the update service buffer.
try (UpdateServiceBuffer ignored1 = mUpdateServiceBuffer.acquire();
ReentrancyGuard ignored2 = mReentrancyGuard.acquire()) {
int index = mRegistrations.indexOfKey(key);
if (index < 0) {
return false;
}
TRegistration registration = mRegistrations.valueAt(index);
if (predicate.test(registration)) {
onRegistrationActiveChanged(registration);
}
return true;
}
}
}
@GuardedBy("mRegistrations")
private void onRegistrationActiveChanged(TRegistration registration) {
if (Build.IS_DEBUGGABLE) {
Preconditions.checkState(Thread.holdsLock(mRegistrations));
}
boolean active = registration.isRegistered() && isActive(registration);
boolean changed = registration.setActive(active);
if (changed) {
if (active) {
if (++mActiveRegistrationsCount == 1) {
onActive();
}
registration.onActive();
} else {
registration.onInactive();
if (--mActiveRegistrationsCount == 0) {
onInactive();
}
}
updateService();
}
}
/**
* Executes the given function for all active registrations. If the function returns a non-null
* operation, that operation will be invoked with the associated listener. The function may not
* change the active state of the registration.
*/
protected final void deliverToListeners(
@NonNull Function<TRegistration, ListenerOperation<TListener>> function) {
synchronized (mRegistrations) {
try (ReentrancyGuard ignored = mReentrancyGuard.acquire()) {
final int size = mRegistrations.size();
for (int i = 0; i < size; i++) {
TRegistration registration = mRegistrations.valueAt(i);
if (registration.isActive()) {
ListenerOperation<TListener> operation = function.apply(registration);
if (operation != null) {
registration.executeOperation(operation);
}
}
}
}
}
}
/**
* Executes the given operation for all active listeners. This is a convenience function
* equivalent to:
* <pre>
* deliverToListeners(registration -> operation);
* </pre>
*/
protected final void deliverToListeners(@NonNull ListenerOperation<TListener> operation) {
synchronized (mRegistrations) {
try (ReentrancyGuard ignored = mReentrancyGuard.acquire()) {
final int size = mRegistrations.size();
for (int i = 0; i < size; i++) {
TRegistration registration = mRegistrations.valueAt(i);
if (registration.isActive()) {
registration.executeOperation(operation);
}
}
}
}
}
private void unregister(TRegistration registration) {
registration.unregisterInternal();
onRegistrationActiveChanged(registration);
}
/**
* Dumps debug information.
*/
public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
synchronized (mRegistrations) {
pw.print("service: ");
pw.print(getServiceState());
pw.println();
if (!mRegistrations.isEmpty()) {
pw.println("listeners:");
final int size = mRegistrations.size();
for (int i = 0; i < size; i++) {
TRegistration registration = mRegistrations.valueAt(i);
pw.print(" ");
pw.print(registration);
if (!registration.isActive()) {
pw.println(" (inactive)");
} else {
pw.println();
}
}
}
}
}
/**
* May be overridden to provide additional details on service state when dumping the manager
* state. Invoked while holding the multiplexer's internal lock.
*/
protected String getServiceState() {
if (mServiceRegistered) {
if (mMerged != null) {
return mMerged.toString();
} else {
return "registered";
}
} else {
return "unregistered";
}
}
/**
* A reference counted helper class that guards against re-entrancy, and also helps implement
* registration removal during reentrancy. When this class is {@link #acquire()}d, it increments
* the reference count. To check whether re-entrancy is occurring, clients may use
* {@link #isReentrant()}, and modify their behavior (such as by failing the call, or calling
* {@link #markForRemoval(Object, ListenerRegistration)}). When this class is {@link #close()}d,
* any key/registration pairs that were marked for removal prior to the close operation will
* then be removed - which is safe since the operation will no longer be re-entrant.
*/
private final class ReentrancyGuard implements AutoCloseable {
@GuardedBy("mRegistrations")
private int mGuardCount;
@GuardedBy("mRegistrations")
private @Nullable ArraySet<Entry<Object, ListenerRegistration<?>>> mScheduledRemovals;
ReentrancyGuard() {
mGuardCount = 0;
mScheduledRemovals = null;
}
@GuardedBy("mRegistrations")
boolean isReentrant() {
if (Build.IS_DEBUGGABLE) {
Preconditions.checkState(Thread.holdsLock(mRegistrations));
}
return mGuardCount != 0;
}
@GuardedBy("mRegistrations")
void markForRemoval(Object key, ListenerRegistration<?> registration) {
if (Build.IS_DEBUGGABLE) {
Preconditions.checkState(Thread.holdsLock(mRegistrations));
}
Preconditions.checkState(isReentrant());
if (mScheduledRemovals == null) {
mScheduledRemovals = new ArraySet<>(mRegistrations.size());
}
mScheduledRemovals.add(new AbstractMap.SimpleImmutableEntry<>(key, registration));
}
ReentrancyGuard acquire() {
++mGuardCount;
return this;
}
@Override
public void close() {
ArraySet<Entry<Object, ListenerRegistration<?>>> scheduledRemovals = null;
Preconditions.checkState(mGuardCount > 0);
if (--mGuardCount == 0) {
scheduledRemovals = mScheduledRemovals;
mScheduledRemovals = null;
}
if (scheduledRemovals == null) {
return;
}
try (UpdateServiceBuffer ignored = mUpdateServiceBuffer.acquire()) {
final int size = scheduledRemovals.size();
for (int i = 0; i < size; i++) {
Entry<Object, ListenerRegistration<?>> entry = scheduledRemovals.valueAt(i);
removeRegistration(entry.getKey(), entry.getValue());
}
}
}
}
/**
* A reference counted helper class that buffers class to {@link #updateService()}. Since
* {@link #updateService()} iterates through every registration and performs request merging
* work, it can often be the most expensive part of any update to the multiplexer. This means
* that if multiple calls to updateService() can be buffered, work will be saved. This class
* allows clients to begin buffering calls after {@link #acquire()}ing this class, and when
* {@link #close()} is called, any buffered calls to {@link #updateService()} will be combined
* into a single final call. Clients should acquire this class when they are doing work that
* could potentially result in multiple calls to updateService(), and close when they are done
* with that work.
*/
private final class UpdateServiceBuffer implements AutoCloseable {
// requires internal locking because close() may be exposed externally and could be called
// from any thread
@GuardedBy("this")
private int mBufferCount;
@GuardedBy("this")
private boolean mUpdateServiceRequired;
UpdateServiceBuffer() {
mBufferCount = 0;
mUpdateServiceRequired = false;
}
synchronized boolean isBuffered() {
return mBufferCount != 0;
}
synchronized void markUpdateServiceRequired() {
Preconditions.checkState(isBuffered());
mUpdateServiceRequired = true;
}
synchronized UpdateServiceBuffer acquire() {
++mBufferCount;
return this;
}
@Override
public void close() {
boolean updateServiceRequired = false;
synchronized (this) {
Preconditions.checkState(mBufferCount > 0);
if (--mBufferCount == 0) {
updateServiceRequired = mUpdateServiceRequired;
mUpdateServiceRequired = false;
}
}
if (updateServiceRequired) {
updateService();
}
}
}
/**
* Acquiring this lock will buffer all calls to {@link #updateService()} until the lock is
* {@link #close()}ed. This can be used to save work by acquiring the lock before multiple calls
* to updateService() are expected, and closing the lock after.
*/
public final class UpdateServiceLock implements AutoCloseable {
private @Nullable UpdateServiceBuffer mUpdateServiceBuffer;
UpdateServiceLock(UpdateServiceBuffer updateServiceBuffer) {
mUpdateServiceBuffer = updateServiceBuffer;
}
@Override
public void close() {
if (mUpdateServiceBuffer != null) {
UpdateServiceBuffer buffer = mUpdateServiceBuffer;
mUpdateServiceBuffer = null;
buffer.close();
}
}
}
}