public interface AgentResultCache {
    <ResultType> void put(String agentIdentifier, ResultType value, long initialCacheLifetimeMs);
    <ResultType> ResultType get(String agentIdentifier, long requestCacheLifetimeMs);
    void removeCache(String agentIdentifier);
} public class StandardAgentResultCache implements AgentResultCache, InactivityCleanupListener {
    private static final String TAG = StandardAgentResultCache.class.getSimpleName();

    private static final long DEFAULT_INTERVAL_MS = 250;
    private static final long DEFAULT_IDLE_MS = TimeUnit.SECONDS.toMillis(30);

    private final InactivityCleanupRunnable mInactivityCleanupRunnable;
    private final Map<String, CachedAgentResult> mCacheMap;

    private StandardAgentResultCache(Builder builder) {
        mCacheMap = Collections.synchronizedMap(new HashMap<String, CachedAgentResult>());
        mInactivityCleanupRunnable = builder.inactivityCleanupRunnable;
    }

    public <ResultType> void put(String agentIdentifier, ResultType value, long initialCacheLifetimeMs) {
        mInactivityCleanupRunnable.restartTimer();
        if (initialCacheLifetimeMs > 0) {
            CachedAgentResult<ResultType> cachedAgentResult = new CachedAgentResult<>(value, initialCacheLifetimeMs);
            mCacheMap.put(agentIdentifier, cachedAgentResult);
        }
    }

    @Override
    public <ResultType> ResultType get(String agentIdentifier, long requestCacheLifetimeMs) {
        mInactivityCleanupRunnable.restartTimer();
        ResultType result = null;
        try {
            //This isn't unchecked, we know that it should be the type we expect. We catch the exception anyway.
            @SuppressWarnings("unchecked")
            CachedAgentResult<ResultType> cachedAgentResult = mCacheMap.get(agentIdentifier);

            if (cachedAgentResult != null) {
                //Increase maximum lifetime if this lifetime is greater.
                cachedAgentResult.setMaximumLifetimeMs(requestCacheLifetimeMs);

                //Use client specified lifetime to determine if this is a cache hit.
                if (!cachedAgentResult.isExpiredForSpecifiedLifetime(requestCacheLifetimeMs)) {
                    result = cachedAgentResult.getValue();
                }
            }
        } catch (ClassCastException e) {
            Log.e(TAG, "Cached object was an unexpected type", e);
        }
        return result;
    }

    public void cleanStaleCache() {
        synchronized (mCacheMap) {
            for (Iterator<String> cacheMapKeyIterator = mCacheMap.keySet().iterator(); cacheMapKeyIterator.hasNext(); ) {
                String agentIdentifier = cacheMapKeyIterator.next();
                CachedAgentResult cachedAgentResult = mCacheMap.get(agentIdentifier);
                if (cachedAgentResult != null && cachedAgentResult.isExpiredForMaxLifetime()) {
                    cacheMapKeyIterator.remove();
                }
            }
        }
    }

    @Override
    public void removeCache(String agentIdentifier) {
        mCacheMap.remove(agentIdentifier);
    }

    @Override
    public boolean isBusy() {
        return !mCacheMap.isEmpty();
    }

    @Override
    public void enterIdleState() {
        //Nothing to do here.
    }

    @Override
    public void performCleanup() {
        cleanStaleCache();
    }

    /**
     * Create a new builder instance.
     */
    public static Builder builder() {
        return new Builder();
    }

    /**
     * Builder implementation required to compose a StandardAgentResultCache.
     */
    public static class Builder {

        private InactivityCleanupRunnable inactivityCleanupRunnable;

        public Builder setInactivityCleanupRunnable(InactivityCleanupRunnable inactivityCleanupRunnable) {
            this.inactivityCleanupRunnable = inactivityCleanupRunnable;
            return this;
        }

        public StandardAgentResultCache build() {

            if (inactivityCleanupRunnable == null) {
                setInactivityCleanupRunnable(new StandardInactivityCleanupRunnable(DEFAULT_IDLE_MS, DEFAULT_INTERVAL_MS));
            }

            StandardAgentResultCache standardAgentResultCache = new StandardAgentResultCache(this);
            inactivityCleanupRunnable.setListener(standardAgentResultCache);

            return new StandardAgentResultCache(this);
        }
    }
} public interface PriorityQueueingPoolExecutorService {

    /**
     * Add the list of jobs to the work queue
     */
    void enqueue(List<Job> jobList);

    /**
     * Add the job(s) to the work queue
     */
    void enqueue(Job ... jobList);

    /**
     * Update the priority on a queued job identified by the jobId to the supplied priority.
     * If the priority changes to {@link com.bottlerocketstudios.groundcontrol.executor.JobPriority#IMMEDIATE}
     * then the job will be executed immediately.
     */
    void updateJobPriority(long jobId, JobPriority priority);

    /**
     * Return a number unique to this instance of the service which can be used for the next job.
     */
    long getNextJobId();

    /**
     * Return true if there are any jobs queued or executing currently.
     */
    boolean hasRunningJobs();

    /**
     * Return true if the service has been put into idle state due to inactivity.
     */
    boolean isIdle();
} public class StandardPriorityQueueingPoolExecutorService implements PriorityQueueingPoolExecutorService, InactivityCleanupListener {

    private static final JobPriorityAndIdComparator JOB_PRIORITY_COMPARATOR = new JobPriorityAndIdComparator();
    private static final int DEFAULT_QUEUE_SIZE = 10;

    private final int mMaxSimultaneousJobs;
    private final PriorityBlockingQueue<Job> mJobQueue;
    private final List<RunningJob> mRunningJobs;
    private final boolean mEnableLogging;
    private final ThreadFactory mThreadFactory;
    private final String mLogTag;
    private final int mHighSpeedQueueThreshold;
    private final IdSequence mIdSequence;
    private final InactivityCleanupRunnable mJobCleanupRunnable;
    private final int mOsThreadPriority;

    private ExecutorService mExecutorService;
    private boolean mIdle;

    /**
     * Enforce Builder usage.
     */
    private StandardPriorityQueueingPoolExecutorService(Builder builder) {
        mMaxSimultaneousJobs = builder.maxSimultaneousJobs;
        mJobQueue = new PriorityBlockingQueue<>(DEFAULT_QUEUE_SIZE, JOB_PRIORITY_COMPARATOR);
        mRunningJobs = Collections.synchronizedList(new ArrayList<RunningJob>());
        mJobCleanupRunnable = builder.jobCleanupRunnable;
        mEnableLogging = builder.enableLogging;
        mThreadFactory = builder.threadFactory;
        mLogTag = builder.logTag;
        mHighSpeedQueueThreshold = builder.highSpeedQueueThreshold;
        mOsThreadPriority = builder.osThreadPriority;

        mIdSequence = new IdSequence();
    }

    @Override
    public void enqueue(List<Job> jobList) {
        for (Job job: jobList) {
            if (mEnableLogging) Log.i(mLogTag, "Job entered Queue " + job.toString());
            if (job.getPriority().equals(JobPriority.IMMEDIATE)) {
                if (mEnableLogging) Log.i(mLogTag, "Executing immediate priority work " + job.toString());
                executeJob(job);
            } else {
                if (mEnableLogging) Log.i(mLogTag, "Queueing job " + job.toString());
                mJobQueue.offer(job);
            }
        }
        processQueue();
    }

    @Override
    public void enqueue(Job ... jobList) {
        enqueue(Arrays.asList(jobList));
    }

    private void processQueue() {
        synchronized (mJobQueue) {
            while (mJobQueue.size() > 0 && mRunningJobs.size() < mMaxSimultaneousJobs) {
                Job job = mJobQueue.remove();
                executeJob(job);
            }
        }
        if (mJobQueue.size() > mHighSpeedQueueThreshold && !mJobCleanupRunnable.isHighSpeedMode()) {
            if (mEnableLogging) Log.i(mLogTag, "The job queue is very large, entering high speed queue processing mode.");
            mJobCleanupRunnable.enterHighSpeedMode();
        } else if (mJobQueue.size() < mHighSpeedQueueThreshold && mJobCleanupRunnable.isHighSpeedMode()) {
            if (mEnableLogging) Log.i(mLogTag, "Exiting high speed queue processing mode.");
            mJobCleanupRunnable.exitHighSpeedMode();
        }
    }

    /**
     * Start job running on the ExecutorService.
     */
    private void executeJob(Job job) {
        if (mEnableLogging) Log.i(mLogTag, "Executing job " + job.toString());
        mIdle = false;
        mJobCleanupRunnable.restartTimer();
        mRunningJobs.add(new RunningJob(job, getExecutorService().submit(job.getRunnable()), getTime()));
        job.notifyJobExecuted();
    }

    private long getTime() {
        return SystemClock.uptimeMillis();
    }

    @Override
    public boolean isBusy() {
        return hasRunningJobs();
    }

    @Override
    public void enterIdleState() {
        mIdle = true;
        if(mExecutorService != null) {
            if (mEnableLogging) Log.i(mLogTag, "Entering idle state");
            mExecutorService.shutdown();
            mExecutorService = null;
        }
    }

    @Override
    public void performCleanup() {
        synchronized (mRunningJobs) {
            for (Iterator<RunningJob> runningJobIterator = mRunningJobs.iterator(); runningJobIterator.hasNext(); ) {
                RunningJob runningJob = runningJobIterator.next();
                long now = getTime();
                if (runningJob.isComplete()) {
                    if (mEnableLogging) Log.i(mLogTag, "Cleaning up completed job " + runningJob.toString());
                    runningJobIterator.remove();
                } else if (runningJob.isPastExecutionTimeLimit(now)) {
                    Log.w(mLogTag, "Killing overdue job " + runningJob.toString());
                    runningJob.cancel(true);
                    runningJobIterator.remove();
                }
            }
        }

        processQueue();
    }

    /**
     * Lazy load ExecutorService and reinitialize if we have been idled.
     */
    private ExecutorService getExecutorService() {
        if (mExecutorService == null) {
            if (mEnableLogging) Log.i(mLogTag, "Creating ExecutorService");
            mExecutorService = ThreadPoolExecutorWithExceptions.newCachedThreadPool(mThreadFactory, mOsThreadPriority);
        }
        return mExecutorService;
    }

    @Override
    public boolean isIdle() {
        return mIdle;
    }

    @Override
    public long getNextJobId() {
        return mIdSequence.getNext();
    }

    @Override
    public boolean hasRunningJobs() {
        return !(mRunningJobs.size() == 0 && mJobQueue.size() == 0);
    }

    @Override
    public void updateJobPriority(long jobId, JobPriority priority) {

        synchronized (mRunningJobs) {
            for (RunningJob runningJob : mRunningJobs) {
                //First determine if job is already running and skip it if it is.
                if (runningJob.getJobId() == jobId) {
                    return;
                }
            }
        }

        synchronized (mJobQueue) {
            for (Iterator<Job> jobIterator = mJobQueue.iterator(); jobIterator.hasNext(); ) {
                Job job = jobIterator.next();
                if (job.getId() == jobId) {
                    //Found it in the queue.
                    if (priority.equals(JobPriority.IMMEDIATE)) {
                        //If it is immediate priority, remove from queue and execute.
                        jobIterator.remove();
                        executeJob(job);
                    } else {
                        //Otherwise, just change the priority. It will dequeue appropriately.
                        job.setPriority(priority);
                    }
                    return;
                }
            }
        }
    }

    public static Builder builder() {
        return new Builder();
    }

    public static class Builder {
        private static final long DEFAULT_INACTIVITY_IDLE_MS = TimeUnit.MINUTES.toMillis(2);
        private static final long DEFAULT_NORMAL_CLEANUP_INTERVAL_MS = 100;
        private static final long DEFAULT_HIGH_SPEED_CLEANUP_INTERVAL_MS = 5;
        private static final int DEFAULT_MAX_SIMULTANEOUS_JOBS = 10;
        private static final int DEFAULT_HIGH_SPEED_QUEUE_THRESHOLD = 20;
        private static final int DEFAULT_OS_THREAD_PRIORITY = android.os.Process.THREAD_PRIORITY_BACKGROUND;

        private InactivityCleanupRunnable jobCleanupRunnable;
        private int maxSimultaneousJobs;
        private Boolean enableLogging;
        private ThreadFactory threadFactory;
        private String logTag;
        private int highSpeedQueueThreshold;
        private Integer osThreadPriority;

        public PriorityQueueingPoolExecutorService build() {
            //Configure defaults if unspecified

            if (logTag == null) {
                logTag = StandardPriorityQueueingPoolExecutorService.class.getSimpleName();
            }

            if (enableLogging == null) {
                enableLogging = false;
            }

            if (enableLogging) {
                Log.w(logTag, "Logging is enabled. This will reduce performance.");
            }

            if (jobCleanupRunnable == null) {
                setJobCleanupRunnable(new StandardInactivityCleanupRunnable(DEFAULT_INACTIVITY_IDLE_MS, DEFAULT_NORMAL_CLEANUP_INTERVAL_MS, DEFAULT_HIGH_SPEED_CLEANUP_INTERVAL_MS));
            }

            if (maxSimultaneousJobs <= 0) {
                setMaxSimultaneousJobs(DEFAULT_MAX_SIMULTANEOUS_JOBS);
            }

            if (threadFactory == null) {
                setThreadFactory(Executors.defaultThreadFactory());
            }

            if (highSpeedQueueThreshold <= 0) {
                setHighSpeedQueueThreshold(DEFAULT_HIGH_SPEED_QUEUE_THRESHOLD);
            }

            if (osThreadPriority == null) {
                setOsThreadPriority(DEFAULT_OS_THREAD_PRIORITY);
            }

            //Finish creation and linking.

            StandardPriorityQueueingPoolExecutorService priorityQueueingPoolExecutorService = new StandardPriorityQueueingPoolExecutorService(this);
            jobCleanupRunnable.setListener(priorityQueueingPoolExecutorService);

            return priorityQueueingPoolExecutorService;
        }

        /**
         * Runnable that will handle notifying the service to cleanup expired items and enter idle state.
         */
        public Builder setJobCleanupRunnable(InactivityCleanupRunnable jobCleanupRunnable) {
            this.jobCleanupRunnable = jobCleanupRunnable;
            return this;
        }

        /**
         * Maximum number of prioritized jobs to execute simultaneously. Immediate priority jobs will
         * be executed immediately, but will count toward the total when scheduling the next job.
         */
        public Builder setMaxSimultaneousJobs(int maxSimultaneousJobs) {
            this.maxSimultaneousJobs = maxSimultaneousJobs;
            return this;
        }

        /**
         * Enable logging for debugging purposes.
         */
        public Builder setEnableLogging(boolean enableLogging) {
            this.enableLogging = enableLogging;
            return this;
        }

        /**
         * Provide a thread factory to use for threads executed by this service. 
         */
        public Builder setThreadFactory(ThreadFactory threadFactory) {
            this.threadFactory = threadFactory;
            return this;
        }

        /**
         * Set the log tag to be used with this service if logging is enabled.
         */
        public Builder setLogTag(String logTag) {
            this.logTag = logTag;
            return this;
        }

        /**
         * Set the threshold size of the queue at which the queue will be processed at the InactivityCleanupRunnable high speed interval.
         */
        public Builder setHighSpeedQueueThreshold(int highSpeedQueueThreshold) {
            this.highSpeedQueueThreshold = highSpeedQueueThreshold;
            return this;
        }

        /**
         * Set the Linux OS Thread priority that thread pool executions will run with.
         * Use priority values from android.os.Process.
         */
        public Builder setOsThreadPriority(int osThreadPriority) {
            this.osThreadPriority = osThreadPriority;
            return this;
        }
    }
} public interface InactivityCleanupRunnable extends Runnable {

    /**
     * Store a reference to the provided listener for future notifications.
     */
    void setListener(InactivityCleanupListener inactivityCleanupListener);

    /**
     * Stop running updates.
     */
    void stop();

    /**
     * Restart the idle timeout for the associated listener.
     */
    void restartTimer();

    /**
     * Start running cleanup on high speed interval
     */
    void enterHighSpeedMode();

    /**
     * Start running cleanup on normal interval
     */
    void exitHighSpeedMode();

    /**
     * Return true if high speed processing is underway.
     */
    boolean isHighSpeedMode();
} public class StandardInactivityCleanupRunnable implements InactivityCleanupRunnable {

    private final long mInactivityIdleMs;
    private final long mNormalCleanupIntervalMs;
    private final long mHighSpeedCleanupIntervalMs;
    private final String mLooperGuid = UUID.randomUUID().toString();
    private final HandlerCache mHandlerCache;

    private InactivityCleanupListener mInactivityCleanupListener;
    private boolean mStopped;
    private long mLastActivityTimestamp;
    private long mCleanupIntervalMs;

    public StandardInactivityCleanupRunnable(long inactivityIdleMs, long normalCleanupIntervalMs) {
        this(inactivityIdleMs, normalCleanupIntervalMs, normalCleanupIntervalMs / 2L);
    }

    public StandardInactivityCleanupRunnable(long inactivityIdleMs, long normalCleanupIntervalMs, long highSpeedCleanupIntervalMs) {
        mInactivityIdleMs = inactivityIdleMs;
        mNormalCleanupIntervalMs = normalCleanupIntervalMs;
        mHighSpeedCleanupIntervalMs = highSpeedCleanupIntervalMs;
        mStopped = true;

        mCleanupIntervalMs = mNormalCleanupIntervalMs;
        mHandlerCache = new HandlerCache();
    }

    private void start() {
        mStopped = false;
        postSelf();
    }

    private void postSelf() {
        getHandler().removeCallbacks(this);
        getHandler().postDelayed(this, mCleanupIntervalMs);
    }

    @Override
    public void setListener(InactivityCleanupListener inactivityCleanupListener) {
        mInactivityCleanupListener = inactivityCleanupListener;
    }

    @Override
    public void stop() {
        mStopped = true;
        getHandler().removeCallbacks(this);
        mHandlerCache.stopHandler(getLooperGuid());
    }

    @Override
    public void run() {
        if (!mStopped) {
            mInactivityCleanupListener.performCleanup();
            checkInactivityShutdown();
            postSelf();
        }
    }

    @Override
    public void restartTimer() {
        mLastActivityTimestamp = getTime();
        if (mStopped) start();
    }

    @Override
    public void enterHighSpeedMode() {
        mCleanupIntervalMs = mHighSpeedCleanupIntervalMs;
    }

    @Override
    public void exitHighSpeedMode() {
        mCleanupIntervalMs = mNormalCleanupIntervalMs;
    }

    @Override
    public boolean isHighSpeedMode() {
        return mCleanupIntervalMs == mHighSpeedCleanupIntervalMs;
    }

    private void checkInactivityShutdown() {
        if (!mInactivityCleanupListener.isBusy()
                && ((getTime() - mLastActivityTimestamp) > mInactivityIdleMs)) {
            stop();
            mInactivityCleanupListener.enterIdleState();
        }
    }

    private Handler getHandler() {
        return  mHandlerCache.getHandler(getLooperGuid());
    }

    private String getLooperGuid() {
        return mLooperGuid;
    }

    private long getTime() {
        return SystemClock.uptimeMillis();
    }
} public class HandlerCache {

    private final Map<String, Handler> mHandlerMap;
    private final int mDefaultOsThreadPriority;


    /**
     * Create a new HandlerCache using the Process.THREAD_PRIORITY_BACKGROUND
     */
    public HandlerCache() {
        this(Process.THREAD_PRIORITY_BACKGROUND);
    }

    /**
     * Create a new HandlerCache that will use supplied OS thread priority as default.
     */
    public HandlerCache(int defaultOsThreadPriority) {
        mHandlerMap = Collections.synchronizedMap(new HashMap<String, Handler>());
        mDefaultOsThreadPriority = defaultOsThreadPriority;
    }

    /**
     * Get or create a handler with a looper using the associated looperId creating a looping Looper if necessary. Uses
     * default OS thread priority provided to constructor.
     */
    public Handler getHandler(String looperId) {
        return getHandler(looperId, mDefaultOsThreadPriority);
    }

    /**
     * Get or create a handler with a looper using the associated looperId creating a looping Looper if necessary. If
     * creating a new looper, will use the supplied osThreadPriority.
     */
    public synchronized Handler getHandler(String looperId, int osThreadPriority) {
        Handler handler = mHandlerMap.get(looperId);
        if (handler == null) {
            handler = new Handler(LooperController.getLooper(looperId, osThreadPriority));
            mHandlerMap.put(looperId, handler);
        }
        return handler;
    }

    /**
     * Shutdown the looper associated with the specified handler id.
     */
    public synchronized void stopHandler(String looperId) {
        mHandlerMap.remove(looperId);
        LooperController.stopLooper(looperId);
    }

} public class AgentPolicy {
    private final String mCallbackLooperId;
    private final long mPolicyTimeoutMs;
    private final long mParallelCallbackTimeoutMs;
    private final long mMaxCacheAgeMs;
    private final JobPriority mJobPriority;
    private final boolean mParallelBackgroundCallback;
    private final boolean mBypassCache;
    private final boolean mClearCache;

    public AgentPolicy(AgentPolicyBuilder builder) {
        mCallbackLooperId = builder.getCallbackLooperId();
        mPolicyTimeoutMs = builder.getPolicyTimeoutMs();
        mMaxCacheAgeMs = builder.getMaxCacheAgeMs();
        mJobPriority = builder.getJobPriority();
        mParallelBackgroundCallback = builder.isParallelBackgroundCallback();
        mBypassCache = builder.shouldBypassCache();
        mParallelCallbackTimeoutMs = builder.getParallelCallbackTimeoutMs();
        mClearCache = builder.shouldClearCache();
    }

    public String getCallbackLooperId() {
        return mCallbackLooperId;
    }

    public long getPolicyTimeoutMs() {
        return mPolicyTimeoutMs;
    }

    public long getMaxCacheAgeMs() {
        return mMaxCacheAgeMs;
    }

    public JobPriority getJobPriority() {
        return mJobPriority;
    }

    public boolean isParallelBackgroundCallback() {
        return mParallelBackgroundCallback;
    }

    public boolean shouldBypassCache() {
        return mBypassCache;
    }

    public long getParallelCallbackTimeoutMs() {
        return mParallelCallbackTimeoutMs;
    }

    public boolean shouldClearCache() {
        return mClearCache;
    }
} public class StandardAgentPolicyBuilder implements AgentPolicyBuilder {

    private static final String DEFAULT_CALLBACK_LOOPER_ID = LooperController.UI_LOOPER_ID;
    private static final long DEFAULT_POLICY_TIMEOUT_MS = TimeUnit.MINUTES.toMillis(5);
    private static final long DEFAULT_CACHE_AGE_MS = TimeUnit.MINUTES.toMillis(2);
    private static final long DEFAULT_PARALLEL_CALLBACK_TIMEOUT_MS = TimeUnit.MINUTES.toMillis(6);
    private static final JobPriority DEFAULT_JOB_PRIORITY = JobPriority.NORMAL;
    private static final boolean DEFAULT_PARALLEL_BACKGROUND_CALLBACK = false;

    private String mDefaultCallbackLooperId;
    private long mDefaultPolicyTimeoutMs;
    private long mDefaultCacheAgeMs;
    private long mDefaultParallelCallbackTimeoutMs;
    private JobPriority mDefaultJobPriority;
    private boolean mDefaultParallelBackgroundCallback;

    private String mCallbackLooperId;
    private long mPolicyTimeoutMs;
    private long mMaxCacheAgeMs;
    private long mParallelCallbackTimeoutMs;
    private JobPriority mJobPriority;
    private Boolean mParallelBackgroundCallback;
    private boolean mBypassCache;
    private boolean mClearCache;

    private boolean mCallbackSet;

    public StandardAgentPolicyBuilder() {
        mDefaultCallbackLooperId = DEFAULT_CALLBACK_LOOPER_ID;
        mDefaultPolicyTimeoutMs = DEFAULT_POLICY_TIMEOUT_MS;
        mDefaultCacheAgeMs = DEFAULT_CACHE_AGE_MS;
        mDefaultParallelCallbackTimeoutMs = DEFAULT_PARALLEL_CALLBACK_TIMEOUT_MS;
        mDefaultJobPriority = DEFAULT_JOB_PRIORITY;
        mDefaultParallelBackgroundCallback = DEFAULT_PARALLEL_BACKGROUND_CALLBACK;
        clear();
    }

    public void setDefaultCallbackLooperId(String defaultCallbackLooperId) {
        if (mDefaultParallelBackgroundCallback && defaultCallbackLooperId != null) {
            throw new IllegalArgumentException("You must setDefaultParallelBackgroundCallback(false) first");
        }
        mDefaultCallbackLooperId = defaultCallbackLooperId;
    }

    public void setDefaultPolicyTimeoutMs(long defaultPolicyTimeoutMs) {
        mDefaultPolicyTimeoutMs = defaultPolicyTimeoutMs;
    }

    public void setDefaultCacheAgeMs(long defaultCacheAgeMs) {
        mDefaultCacheAgeMs = defaultCacheAgeMs;
    }

    public void setDefaultParallelCallbackTimeoutMs(long defaultParallelCallbackTimeoutMs) {
        mDefaultParallelCallbackTimeoutMs = defaultParallelCallbackTimeoutMs;
    }

    public void setDefaultJobPriority(JobPriority defaultJobPriority) {
        mDefaultJobPriority = defaultJobPriority;
    }

    public void setDefaultParallelBackgroundCallback(boolean defaultParallelBackgroundCallback) {
        if (mDefaultCallbackLooperId != null && defaultParallelBackgroundCallback) {
            throw new IllegalArgumentException("You must setDefaultCallbackLooperId(null) first");
        }
        mDefaultParallelBackgroundCallback = defaultParallelBackgroundCallback;
    }

    @Override
    public AgentPolicyBuilder setCallbackLooperId(String callbackLooperId) {
        if (mCallbackSet) {
            throw new IllegalArgumentException("Cannot call either setParallelBackgroundCallback or setCallbackLooperId again without clearing");
        } else if (TextUtils.isEmpty(callbackLooperId)) {
            throw new IllegalArgumentException("callbackLooperId cannot be empty or null. Maybe you were trying to setParallelBackgroundCallback(true)?");
        }
        mCallbackLooperId = callbackLooperId;
        mCallbackSet = true;
        if (isBackgroundLooper(callbackLooperId)) {
            disableCache();
        }
        return this;
    }

    @Override
    public String getCallbackLooperId() {
        return mCallbackLooperId;
    }

    @Override
    public AgentPolicyBuilder setParallelBackgroundCallback(boolean parallelBackgroundCallback) {
        if (mCallbackSet) {
            throw new IllegalArgumentException("Cannot call either setParallelBackgroundCallback or setCallbackLooperId again without clearing");
        } else if (!parallelBackgroundCallback) {
            throw new IllegalArgumentException("Do not setParallelBackgroundCallback(false) instead call setCallbackLooperId");
        }
        mParallelBackgroundCallback = true;
        mCallbackSet = true;
        disableCache();
        return this;
    }

    @Override
    public boolean isParallelBackgroundCallback() {
        return mParallelBackgroundCallback;
    }

    @Override
    public AgentPolicyBuilder setPolicyTimeoutMs(long policyTimeoutMs) {
        mPolicyTimeoutMs = policyTimeoutMs;
        return this;
    }

    @Override
    public long getPolicyTimeoutMs() {
        return mPolicyTimeoutMs;
    }

    @Override
    public AgentPolicyBuilder setMaxCacheAgeMs(long maxCacheAgeMs) {
        mMaxCacheAgeMs = maxCacheAgeMs;
        return this;
    }

    @Override
    public long getMaxCacheAgeMs() {
        return mMaxCacheAgeMs;
    }

    @Override
    public long getParallelCallbackTimeoutMs() {
        return mParallelCallbackTimeoutMs;
    }

    @Override
    public AgentPolicyBuilder setClearCache(boolean clearCache) {
        mClearCache = clearCache;
        return this;
    }

    @Override
    public boolean shouldClearCache() {
        return mClearCache;
    }

    @Override
    public AgentPolicyBuilder setParallelCallbackTimeoutMs(long parallelCallbackTimeoutMs) {
        mParallelCallbackTimeoutMs = parallelCallbackTimeoutMs;
        return this;
    }

    @Override
    public AgentPolicyBuilder setJobPriority(JobPriority jobPriority) {
        mJobPriority = jobPriority;
        return this;
    }

    @Override
    public JobPriority getJobPriority() {
        return mJobPriority;
    }

    @Override
    public AgentPolicyBuilder setBypassCache(boolean bypassCache) {
        mBypassCache = bypassCache;
        return this;
    }

    @Override
    public boolean shouldBypassCache() {
        return mBypassCache;
    }

    @Override
    public AgentPolicyBuilder clear() {
        mCallbackLooperId = null;
        mParallelBackgroundCallback = false;
        mCallbackSet = false;
        setPolicyTimeoutMs(0);
        setMaxCacheAgeMs(-1);
        setParallelCallbackTimeoutMs(0);
        setJobPriority(null);
        setBypassCache(false);
        setClearCache(false);
        return this;
    }

    @Override
    public AgentPolicyBuilder buildUpon(AgentPolicy agentPolicy) {
        clear();

        if (agentPolicy == null) {
            //When given a null policy to build upon, just clear to defaults.
            return this;
        }

        if (agentPolicy.isParallelBackgroundCallback()) {
            setParallelBackgroundCallback(true);
        } else {
            setCallbackLooperId(agentPolicy.getCallbackLooperId());
        }
        setPolicyTimeoutMs(agentPolicy.getPolicyTimeoutMs());
        setMaxCacheAgeMs(agentPolicy.getMaxCacheAgeMs());
        setParallelCallbackTimeoutMs(agentPolicy.getParallelCallbackTimeoutMs());
        setJobPriority(agentPolicy.getJobPriority());
        setBypassCache(agentPolicy.shouldBypassCache());
        setClearCache(agentPolicy.shouldClearCache());
        return this;
    }

    @Override
    public AgentPolicy build() {
        if (!mCallbackSet) {
            if (mDefaultParallelBackgroundCallback) {
                setParallelBackgroundCallback(true);
            } else {
                setCallbackLooperId(mDefaultCallbackLooperId);
            }
        }

        if (getPolicyTimeoutMs() <= 0) {
            setPolicyTimeoutMs(mDefaultPolicyTimeoutMs);
        }

        if (getMaxCacheAgeMs() < 0) {
            setMaxCacheAgeMs(mDefaultCacheAgeMs);
        }

        if (getParallelCallbackTimeoutMs() <= 0) {
            setParallelCallbackTimeoutMs(mDefaultParallelCallbackTimeoutMs);
        }

        if (getJobPriority() == null) {
            setJobPriority(mDefaultJobPriority);
        }

        validatePolicy();

        return new AgentPolicy(this);
    }

    @Override
    public AgentPolicyBuilder disableCache() {
        setBypassCache(true);
        setMaxCacheAgeMs(0);
        return this;
    }

    private void validatePolicy() {
        if (isUsingCacheOnBackgroundRequest()) {
            throw new IllegalStateException("Cannot build a policy that will use cache for a background delivery. Cache should only be used for UI Looper delivery. Call setBypassCache(true).");
        }

        if (shouldClearCache()) {
            //Cannot clear cache without also bypassing it.
            setBypassCache(true);
        }

    }

    private boolean isBackgroundLooper(String callbackLooperId) {
        return !TextUtils.equals(LooperController.UI_LOOPER_ID, callbackLooperId);
    }

    private boolean isUsingCacheOnBackgroundRequest() {
        return !shouldBypassCache() && (isParallelBackgroundCallback() || isBackgroundLooper(getCallbackLooperId()));
    }
} public interface AgentRequestController {
    /**
     * Deliver an individual completion message using the specified LoaderId or by adding it to the
     * background execution queue.
     */
    <ResultType> void deliverCompletion(AgentRequest<ResultType, ?> agentRequest, ResultType result);

    /**
     * Deliver an individual progress message using the specified LoaderId or by adding it to the
     * background execution queue.
     */
    <ProgressType> void deliverProgress(AgentRequest<?, ProgressType> agentRequest, ProgressType progress);

    /**
     * Add a new AgentRequest to the collection that will be notified when the result is delivered.
     */
    void addAgentRequest(AgentRequest agentRequest);

    /**
     * Notify all AgentListeners associated with the agentIdentifier of the result in order of priority.
     */
    <ResultType> void notifyAgentCompletion(String agentIdentifier, ResultType result);

    /**
     * Notify all AgentListeners associated with the agentIdentifier of progress in order of priority.
     */
    <ProgressType> void notifyAgentProgress(String agentIdentifier, ProgressType progress);

    /**
     * Remove the AgentRequest associated with agentIdentifier and supplied AgentListener.
     */
    void removeRequestForAgent(String agentIdentifier, AgentListener agentListener);

    /**
     * Return true if there are AgentRequests pending delivery for the specified agentIdentifier.
     */
    boolean hasActiveRequests(String agentIdentifier);

    /**
     * Walk through collection of AgentRequests and notify those that are past their deadline with
     * a null completion message.
     */
    void notifyPastDeadline();
} public class StandardAgentRequestController implements AgentRequestController {
    private static final String TAG = StandardAgentRequestController.class.getSimpleName();

    private final HandlerCache mHandlerCache;
    private final PriorityQueueingPoolExecutorService mListenerExecutorService;
    private final Map<String, List<AgentRequest>> mAgentRequestMap;

    public StandardAgentRequestController(PriorityQueueingPoolExecutorService listenerExecutorService, HandlerCache handlerCache) {
        mAgentRequestMap = Collections.synchronizedMap(new HashMap<String, List<AgentRequest>>());
        mListenerExecutorService = listenerExecutorService;
        mHandlerCache = handlerCache;
    }

    @Override
    public  <ResultType> void deliverCompletion(AgentRequest<ResultType, ?> agentRequest, ResultType result) {
        ListenerCompletionRunnable<ResultType> completionRunnable = new ListenerCompletionRunnable<>(
                agentRequest.getAgentIdentifier(),
                agentRequest.getAgentListener(),
                result);

        if (agentRequest.getCallbackLooperId() != null) {
            mHandlerCache.getHandler(agentRequest.getCallbackLooperId()).post(completionRunnable);
        } else {
            Job listenerCompletionJob = new Job(mListenerExecutorService.getNextJobId(), completionRunnable, agentRequest.getParallelCallbackTimeoutMs(), agentRequest.getJobPriority());
            mListenerExecutorService.enqueue(listenerCompletionJob);
        }
    }

    @Override
    public <ProgressType> void deliverProgress(AgentRequest<?, ProgressType> agentRequest, ProgressType progress) {
        ListenerProgressRunnable<ProgressType> completionRunnable = new ListenerProgressRunnable<>(
                agentRequest.getAgentIdentifier(),
                agentRequest.getAgentListener(),
                progress);

        if (agentRequest.getCallbackLooperId() != null) {
            mHandlerCache.getHandler(agentRequest.getCallbackLooperId()).post(completionRunnable);
        } else {
            Job listenerCompletionJob = new Job(mListenerExecutorService.getNextJobId(), completionRunnable, agentRequest.getParallelCallbackTimeoutMs(), agentRequest.getJobPriority());
            mListenerExecutorService.enqueue(listenerCompletionJob);
        }
    }

    @Override
    public void addAgentRequest(AgentRequest agentRequest) {
        synchronized (mAgentRequestMap) {
            List<AgentRequest> agentRequestList = mAgentRequestMap.get(agentRequest.getAgentIdentifier());
            if (agentRequestList == null) {
                agentRequestList = Collections.synchronizedList(new ArrayList<AgentRequest>());
                mAgentRequestMap.put(agentRequest.getAgentIdentifier(), agentRequestList);
            }
            agentRequestList.add(agentRequest);
        }
    }

    private void sortAgentRequestListByPriority(List<AgentRequest> agentRequestList) {
        Collections.sort(agentRequestList, new AgentRequestPriorityComparator());
    }

    @Override
    public <ResultType> void notifyAgentCompletion(String agentIdentifier, ResultType result) {
        List<AgentRequest> agentRequestList = mAgentRequestMap.remove(agentIdentifier);
        if (agentRequestList != null) {
            try {
                /*
                 * Deliver results in order of priority. If a Handler is being used, this matters
                 * as Handlers are FIFO and serial.
                 */
                sortAgentRequestListByPriority(agentRequestList);

                //We know that all AgentRequests for the specified agentIdentifier are of the expected type.
                //noinspection unchecked
                for (AgentRequest<ResultType, ?> agentRequest : agentRequestList) {
                    deliverCompletion(agentRequest, result);
                }
            } catch (ClassCastException e) {
                Log.e(TAG, "AgentRequest ResultType mismatch " + agentIdentifier, e);
            }
        }
    }

    @Override
    public <ProgressType> void notifyAgentProgress(String agentIdentifier, ProgressType progress) {
        List<AgentRequest> agentRequestList = mAgentRequestMap.get(agentIdentifier);
        if (agentRequestList != null) {
            try {
                /*
                 * Deliver progress in order of priority. If a Handler is being used, this matters
                 * as Handlers are FIFO and serial.
                 */
                List<AgentRequest> snapshot = new ArrayList<>(agentRequestList);
                sortAgentRequestListByPriority(snapshot);

                //We know that all AgentRequests for the specified agentIdentifier are of the expected type.
                //noinspection unchecked
                for (AgentRequest<?, ProgressType> agentRequest : snapshot) {
                    deliverProgress(agentRequest, progress);
                }
            } catch (ClassCastException e) {
                Log.e(TAG, "AgentRequest ProgressType mismatch " + agentIdentifier, e);
            }
        }
    }

    @Override
    public void removeRequestForAgent(String agentIdentifier, AgentListener agentListener) {
        List<AgentRequest> agentRequestList = mAgentRequestMap.get(agentIdentifier);
        if (agentRequestList != null) {
            //Find the associated request for this agentId, listener combo.
            for (Iterator<AgentRequest> agentRequestIterator = agentRequestList.iterator(); agentRequestIterator.hasNext();) {
                AgentRequest agentRequest = agentRequestIterator.next();
                if (agentRequest.getAgentListener().equals(agentListener)) {
                    agentRequestIterator.remove();
                }
            }

            //There are no more associated requests, remove the entire list.
            if (agentRequestList.size() == 0) {
                mAgentRequestMap.remove(agentIdentifier);
            }
        }
    }

    @Override
    public boolean hasActiveRequests(String agentIdentifier) {
        List<AgentRequest> agentRequestList = mAgentRequestMap.get(agentIdentifier);
        return (agentRequestList != null && agentRequestList.size() > 0);
    }

    @Override
    public void notifyPastDeadline() {
        synchronized (mAgentRequestMap) {
            for (String agentIdentifier : mAgentRequestMap.keySet()) {
                List<AgentRequest> agentRequestList = mAgentRequestMap.get(agentIdentifier);
                for (Iterator<AgentRequest> agentRequestIterator = agentRequestList.iterator(); agentRequestIterator.hasNext(); ) {
                    AgentRequest agentRequest = agentRequestIterator.next();
                    if (agentRequest.isPastDeadline()) {
                        //The type of the AgentRequest does not matter, we are delivering null.
                        //noinspection unchecked
                        deliverCompletion(agentRequest, null);
                        agentRequestIterator.remove();
                    }
                }
            }
        }
    }
} public interface AgentTetherBuilder {
    /**
     * The AgentExecutor this tether is connected to.
     */
    AgentTetherBuilder setAgentExecutor(AgentExecutor agentExecutor);

    AgentExecutor getAgentExecutor();

    /**
     * The agentIdentifier associated with this Tether.
     */
    AgentTetherBuilder setAgentIdentifier(String agentIdentifier);

    String getAgentIdentifier();

    /**
     * The AgentListener associated with this Tether.
     */
    AgentTetherBuilder setAgentListener(AgentListener agentListener);

    AgentListener getAgentListener();

    /**
     * Clear to build a new instance.
     */
    AgentTetherBuilder clear();

    /**
     * Create a new instance of the AgentTether using the supplied values.
     */
    AgentTether build();
} public class StandardAgentTetherBuilder implements AgentTetherBuilder {

    private AgentExecutor mAgentExecutor;
    private String mAgentIdentifier;
    private AgentListener mAgentListener;

    @Override
    public AgentTetherBuilder setAgentExecutor(AgentExecutor agentExecutor) {
        mAgentExecutor = agentExecutor;
        return this;
    }

    @Override
    public AgentExecutor getAgentExecutor() {
        return mAgentExecutor;
    }

    @Override
    public AgentTetherBuilder setAgentIdentifier(String agentIdentifier) {
        mAgentIdentifier = agentIdentifier;
        return this;
    }

    @Override
    public String getAgentIdentifier() {
        return mAgentIdentifier;
    }

    @Override
    public AgentTetherBuilder setAgentListener(AgentListener agentListener) {
        mAgentListener = agentListener;
        return this;
    }

    @Override
    public AgentListener getAgentListener() {
        return mAgentListener;
    }

    @Override
    public AgentTetherBuilder clear() {
        setAgentIdentifier(null);
        setAgentExecutor(null);
        setAgentListener(null);
        return this;
    }

    @Override
    public AgentTether build() {
        return new StandardAgentTether(this);
    }
}
import com.bottlerocketstudios.groundcontrol.cache.AgentResultCache;
import com.bottlerocketstudios.groundcontrol.cache.StandardAgentResultCache;
import com.bottlerocketstudios.groundcontrol.executor.PriorityQueueingPoolExecutorService;
import com.bottlerocketstudios.groundcontrol.executor.StandardPriorityQueueingPoolExecutorService;
import com.bottlerocketstudios.groundcontrol.inactivity.InactivityCleanupRunnable;
import com.bottlerocketstudios.groundcontrol.inactivity.StandardInactivityCleanupRunnable;
import com.bottlerocketstudios.groundcontrol.looper.HandlerCache;
import com.bottlerocketstudios.groundcontrol.policy.AgentPolicy;
import com.bottlerocketstudios.groundcontrol.policy.StandardAgentPolicyBuilder;
import com.bottlerocketstudios.groundcontrol.request.AgentRequestController;
import com.bottlerocketstudios.groundcontrol.request.StandardAgentRequestController;
import com.bottlerocketstudios.groundcontrol.tether.AgentTetherBuilder;
import com.bottlerocketstudios.groundcontrol.tether.StandardAgentTetherBuilder;
import java.util.concurrent.TimeUnit;
/*
 * Copyright (c) 2016. Bottle Rocket LLC
 * 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.bottlerocketstudios.groundcontrol;



/**
 * Builder for AgentExecutor. Required for creation of new instances of AgentExecutor.
 */
