1 /*
2  * Copyright (C) 2020 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 android.os.Looper
25 import android.text.format.DateUtils
26 import androidx.annotation.VisibleForTesting
27 
28 import com.android.deskclock.LogUtils
29 import com.android.deskclock.Utils
30 
31 import java.util.concurrent.CopyOnWriteArrayList
32 import java.util.Calendar
33 
34 /**
35  * All callbacks to be delivered at requested times on the main thread if the application is in the
36  * foreground when the callback time passes.
37  */
38 internal class PeriodicCallbackModel(context: Context) {
39 
40     @VisibleForTesting
41     internal enum class Period {
42         MINUTE, QUARTER_HOUR, HOUR, MIDNIGHT
43     }
44 
45     /** Reschedules callbacks when the device time changes.  */
46     private val mTimeChangedReceiver: BroadcastReceiver = TimeChangedReceiver()
47 
48     private val mPeriodicRunnables: MutableList<PeriodicRunnable> = CopyOnWriteArrayList()
49 
50     init {
51         // Reschedules callbacks when the device time changes.
52         val timeChangedBroadcastFilter = IntentFilter()
53         timeChangedBroadcastFilter.addAction(Intent.ACTION_TIME_CHANGED)
54         timeChangedBroadcastFilter.addAction(Intent.ACTION_DATE_CHANGED)
55         timeChangedBroadcastFilter.addAction(Intent.ACTION_TIMEZONE_CHANGED)
56         context.registerReceiver(mTimeChangedReceiver, timeChangedBroadcastFilter)
57     }
58 
59     /**
60      * @param runnable to be called every minute
61      * @param offset an offset applied to the minute to control when the callback occurs
62      */
addMinuteCallbacknull63     fun addMinuteCallback(runnable: Runnable, offset: Long) {
64         addPeriodicCallback(runnable, Period.MINUTE, offset)
65     }
66 
67     /**
68      * @param runnable to be called every quarter-hour
69      */
addQuarterHourCallbacknull70     fun addQuarterHourCallback(runnable: Runnable) {
71         // Callbacks *can* occur early so pad in an extra 100ms on the quarter-hour callback
72         // to ensure the sampled wallclock time reflects the subsequent quarter-hour.
73         addPeriodicCallback(runnable, Period.QUARTER_HOUR, 100L)
74     }
75 
76     /**
77      * @param runnable to be called every hour
78      */
addHourCallbacknull79     fun addHourCallback(runnable: Runnable) {
80         // Callbacks *can* occur early so pad in an extra 100ms on the hour callback to ensure
81         // the sampled wallclock time reflects the subsequent hour.
82         addPeriodicCallback(runnable, Period.HOUR, 100L)
83     }
84 
85     /**
86      * @param runnable to be called every midnight
87      */
addMidnightCallbacknull88     fun addMidnightCallback(runnable: Runnable) {
89         // Callbacks *can* occur early so pad in an extra 100ms on the midnight callback to ensure
90         // the sampled wallclock time reflects the subsequent day.
91         addPeriodicCallback(runnable, Period.MIDNIGHT, 100L)
92     }
93 
94     /**
95      * @param runnable to be called periodically
96      */
addPeriodicCallbacknull97     private fun addPeriodicCallback(runnable: Runnable, period: Period, offset: Long) {
98         val periodicRunnable = PeriodicRunnable(runnable, period, offset)
99         mPeriodicRunnables.add(periodicRunnable)
100         periodicRunnable.schedule()
101     }
102 
103     /**
104      * @param runnable to no longer be called periodically
105      */
removePeriodicCallbacknull106     fun removePeriodicCallback(runnable: Runnable) {
107         for (periodicRunnable in mPeriodicRunnables) {
108             if (periodicRunnable.mDelegate === runnable) {
109                 periodicRunnable.unSchedule()
110                 mPeriodicRunnables.remove(periodicRunnable)
111                 return
112             }
113         }
114     }
115 
116     /**
117      * Schedules the execution of the given delegate Runnable at the next callback time.
118      */
119     private class PeriodicRunnable(
120         val mDelegate: Runnable,
121         private val mPeriod: Period,
122         private val mOffset: Long
123     ) : Runnable {
runnull124         override fun run() {
125             LOGGER.i("Executing periodic callback for %s because the period ended", mPeriod)
126             mDelegate.run()
127             schedule()
128         }
129 
runAndReschedulenull130         fun runAndReschedule() {
131             LOGGER.i("Executing periodic callback for %s because the time changed", mPeriod)
132             unSchedule()
133             mDelegate.run()
134             schedule()
135         }
136 
schedulenull137         fun schedule() {
138             val delay = getDelay(System.currentTimeMillis(), mPeriod, mOffset)
139             handler.postDelayed(this, delay)
140         }
141 
unSchedulenull142         fun unSchedule() {
143             handler.removeCallbacks(this)
144         }
145     }
146 
147     /**
148      * Reschedules callbacks when the device time changes.
149      */
150     private inner class TimeChangedReceiver : BroadcastReceiver() {
onReceivenull151         override fun onReceive(context: Context, intent: Intent) {
152             for (periodicRunnable in mPeriodicRunnables) {
153                 periodicRunnable.runAndReschedule()
154             }
155         }
156     }
157 
158     companion object {
159         private val LOGGER = LogUtils.Logger("Periodic")
160 
161         private const val QUARTER_HOUR_IN_MILLIS = 15 * DateUtils.MINUTE_IN_MILLIS
162 
163         private var sHandler: Handler? = null
164 
165         /**
166          * Return the delay until the given `period` elapses adjusted by the given `offset`.
167          *
168          * @param now the current time
169          * @param period the frequency with which callbacks should be given
170          * @param offset an offset to add to the normal period; allows the callback to
171          * be made relative to the normally scheduled period end
172          * @return the time delay from `now` to schedule the callback
173          */
174         @VisibleForTesting
175         @JvmStatic
getDelaynull176         fun getDelay(now: Long, period: Period, offset: Long): Long {
177             val periodStart = now - offset
178 
179             return when (period) {
180                 Period.MINUTE -> {
181                     val lastMinute = periodStart - periodStart % DateUtils.MINUTE_IN_MILLIS
182                     val nextMinute = lastMinute + DateUtils.MINUTE_IN_MILLIS
183                     nextMinute - now + offset
184                 }
185                 Period.QUARTER_HOUR -> {
186                     val lastQuarterHour = periodStart - periodStart % QUARTER_HOUR_IN_MILLIS
187                     val nextQuarterHour = lastQuarterHour + QUARTER_HOUR_IN_MILLIS
188                     nextQuarterHour - now + offset
189                 }
190                 Period.HOUR -> {
191                     val lastHour = periodStart - periodStart % DateUtils.HOUR_IN_MILLIS
192                     val nextHour = lastHour + DateUtils.HOUR_IN_MILLIS
193                     nextHour - now + offset
194                 }
195                 Period.MIDNIGHT -> {
196                     val nextMidnight = Calendar.getInstance()
197                     nextMidnight.timeInMillis = periodStart
198                     nextMidnight.add(Calendar.DATE, 1)
199                     nextMidnight[Calendar.HOUR_OF_DAY] = 0
200                     nextMidnight[Calendar.MINUTE] = 0
201                     nextMidnight[Calendar.SECOND] = 0
202                     nextMidnight[Calendar.MILLISECOND] = 0
203                     nextMidnight.timeInMillis - now + offset
204                 }
205             }
206         }
207 
208         private val handler: Handler
209             get() {
210                 Utils.enforceMainLooper()
211                 if (sHandler == null) {
212                     sHandler = Handler(Looper.myLooper()!!)
213                 }
214                 return sHandler!!
215             }
216     }
217 }