WorkTimer.java

/*
 * Copyright 2019 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 androidx.work.impl.utils;

import androidx.annotation.NonNull;
import androidx.annotation.RestrictTo;
import androidx.annotation.VisibleForTesting;
import androidx.work.Logger;
import androidx.work.WorkRequest;

import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.TimeUnit;

/**
 * Manages timers to enforce a time limit for processing {@link WorkRequest}.
 * Notifies a {@link TimeLimitExceededListener} when the time limit
 * is exceeded.
 *
 * @hide
 */
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
public class WorkTimer {

    private static final String TAG = Logger.tagWithPrefix("WorkTimer");

    private final ThreadFactory mBackgroundThreadFactory = new ThreadFactory() {

        private int mThreadsCreated = 0;

        @Override
        public Thread newThread(@NonNull Runnable r) {
            // Delegate to the default factory, but keep track of the current thread being used.
            Thread thread = Executors.defaultThreadFactory().newThread(r);
            thread.setName("WorkManager-WorkTimer-thread-" + mThreadsCreated);
            mThreadsCreated++;
            return thread;
        }
    };

    private final ScheduledExecutorService mExecutorService;
    final Map<String, WorkTimerRunnable> mTimerMap;
    final Map<String, TimeLimitExceededListener> mListeners;
    final Object mLock;

    public WorkTimer() {
        mTimerMap = new HashMap<>();
        mListeners = new HashMap<>();
        mLock = new Object();
        mExecutorService = Executors.newSingleThreadScheduledExecutor(mBackgroundThreadFactory);
    }

    /**
     * Keeps track of execution time for a given {@link androidx.work.impl.model.WorkSpec}.
     * The {@link TimeLimitExceededListener} is notified when the execution time exceeds {@code
     * processingTimeMillis}.
     *
     * @param workSpecId           The {@link androidx.work.impl.model.WorkSpec} id
     * @param processingTimeMillis The allocated time for execution in milliseconds
     * @param listener             The listener which is notified when the execution time exceeds
     *                             {@code processingTimeMillis}
     */
    @SuppressWarnings("FutureReturnValueIgnored")
    public void startTimer(@NonNull final String workSpecId,
            long processingTimeMillis,
            @NonNull TimeLimitExceededListener listener) {

        synchronized (mLock) {
            Logger.get().debug(TAG, String.format("Starting timer for %s", workSpecId));
            // clear existing timer's first
            stopTimer(workSpecId);
            WorkTimerRunnable runnable = new WorkTimerRunnable(this, workSpecId);
            mTimerMap.put(workSpecId, runnable);
            mListeners.put(workSpecId, listener);
            mExecutorService.schedule(runnable, processingTimeMillis, TimeUnit.MILLISECONDS);
        }
    }

    /**
     * Stops tracking the execution time for a given {@link androidx.work.impl.model.WorkSpec}.
     *
     * @param workSpecId The {@link androidx.work.impl.model.WorkSpec} id
     */
    public void stopTimer(@NonNull final String workSpecId) {
        synchronized (mLock) {
            WorkTimerRunnable removed = mTimerMap.remove(workSpecId);
            if (removed != null) {
                Logger.get().debug(TAG, String.format("Stopping timer for %s", workSpecId));
                mListeners.remove(workSpecId);
            }
        }
    }

    /**
     * This method needs to be idempotent. This could be called more than once, and therefore,
     * this method should only perform cleanup when necessary.
     */
    public void onDestroy() {
        if (!mExecutorService.isShutdown()) {
            // Calling shutdown() waits for pending scheduled WorkTimerRunnable's which is not
            // something we care about. Hence call shutdownNow().
            mExecutorService.shutdownNow();
        }
    }

    @VisibleForTesting
    @NonNull
    public synchronized Map<String, WorkTimerRunnable> getTimerMap() {
        return mTimerMap;
    }

    @VisibleForTesting
    @NonNull
    public synchronized Map<String, TimeLimitExceededListener> getListeners() {
        return mListeners;
    }

    @VisibleForTesting
    @NonNull
    public ScheduledExecutorService getExecutorService() {
        return mExecutorService;
    }

    /**
     * The actual runnable scheduled on the scheduled executor.
     *
     * @hide
     */
    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
    public static class WorkTimerRunnable implements Runnable {
        static final String TAG = "WrkTimerRunnable";

        private final WorkTimer mWorkTimer;
        private final String mWorkSpecId;

        WorkTimerRunnable(@NonNull WorkTimer workTimer, @NonNull String workSpecId) {
            mWorkTimer = workTimer;
            mWorkSpecId = workSpecId;
        }

        @Override
        public void run() {
            synchronized (mWorkTimer.mLock) {
                WorkTimerRunnable removed = mWorkTimer.mTimerMap.remove(mWorkSpecId);
                if (removed != null) {
                    // notify time limit exceeded.
                    TimeLimitExceededListener listener = mWorkTimer.mListeners.remove(mWorkSpecId);
                    if (listener != null) {
                        listener.onTimeLimitExceeded(mWorkSpecId);
                    }
                } else {
                    Logger.get().debug(TAG, String.format(
                            "Timer with %s is already marked as complete.", mWorkSpecId));
                }
            }
        }
    }

    /**
     * @hide
     */
    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
    public interface TimeLimitExceededListener {
        /**
         * The time limit exceeded listener.
         *
         * @param workSpecId The {@link androidx.work.impl.model.WorkSpec} id for which time limit
         *                   has exceeded.
         */
        void onTimeLimitExceeded(@NonNull String workSpecId);
    }
}