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 }