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 androidx.work.impl.model.WorkGenerationalId;

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<WorkGenerationalId, WorkTimerRunnable> mTimerMap;
    final Map<WorkGenerationalId, 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 id           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 WorkGenerationalId id,
            long processingTimeMillis,
            @NonNull TimeLimitExceededListener listener) {

        synchronized (mLock) {
            Logger.get().debug(TAG, "Starting timer for " + id);
            // clear existing timer's first
            stopTimer(id);
            WorkTimerRunnable runnable = new WorkTimerRunnable(this, id);
            mTimerMap.put(id, runnable);
            mListeners.put(id, listener);
            mRunnableScheduler.scheduleWithDelay(processingTimeMillis, runnable);
        }
    }

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

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

    @VisibleForTesting
    @NonNull
    public Map<WorkGenerationalId, TimeLimitExceededListener> getListeners() {
        synchronized (mLock) {
            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 WorkGenerationalId mWorkGenerationalId;

        WorkTimerRunnable(@NonNull WorkTimer workTimer, @NonNull WorkGenerationalId id) {
            mWorkTimer = workTimer;
            mWorkGenerationalId = id;
        }

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

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