1 /*
2  * Copyright (c) 2017 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 android.service.notification;
18 
19 import android.service.notification.ZenModeConfig.ScheduleInfo;
20 import android.util.ArraySet;
21 import android.util.Log;
22 
23 import java.util.Calendar;
24 import java.util.Objects;
25 import java.util.TimeZone;
26 
27 /**
28  * @hide
29  */
30 public class ScheduleCalendar {
31     public static final String TAG = "ScheduleCalendar";
32     public static final boolean DEBUG = Log.isLoggable("ConditionProviders", Log.DEBUG);
33     private final ArraySet<Integer> mDays = new ArraySet<Integer>();
34     private final Calendar mCalendar = Calendar.getInstance();
35 
36     private ScheduleInfo mSchedule;
37 
38     @Override
toString()39     public String toString() {
40         return "ScheduleCalendar[mDays=" + mDays + ", mSchedule=" + mSchedule + "]";
41     }
42 
43     /**
44      * @return true if schedule will exit on alarm, else false
45      */
exitAtAlarm()46     public boolean exitAtAlarm() {
47         return mSchedule.exitAtAlarm;
48     }
49 
50     /**
51      * Sets schedule information
52      */
setSchedule(ScheduleInfo schedule)53     public void setSchedule(ScheduleInfo schedule) {
54         if (Objects.equals(mSchedule, schedule)) return;
55         mSchedule = schedule;
56         updateDays();
57     }
58 
59     /**
60      * Sets next alarm of the schedule if the saved next alarm has passed or is further
61      * in the future than given nextAlarm
62      * @param now current time in milliseconds
63      * @param nextAlarm time of next alarm in milliseconds
64      */
maybeSetNextAlarm(long now, long nextAlarm)65     public void maybeSetNextAlarm(long now, long nextAlarm) {
66         if (mSchedule != null && mSchedule.exitAtAlarm) {
67             // alarm canceled
68             if (nextAlarm == 0) {
69                 mSchedule.nextAlarm = 0;
70             }
71             // only allow alarms in the future
72             if (nextAlarm > now) {
73                 if (mSchedule.nextAlarm == 0 || mSchedule.nextAlarm < now) {
74                     mSchedule.nextAlarm = nextAlarm;
75                 } else {
76                     // store earliest alarm
77                     mSchedule.nextAlarm = Math.min(mSchedule.nextAlarm, nextAlarm);
78                 }
79             } else if (mSchedule.nextAlarm < now) {
80                 if (DEBUG) {
81                     Log.d(TAG, "All alarms are in the past " + mSchedule.nextAlarm);
82                 }
83                 mSchedule.nextAlarm = 0;
84             }
85         }
86     }
87 
88     /**
89      * Set calendar time zone to tz
90      * @param tz current time zone
91      */
setTimeZone(TimeZone tz)92     public void setTimeZone(TimeZone tz) {
93         mCalendar.setTimeZone(tz);
94     }
95 
96     /**
97      * @param now current time in milliseconds
98      * @return next time this rule changes (starts or ends)
99      */
getNextChangeTime(long now)100     public long getNextChangeTime(long now) {
101         if (mSchedule == null) return 0;
102         final long nextStart = getNextTime(now, mSchedule.startHour, mSchedule.startMinute);
103         final long nextEnd = getNextTime(now, mSchedule.endHour, mSchedule.endMinute);
104         long nextScheduleTime = Math.min(nextStart, nextEnd);
105 
106         return nextScheduleTime;
107     }
108 
getNextTime(long now, int hr, int min)109     private long getNextTime(long now, int hr, int min) {
110         final long time = getTime(now, hr, min);
111         return time <= now ? addDays(time, 1) : time;
112     }
113 
getTime(long millis, int hour, int min)114     private long getTime(long millis, int hour, int min) {
115         mCalendar.setTimeInMillis(millis);
116         mCalendar.set(Calendar.HOUR_OF_DAY, hour);
117         mCalendar.set(Calendar.MINUTE, min);
118         mCalendar.set(Calendar.SECOND, 0);
119         mCalendar.set(Calendar.MILLISECOND, 0);
120         return mCalendar.getTimeInMillis();
121     }
122 
123     /**
124      * @param time milliseconds since Epoch
125      * @return true if time is within the schedule, else false
126      */
isInSchedule(long time)127     public boolean isInSchedule(long time) {
128         if (mSchedule == null || mDays.size() == 0) return false;
129         final long start = getTime(time, mSchedule.startHour, mSchedule.startMinute);
130         long end = getTime(time, mSchedule.endHour, mSchedule.endMinute);
131         if (end <= start) {
132             end = addDays(end, 1);
133         }
134         return isInSchedule(-1, time, start, end) || isInSchedule(0, time, start, end);
135     }
136 
137     /**
138      * @param alarm milliseconds since Epoch
139      * @param now milliseconds since Epoch
140      * @return true if alarm and now is within the schedule, else false
141      */
isAlarmInSchedule(long alarm, long now)142     public boolean isAlarmInSchedule(long alarm, long now) {
143         if (mSchedule == null || mDays.size() == 0) return false;
144         final long start = getTime(alarm, mSchedule.startHour, mSchedule.startMinute);
145         long end = getTime(alarm, mSchedule.endHour, mSchedule.endMinute);
146         if (end <= start) {
147             end = addDays(end, 1);
148         }
149         return (isInSchedule(-1, alarm, start, end)
150                 && isInSchedule(-1, now, start, end))
151                 || (isInSchedule(0, alarm, start, end)
152                 && isInSchedule(0, now, start, end));
153     }
154 
155     /**
156      * @param time milliseconds since Epoch
157      * @return true if should exit at time for next alarm, else false
158      */
shouldExitForAlarm(long time)159     public boolean shouldExitForAlarm(long time) {
160         if (mSchedule == null) {
161             return false;
162         }
163         return mSchedule.exitAtAlarm
164                 && mSchedule.nextAlarm != 0
165                 && time >= mSchedule.nextAlarm
166                 && isAlarmInSchedule(mSchedule.nextAlarm, time);
167     }
168 
isInSchedule(int daysOffset, long time, long start, long end)169     private boolean isInSchedule(int daysOffset, long time, long start, long end) {
170         final int n = Calendar.SATURDAY;
171         final int day = ((getDayOfWeek(time) - 1) + (daysOffset % n) + n) % n + 1;
172         start = addDays(start, daysOffset);
173         end = addDays(end, daysOffset);
174         return mDays.contains(day) && time >= start && time < end;
175     }
176 
getDayOfWeek(long time)177     private int getDayOfWeek(long time) {
178         mCalendar.setTimeInMillis(time);
179         return mCalendar.get(Calendar.DAY_OF_WEEK);
180     }
181 
updateDays()182     private void updateDays() {
183         mDays.clear();
184         if (mSchedule != null && mSchedule.days != null) {
185             for (int i = 0; i < mSchedule.days.length; i++) {
186                 mDays.add(mSchedule.days[i]);
187             }
188         }
189     }
190 
addDays(long time, int days)191     private long addDays(long time, int days) {
192         mCalendar.setTimeInMillis(time);
193         mCalendar.add(Calendar.DATE, days);
194         return mCalendar.getTimeInMillis();
195     }
196 }
197