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 }