public class AgentExecutorBuilder {

    private static final long DEFAULT_INACTIVITY_IDLE_MS = TimeUnit.MINUTES.toMillis(2);
    private static final long DEFAULT_CLEANUP_INTERVAL_MS = 250;
    private static final long DEFAULT_ABANDONED_CACHE_TIMEOUT_MS = 500;

    private final String mId;
    private AgentResultCache mAgentResultCache;
    private AgentTetherBuilder mAgentTetherBuilder;
    private AgentPolicy mDefaultAgentPolicy;
    private PriorityQueueingPoolExecutorService mCacheExecutorService;
    private PriorityQueueingPoolExecutorService mAgentExecutorService;
    private AgentRequestController mAgentRequestController;
    private HandlerCache mHandlerCache;
    private InactivityCleanupRunnable mInactivityCleanupRunnable;
    private long mAbandonedCacheTimeoutMs;

    /**
     * Create a builder instance with the supplied ID. This ID must be globally unique for the
     * application, but does not need to be the same across future application instances.
     */
    public AgentExecutorBuilder(String id) {
        mId = id;
    }

    public String getId() {
        return mId;
    }

    /**
     * Provide your own AgentResultCache or use the StandardAgentResultCache.Builder to create a special
     * instance of that cache.
     */
    public AgentExecutorBuilder setAgentResultCache(AgentResultCache agentResultCache) {
        mAgentResultCache = agentResultCache;
        return this;
    }

