/* The MIT License (MIT)
* Copyright (c) 2016 YouView Ltd
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.ruesga.android.wallpapers.photophase.cast.mdsn;
import android.os.Handler;
import android.os.SystemClock;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
/**
* <p>Map-like interface supporting service visibility debouncing:</p>
* <ul>
* <li>Inserts (null -> A) are immediately notified to the {@code Listener}
* <li>Updates (A -> B) are immediate
* <li>Deletes (A -> null) are debounced such that (A -> null -> A) within a certain time interval
* will never be deleted
* </ul>
*/
class MapDebouncer<Key, Value> {
interface Listener<Key, Value> {
void put(Key key, Value value);
}
private final int mDebouncePeriodMillis;
private final Listener<Key, Value> mListener;
private final Map<Key, Value> mBackingMap = new HashMap<>();
private final Map<Key, Long> mRemovalSchedule = new HashMap<>();
private long mNextScheduledRemoval;
private Handler mHandler;
MapDebouncer(int debouncePeriodMillis, Listener<Key, Value> listener) {
if (debouncePeriodMillis < 0) {
throw new IllegalArgumentException();
}
if (listener == null) {
throw new NullPointerException();
}
mDebouncePeriodMillis = debouncePeriodMillis;
mListener = listener;
}
void put(Key key, Value newValue) {
if (mDebouncePeriodMillis == 0) {
mListener.put(key, newValue);
return;
}
// lazy init the handler to match the thread that calls put()
// also avoids creating a handler if there is no debounce period
if (mHandler == null) {
mHandler = new Handler();
}
Value oldValue = mBackingMap.get(key);
if (oldValue == null) {
if (newValue != null) {
immediateUpdate(key, newValue);
}
} else{
if (newValue == null) {
timedRemoval(key);
} else if (oldValue.equals(newValue)) {
cancelTimedRemoval(key);
} else {
immediateUpdate(key, newValue);
}
}
}
private final Runnable mRemoveRunnable = new Runnable() {
@Override
public void run() {
long currentTime = SystemClock.uptimeMillis();
Iterator<Map.Entry<Key, Long>> it = mRemovalSchedule.entrySet().iterator();
long nextScheduleTime = Long.MAX_VALUE;
while (it.hasNext()) {
Map.Entry<Key, Long> entry = it.next();
long itemTime = entry.getValue();
if (itemTime <= currentTime) {
Key key = entry.getKey();
performUpdate(key, null);
it.remove();
} else if (itemTime < nextScheduleTime) {
nextScheduleTime = itemTime;
}
}
mNextScheduledRemoval = 0;
if (nextScheduleTime < Long.MAX_VALUE) {
mHandler.postAtTime(mRemoveRunnable, nextScheduleTime);
mNextScheduledRemoval = nextScheduleTime;
}
}
};
private void performUpdate(Key key, Value value) {
mBackingMap.put(key, value);
mListener.put(key, value);
}
private void cancelTimedRemoval(Key key) {
Long scheduled = mRemovalSchedule.remove(key);
// if this item was the next to be scheduled, calculate the next most imminent item, if any
if (scheduled != null && scheduled == mNextScheduledRemoval) {
long newSchedule = Long.MAX_VALUE;
for (long time : mRemovalSchedule.values()) {
if (time < newSchedule) {
newSchedule = time;
}
}
if (scheduled != newSchedule) {
mHandler.removeCallbacks(mRemoveRunnable);
if (newSchedule < Long.MAX_VALUE) {
mHandler.postAtTime(mRemoveRunnable, newSchedule);
}
}
}
}
private void timedRemoval(Key key) {
long removalTime = SystemClock.uptimeMillis() + mDebouncePeriodMillis;
if (mNextScheduledRemoval == 0) {
mNextScheduledRemoval = removalTime;
mHandler.postAtTime(mRemoveRunnable, removalTime);
} else if (removalTime < mNextScheduledRemoval) {
mHandler.removeCallbacks(mRemoveRunnable);
mNextScheduledRemoval = removalTime;
mHandler.postAtTime(mRemoveRunnable, removalTime);
}
mRemovalSchedule.put(key, removalTime);
}
private void immediateUpdate(Key key, Value value) {
cancelTimedRemoval(key);
performUpdate(key, value);
}
void clear() {
mBackingMap.clear();
mRemovalSchedule.clear();
if (mHandler != null) {
mHandler.removeCallbacks(mRemoveRunnable);
}
mNextScheduledRemoval = 0;
}
}