package org.commcare.utils; import android.support.annotation.NonNull; /** * A delayed blocking action will be executed at most one time after some fixed period of delay. * * Each action has an associated tag. This tag should always represent the same action, and the * caller should be indifferent as to whether their delayed action runnable fires, or whether * another action fires at another time. If the associated manager receives multiple actions in * sequence it will invalidate the old actions in order, assuming that the request for a delay * has restarted * * An example of an implementing action is a screen update that should fire after a user sets the * value of some interactive widget. Setting a short delay (200ms or so) can ensure that as the user * interacts with a value that can change rapidly (like a slider) that the value has a chance to * "settle" before processing, rather than firing a barrage of value update events. * * Delayed actions time out after a fixed period * * Created by ctsims on 8/26/2016. */ public abstract class DelayedBlockingAction implements Runnable { @NonNull private final Object tag; private final int delay; private final int timeout; private final long timeInitiated; private boolean invalidated = false; private boolean fired = false; private final Object lock = new Object(); public DelayedBlockingAction(Object tag, int delay) { this(tag, delay, 2000); } public DelayedBlockingAction(Object tag, int delay, int timeout) { this.tag = tag; this.delay = delay; this.timeout = timeout; this.timeInitiated = System.currentTimeMillis(); } @Override public final void run() { synchronized (lock){ if (invalidated || fired) { return; } fired = true; } runAction(); } /** * Performs the actual action */ protected abstract void runAction(); public boolean isSameType(DelayedBlockingAction action) { return this.tag.equals(action.tag); } /** * Signal to this action that it should no longer execute itself when it is triggered. Generally * this is done because a different action has taken its place, or the context is no longer * available to receive the action. * * @return false if the action could not be prevented due to having already triggered. */ public boolean invalidate() { synchronized (lock) { if (this.fired) { return false; } else { this.invalidated = true; return true; } } } public boolean isPending() { synchronized (lock) { if (!this.fired && hasTimedOut()) { this.invalidated = true; } return !(this.fired || this.invalidated); } } protected boolean hasTimedOut() { long now = System.currentTimeMillis(); return (timeInitiated + timeout) < now; } public int getDelay() { return delay; } }