• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2016 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 com.android.deskclock.uidata;
18 
19 import android.content.BroadcastReceiver;
20 import android.content.Context;
21 import android.content.Intent;
22 import android.content.IntentFilter;
23 import android.os.Handler;
24 import androidx.annotation.VisibleForTesting;
25 
26 import com.android.deskclock.LogUtils;
27 
28 import java.util.Calendar;
29 import java.util.List;
30 import java.util.concurrent.CopyOnWriteArrayList;
31 
32 import static android.content.Intent.ACTION_DATE_CHANGED;
33 import static android.content.Intent.ACTION_TIMEZONE_CHANGED;
34 import static android.content.Intent.ACTION_TIME_CHANGED;
35 import static android.text.format.DateUtils.HOUR_IN_MILLIS;
36 import static android.text.format.DateUtils.MINUTE_IN_MILLIS;
37 import static com.android.deskclock.Utils.enforceMainLooper;
38 import static java.util.Calendar.DATE;
39 import static java.util.Calendar.HOUR_OF_DAY;
40 import static java.util.Calendar.MILLISECOND;
41 import static java.util.Calendar.MINUTE;
42 import static java.util.Calendar.SECOND;
43 
44 /**
45  * All callbacks to be delivered at requested times on the main thread if the application is in the
46  * foreground when the callback time passes.
47  */
48 final class PeriodicCallbackModel {
49 
50     private static final LogUtils.Logger LOGGER = new LogUtils.Logger("Periodic");
51 
52     @VisibleForTesting
53     enum Period {MINUTE, QUARTER_HOUR, HOUR, MIDNIGHT}
54 
55     private static final long QUARTER_HOUR_IN_MILLIS = 15 * MINUTE_IN_MILLIS;
56 
57     private static Handler sHandler;
58 
59     /** Reschedules callbacks when the device time changes. */
60     @SuppressWarnings("FieldCanBeLocal")
61     private final BroadcastReceiver mTimeChangedReceiver = new TimeChangedReceiver();
62 
63     private final List<PeriodicRunnable> mPeriodicRunnables = new CopyOnWriteArrayList<>();
64 
PeriodicCallbackModel(Context context)65     PeriodicCallbackModel(Context context) {
66         // Reschedules callbacks when the device time changes.
67         final IntentFilter timeChangedBroadcastFilter = new IntentFilter();
68         timeChangedBroadcastFilter.addAction(ACTION_TIME_CHANGED);
69         timeChangedBroadcastFilter.addAction(ACTION_DATE_CHANGED);
70         timeChangedBroadcastFilter.addAction(ACTION_TIMEZONE_CHANGED);
71         context.registerReceiver(mTimeChangedReceiver, timeChangedBroadcastFilter);
72     }
73 
74     /**
75      * @param runnable to be called every minute
76      * @param offset an offset applied to the minute to control when the callback occurs
77      */
addMinuteCallback(Runnable runnable, long offset)78     void addMinuteCallback(Runnable runnable, long offset) {
79         addPeriodicCallback(runnable, Period.MINUTE, offset);
80     }
81 
82     /**
83      * @param runnable to be called every quarter-hour
84      * @param offset an offset applied to the quarter-hour to control when the callback occurs
85      */
addQuarterHourCallback(Runnable runnable, long offset)86     void addQuarterHourCallback(Runnable runnable, long offset) {
87         addPeriodicCallback(runnable, Period.QUARTER_HOUR, offset);
88     }
89 
90     /**
91      * @param runnable to be called every hour
92      * @param offset an offset applied to the hour to control when the callback occurs
93      */
addHourCallback(Runnable runnable, long offset)94     void addHourCallback(Runnable runnable, long offset) {
95         addPeriodicCallback(runnable, Period.HOUR, offset);
96     }
97 
98     /**
99      * @param runnable to be called every midnight
100      * @param offset an offset applied to the midnight to control when the callback occurs
101      */
addMidnightCallback(Runnable runnable, long offset)102     void addMidnightCallback(Runnable runnable, long offset) {
103         addPeriodicCallback(runnable, Period.MIDNIGHT, offset);
104     }
105 
106     /**
107      * @param runnable to be called periodically
108      */
addPeriodicCallback(Runnable runnable, Period period, long offset)109     private void addPeriodicCallback(Runnable runnable, Period period, long offset) {
110         final PeriodicRunnable periodicRunnable = new PeriodicRunnable(runnable, period, offset);
111         mPeriodicRunnables.add(periodicRunnable);
112         periodicRunnable.schedule();
113     }
114 
115     /**
116      * @param runnable to no longer be called periodically
117      */
removePeriodicCallback(Runnable runnable)118     void removePeriodicCallback(Runnable runnable) {
119         for (PeriodicRunnable periodicRunnable : mPeriodicRunnables) {
120             if (periodicRunnable.mDelegate == runnable) {
121                 periodicRunnable.unSchedule();
122                 mPeriodicRunnables.remove(periodicRunnable);
123                 return;
124             }
125         }
126     }
127 
128     /**
129      * Return the delay until the given {@code period} elapses adjusted by the given {@code offset}.
130      *
131      * @param now the current time
132      * @param period the frequency with which callbacks should be given
133      * @param offset an offset to add to the normal period; allows the callback to be made relative
134      *      to the normally scheduled period end
135      * @return the time delay from {@code now} to schedule the callback
136      */
137     @VisibleForTesting
getDelay(long now, Period period, long offset)138     static long getDelay(long now, Period period, long offset) {
139         final long periodStart = now - offset;
140 
141         switch (period) {
142             case MINUTE:
143                 final long lastMinute = periodStart - (periodStart % MINUTE_IN_MILLIS);
144                 final long nextMinute = lastMinute + MINUTE_IN_MILLIS;
145                 return nextMinute - now + offset;
146 
147             case QUARTER_HOUR:
148                 final long lastQuarterHour = periodStart - (periodStart % QUARTER_HOUR_IN_MILLIS);
149                 final long nextQuarterHour = lastQuarterHour + QUARTER_HOUR_IN_MILLIS;
150                 return nextQuarterHour - now + offset;
151 
152             case HOUR:
153                 final long lastHour = periodStart - (periodStart % HOUR_IN_MILLIS);
154                 final long nextHour = lastHour + HOUR_IN_MILLIS;
155                 return nextHour - now + offset;
156 
157             case MIDNIGHT:
158                 final Calendar nextMidnight = Calendar.getInstance();
159                 nextMidnight.setTimeInMillis(periodStart);
160                 nextMidnight.add(DATE, 1);
161                 nextMidnight.set(HOUR_OF_DAY, 0);
162                 nextMidnight.set(MINUTE, 0);
163                 nextMidnight.set(SECOND, 0);
164                 nextMidnight.set(MILLISECOND, 0);
165                 return nextMidnight.getTimeInMillis() - now + offset;
166 
167             default:
168                 throw new IllegalArgumentException("unexpected period: " + period);
169         }
170     }
171 
getHandler()172     private static Handler getHandler() {
173         enforceMainLooper();
174         if (sHandler == null) {
175             sHandler = new Handler();
176         }
177         return sHandler;
178     }
179 
180     /**
181      * Schedules the execution of the given delegate Runnable at the next callback time.
182      */
183     private static final class PeriodicRunnable implements Runnable {
184 
185         private final Runnable mDelegate;
186         private final Period mPeriod;
187         private final long mOffset;
188 
PeriodicRunnable(Runnable delegate, Period period, long offset)189         public PeriodicRunnable(Runnable delegate, Period period, long offset) {
190             mDelegate = delegate;
191             mPeriod = period;
192             mOffset = offset;
193         }
194 
195         @Override
run()196         public void run() {
197             LOGGER.i("Executing periodic callback for %s because the period ended", mPeriod);
198             mDelegate.run();
199             schedule();
200         }
201 
runAndReschedule()202         private void runAndReschedule() {
203             LOGGER.i("Executing periodic callback for %s because the time changed", mPeriod);
204             unSchedule();
205             mDelegate.run();
206             schedule();
207         }
208 
schedule()209         private void schedule() {
210             final long delay = getDelay(System.currentTimeMillis(), mPeriod, mOffset);
211             getHandler().postDelayed(this, delay);
212         }
213 
unSchedule()214         private void unSchedule() {
215             getHandler().removeCallbacks(this);
216         }
217     }
218 
219     /**
220      * Reschedules callbacks when the device time changes.
221      */
222     private final class TimeChangedReceiver extends BroadcastReceiver {
223         @Override
onReceive(Context context, Intent intent)224         public void onReceive(Context context, Intent intent) {
225             for (PeriodicRunnable periodicRunnable : mPeriodicRunnables) {
226                 periodicRunnable.runAndReschedule();
227             }
228         }
229     }
230 }