    public AgentResultCache getAgentResultCache() {
        return mAgentResultCache;
    }

    protected AgentResultCache createDefaultAgentResultCache() {
        return StandardAgentResultCache.builder().build();
    }

    /**
     * Provide your own AgentTetherBuilder if you want to build your own special type of Tethers.
     */
    public void setAgentTetherBuilder(AgentTetherBuilder agentTetherBuilder) {
        mAgentTetherBuilder = agentTetherBuilder;
    }

    public AgentTetherBuilder getAgentTetherBuilder() {
        return mAgentTetherBuilder;
    }

    protected AgentTetherBuilder createDefaultAgentTetherBuilder() {
        return new StandardAgentTetherBuilder();
    }

    public AgentPolicy getDefaultAgentPolicy() {
        return mDefaultAgentPolicy;
    }

    /**
     * Set the default AgentPolicy used for requests that do not specify their own.
     */
    public AgentExecutorBuilder setDefaultAgentPolicy(AgentPolicy defaultAgentPolicy) {
        mDefaultAgentPolicy = defaultAgentPolicy;
        return this;
    }

    protected AgentPolicy createDefaultAgentPolicy() {
        return (new StandardAgentPolicyBuilder()).build();
    }

    public PriorityQueueingPoolExecutorService getCacheExecutorService() {
        return mCacheExecutorService;
    }

    /**
     * Provide your own instance of a PriorityQueueingPoolExecutorService to be used for caching.
     */
    public AgentExecutorBuilder setCacheExecutorService(PriorityQueueingPoolExecutorService cacheExecutorService) {
        mCacheExecutorService = cacheExecutorService;
        return this;
    }

    protected PriorityQueueingPoolExecutorService createDefaultCacheExecutorService() {
        return StandardPriorityQueueingPoolExecutorService.builder().setLogTag("CacheExecutorService").build();