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 }