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