1 /*
2  * Copyright 2019 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package androidx.work.impl.utils;
18 
19 import androidx.annotation.RestrictTo;
20 import androidx.annotation.VisibleForTesting;
21 import androidx.work.Logger;
22 import androidx.work.RunnableScheduler;
23 import androidx.work.WorkRequest;
24 import androidx.work.impl.model.WorkGenerationalId;
25 
26 import org.jspecify.annotations.NonNull;
27 
28 import java.util.HashMap;
29 import java.util.Map;
30 
31 /**
32  * Manages timers to enforce a time limit for processing {@link WorkRequest}.
33  * Notifies a {@link TimeLimitExceededListener} when the time limit
34  * is exceeded.
35  *
36  */
37 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
38 public class WorkTimer {
39 
40     private static final String TAG = Logger.tagWithPrefix("WorkTimer");
41 
42     final RunnableScheduler mRunnableScheduler;
43     final Map<WorkGenerationalId, WorkTimerRunnable> mTimerMap;
44     final Map<WorkGenerationalId, TimeLimitExceededListener> mListeners;
45     final Object mLock;
46 
WorkTimer(@onNull RunnableScheduler scheduler)47     public WorkTimer(@NonNull RunnableScheduler scheduler) {
48         mTimerMap = new HashMap<>();
49         mListeners = new HashMap<>();
50         mLock = new Object();
51         mRunnableScheduler = scheduler;
52     }
53 
54     /**
55      * Keeps track of execution time for a given {@link androidx.work.impl.model.WorkSpec}.
56      * The {@link TimeLimitExceededListener} is notified when the execution time exceeds {@code
57      * processingTimeMillis}.
58      *
59      * @param id           The {@link androidx.work.impl.model.WorkSpec} id
60      * @param processingTimeMillis The allocated time for execution in milliseconds
61      * @param listener             The listener which is notified when the execution time exceeds
62      *                             {@code processingTimeMillis}
63      */
64     @SuppressWarnings("FutureReturnValueIgnored")
startTimer(final @NonNull WorkGenerationalId id, long processingTimeMillis, @NonNull TimeLimitExceededListener listener)65     public void startTimer(final @NonNull WorkGenerationalId id,
66             long processingTimeMillis,
67             @NonNull TimeLimitExceededListener listener) {
68 
69         synchronized (mLock) {
70             Logger.get().debug(TAG, "Starting timer for " + id);
71             // clear existing timer's first
72             stopTimer(id);
73             WorkTimerRunnable runnable = new WorkTimerRunnable(this, id);
74             mTimerMap.put(id, runnable);
75             mListeners.put(id, listener);
76             mRunnableScheduler.scheduleWithDelay(processingTimeMillis, runnable);
77         }
78     }
79 
80     /**
81      * Stops tracking the execution time for a given {@link androidx.work.impl.model.WorkSpec}.
82      *
83      * @param id The {@link androidx.work.impl.model.WorkSpec} id
84      */
stopTimer(final @NonNull WorkGenerationalId id)85     public void stopTimer(final @NonNull WorkGenerationalId id) {
86         synchronized (mLock) {
87             WorkTimerRunnable removed = mTimerMap.remove(id);
88             if (removed != null) {
89                 Logger.get().debug(TAG, "Stopping timer for " + id);
90                 mListeners.remove(id);
91             }
92         }
93     }
94 
95     @VisibleForTesting
getTimerMap()96     public @NonNull Map<WorkGenerationalId, WorkTimerRunnable> getTimerMap() {
97         synchronized (mLock) {
98             return mTimerMap;
99         }
100     }
101 
102     @VisibleForTesting
getListeners()103     public @NonNull Map<WorkGenerationalId, TimeLimitExceededListener> getListeners() {
104         synchronized (mLock) {
105             return mListeners;
106         }
107     }
108 
109     /**
110      * The actual runnable scheduled on the scheduled executor.
111      *
112      */
113     @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
114     public static class WorkTimerRunnable implements Runnable {
115         static final String TAG = "WrkTimerRunnable";
116 
117         private final WorkTimer mWorkTimer;
118         private final WorkGenerationalId mWorkGenerationalId;
119 
WorkTimerRunnable(@onNull WorkTimer workTimer, @NonNull WorkGenerationalId id)120         WorkTimerRunnable(@NonNull WorkTimer workTimer, @NonNull WorkGenerationalId id) {
121             mWorkTimer = workTimer;
122             mWorkGenerationalId = id;
123         }
124 
125         @Override
run()126         public void run() {
127             synchronized (mWorkTimer.mLock) {
128                 WorkTimerRunnable removed = mWorkTimer.mTimerMap.remove(mWorkGenerationalId);
129                 if (removed != null) {
130                     // notify time limit exceeded.
131                     TimeLimitExceededListener listener = mWorkTimer.mListeners
132                             .remove(mWorkGenerationalId);
133                     if (listener != null) {
134                         listener.onTimeLimitExceeded(mWorkGenerationalId);
135                     }
136                 } else {
137                     Logger.get().debug(TAG, String.format(
138                             "Timer with %s is already marked as complete.", mWorkGenerationalId));
139                 }
140             }
141         }
142     }
143 
144     /**
145      */
146     @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
147     public interface TimeLimitExceededListener {
148         /**
149          * The time limit exceeded listener.
150          *
151          * @param id The {@link androidx.work.impl.model.WorkSpec} id for which time limit
152          *                   has exceeded.
153          */
onTimeLimitExceeded(@onNull WorkGenerationalId id)154         void onTimeLimitExceeded(@NonNull WorkGenerationalId id);
155     }
156 }
157