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
17package com.android.deskclock.uidata;
18
19import android.content.BroadcastReceiver;
20import android.content.Context;
21import android.content.Intent;
22import android.content.IntentFilter;
23import android.os.Handler;
24import android.support.annotation.VisibleForTesting;
25
26import com.android.deskclock.LogUtils;
27
28import java.util.Calendar;
29import java.util.List;
30import java.util.concurrent.CopyOnWriteArrayList;
31
32import static android.content.Intent.ACTION_DATE_CHANGED;
33import static android.content.Intent.ACTION_TIMEZONE_CHANGED;
34import static android.content.Intent.ACTION_TIME_CHANGED;
35import static android.text.format.DateUtils.HOUR_IN_MILLIS;
36import static android.text.format.DateUtils.MINUTE_IN_MILLIS;
37import static com.android.deskclock.Utils.enforceMainLooper;
38import static java.util.Calendar.DATE;
39import static java.util.Calendar.HOUR_OF_DAY;
40import static java.util.Calendar.MILLISECOND;
41import static java.util.Calendar.MINUTE;
42import 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 */
48final 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
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     */
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     */
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     */
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     */
102    void addMidnightCallback(Runnable runnable, long offset) {
103        addPeriodicCallback(runnable, Period.MIDNIGHT, offset);
104    }
105
106    /**
107     * @param runnable to be called periodically
108     */
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     */
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
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
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
189        public PeriodicRunnable(Runnable delegate, Period period, long offset) {
190            mDelegate = delegate;
191            mPeriod = period;
192            mOffset = offset;
193        }
194
195        @Override
196        public void run() {
197            LOGGER.i("Executing periodic callback for %s because the period ended", mPeriod);
198            mDelegate.run();
199            schedule();
200        }
201
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
209        private void schedule() {
210            final long delay = getDelay(System.currentTimeMillis(), mPeriod, mOffset);
211            getHandler().postDelayed(this, delay);
212        }
213
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
224        public void onReceive(Context context, Intent intent) {
225            for (PeriodicRunnable periodicRunnable : mPeriodicRunnables) {
226                periodicRunnable.runAndReschedule();
227            }
228        }
229    }
230}