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.RunnableScheduler;
import androidx.work.WorkRequest;

import java.util.HashMap;
import java.util.Map;

/**
 * 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");

    final RunnableScheduler mRunnableScheduler;
    final Map<String, WorkTimerRunnable> mTimerMap;
    final Map<String, TimeLimitExceededListener> mListeners;
    final Object mLock;

    public WorkTimer(@NonNull RunnableScheduler scheduler) {
        mTimerMap = new HashMap<>();
        mListeners = new HashMap<>();
        mLock = new Object();
        mRunnableScheduler = scheduler;
    }

    /**
     * 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, "Starting timer for " + workSpecId);
            // clear existing timer's first
            stopTimer(workSpecId);
            WorkTimerRunnable runnable = new WorkTimerRunnable(this, workSpecId);
            mTimerMap.put(workSpecId, runnable);
            mListeners.put(workSpecId, listener);
            mRunnableScheduler.scheduleWithDelay(processingTimeMillis, runnable);
        }
    }

    /**
     * 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, "Stopping timer for " + workSpecId);
                mListeners.remove(workSpecId);
            }
        }
    }

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

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

    /**
     * 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);
    }
}