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      */
addQuarterHourCallback(Runnable runnable)85     void addQuarterHourCallback(Runnable runnable) {
86         // Callbacks *can* occur early so pad in an extra 100ms on the quarter-hour callback
87         // to ensure the sampled wallclock time reflects the subsequent quarter-hour.
88         addPeriodicCallback(runnable, Period.QUARTER_HOUR, 100L);
89     }
90 
91     /**
92      * @param runnable to be called every hour
93      */
addHourCallback(Runnable runnable)94     void addHourCallback(Runnable runnable) {
95         // Callbacks *can* occur early so pad in an extra 100ms on the hour callback to ensure
96         // the sampled wallclock time reflects the subsequent hour.
97         addPeriodicCallback(runnable, Period.HOUR, 100L);
98     }
99 
100     /**
101      * @param runnable to be called every midnight
102      */
addMidnightCallback(Runnable runnable)103     void addMidnightCallback(Runnable runnable) {
104         // Callbacks *can* occur early so pad in an extra 100ms on the midnight callback to ensure
105         // the sampled wallclock time reflects the subsequent day.
106         addPeriodicCallback(runnable, Period.MIDNIGHT, 100L);
107     }
108 
109     /**
110      * @param runnable to be called periodically
111      */
addPeriodicCallback(Runnable runnable, Period period, long offset)112     private void addPeriodicCallback(Runnable runnable, Period period, long offset) {
113         final PeriodicRunnable periodicRunnable = new PeriodicRunnable(runnable, period, offset);
114         mPeriodicRunnables.add(periodicRunnable);
115         periodicRunnable.schedule();
116     }
117 
118     /**
119      * @param runnable to no longer be called periodically
120      */
removePeriodicCallback(Runnable runnable)121     void removePeriodicCallback(Runnable runnable) {
122         for (PeriodicRunnable periodicRunnable : mPeriodicRunnables) {
123             if (periodicRunnable.mDelegate == runnable) {
124                 periodicRunnable.unSchedule();
125                 mPeriodicRunnables.remove(periodicRunnable);
126                 return;
127             }
128         }
129     }
130 
131     /**
132      * Return the delay until the given {@code period} elapses adjusted by the given {@code offset}.
133      *
134      * @param now the current time
135      * @param period the frequency with which callbacks should be given
136      * @param offset an offset to add to the normal period; allows the callback to be made relative
137      *      to the normally scheduled period end
138      * @return the time delay from {@code now} to schedule the callback
139      */
140     @VisibleForTesting
getDelay(long now, Period period, long offset)141     static long getDelay(long now, Period period, long offset) {
142         final long periodStart = now - offset;
143 
144         switch (period) {
145             case MINUTE:
146                 final long lastMinute = periodStart - (periodStart % MINUTE_IN_MILLIS);
147                 final long nextMinute = lastMinute + MINUTE_IN_MILLIS;
148                 return nextMinute - now + offset;
149 
150             case QUARTER_HOUR:
151                 final long lastQuarterHour = periodStart - (periodStart % QUARTER_HOUR_IN_MILLIS);
152                 final long nextQuarterHour = lastQuarterHour + QUARTER_HOUR_IN_MILLIS;
153                 return nextQuarterHour - now + offset;
154 
155             case HOUR:
156                 final long lastHour = periodStart - (periodStart % HOUR_IN_MILLIS);
157                 final long nextHour = lastHour + HOUR_IN_MILLIS;
158                 return nextHour - now + offset;
159 
160             case MIDNIGHT:
161                 final Calendar nextMidnight = Calendar.getInstance();
162                 nextMidnight.setTimeInMillis(periodStart);
163                 nextMidnight.add(DATE, 1);
164                 nextMidnight.set(HOUR_OF_DAY, 0);
165                 nextMidnight.set(MINUTE, 0);
166                 nextMidnight.set(SECOND, 0);
167                 nextMidnight.set(MILLISECOND, 0);
168                 return nextMidnight.getTimeInMillis() - now + offset;
169 
170             default:
171                 throw new IllegalArgumentException("unexpected period: " + period);
172         }
173     }
174 
getHandler()175     private static Handler getHandler() {
176         enforceMainLooper();
177         if (sHandler == null) {
178             sHandler = new Handler();
179         }
180         return sHandler;
181     }
182 
183     /**
184      * Schedules the execution of the given delegate Runnable at the next callback time.
185      */
186     private static final class PeriodicRunnable implements Runnable {
187 
188         private final Runnable mDelegate;
189         private final Period mPeriod;
190         private final long mOffset;
191 
PeriodicRunnable(Runnable delegate, Period period, long offset)192         public PeriodicRunnable(Runnable delegate, Period period, long offset) {
193             mDelegate = delegate;
194             mPeriod = period;
195             mOffset = offset;
196         }
197 
198         @Override
run()199         public void run() {
200             LOGGER.i("Executing periodic callback for %s because the period ended", mPeriod);
201             mDelegate.run();
202             schedule();
203         }
204 
runAndReschedule()205         private void runAndReschedule() {
206             LOGGER.i("Executing periodic callback for %s because the time changed", mPeriod);
207             unSchedule();
208             mDelegate.run();
209             schedule();
210         }
211 
schedule()212         private void schedule() {
213             final long delay = getDelay(System.currentTimeMillis(), mPeriod, mOffset);
214             getHandler().postDelayed(this, delay);
215         }
216 
unSchedule()217         private void unSchedule() {
218             getHandler().removeCallbacks(this);
219         }
220     }
221 
222     /**
223      * Reschedules callbacks when the device time changes.
224      */
225     private final class TimeChangedReceiver extends BroadcastReceiver {
226         @Override
onReceive(Context context, Intent intent)227         public void onReceive(Context context, Intent intent) {
228             for (PeriodicRunnable periodicRunnable : mPeriodicRunnables) {
229                 periodicRunnable.runAndReschedule();
230             }
231         }
232     }
233 }