blob: 209a5912259becd0f389971e34cb6cc6e8580d0d [file] [log] [blame]
Jeff Sharkey17bebd22017-07-19 21:00:38 -06001/*
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
17package android.util;
18
Mathew Inwood4eb56ab2018-08-14 17:24:32 +010019import android.annotation.UnsupportedAppUsage;
Jeff Sharkey17bebd22017-07-19 21:00:38 -060020import android.os.Parcel;
21import android.os.Parcelable;
22
23import com.android.internal.annotations.VisibleForTesting;
24
25import java.io.DataInputStream;
26import java.io.DataOutputStream;
27import java.io.IOException;
28import java.net.ProtocolException;
29import java.time.Clock;
30import java.time.LocalTime;
31import java.time.Period;
32import java.time.ZoneId;
33import java.time.ZonedDateTime;
34import java.util.Iterator;
35import java.util.Objects;
36
37/**
38 * Description of an event that should recur over time at a specific interval
39 * between two anchor points in time.
40 *
41 * @hide
42 */
43public class RecurrenceRule implements Parcelable {
44 private static final String TAG = "RecurrenceRule";
Jeff Sharkey4e0d3072018-02-20 13:36:14 -070045 private static final boolean LOGD = Log.isLoggable(TAG, Log.DEBUG);
Jeff Sharkey17bebd22017-07-19 21:00:38 -060046
47 private static final int VERSION_INIT = 0;
48
49 /** {@hide} */
50 @VisibleForTesting
51 public static Clock sClock = Clock.systemDefaultZone();
52
Mathew Inwood4eb56ab2018-08-14 17:24:32 +010053 @UnsupportedAppUsage
Jeff Sharkey17bebd22017-07-19 21:00:38 -060054 public final ZonedDateTime start;
55 public final ZonedDateTime end;
56 public final Period period;
57
58 public RecurrenceRule(ZonedDateTime start, ZonedDateTime end, Period period) {
59 this.start = start;
60 this.end = end;
61 this.period = period;
62 }
63
64 @Deprecated
65 public static RecurrenceRule buildNever() {
66 return new RecurrenceRule(null, null, null);
67 }
68
69 @Deprecated
Mathew Inwood4eb56ab2018-08-14 17:24:32 +010070 @UnsupportedAppUsage
Jeff Sharkey17bebd22017-07-19 21:00:38 -060071 public static RecurrenceRule buildRecurringMonthly(int dayOfMonth, ZoneId zone) {
72 // Assume we started last January, since it has all possible days
73 final ZonedDateTime now = ZonedDateTime.now(sClock).withZoneSameInstant(zone);
74 final ZonedDateTime start = ZonedDateTime.of(
75 now.toLocalDate().minusYears(1).withMonth(1).withDayOfMonth(dayOfMonth),
76 LocalTime.MIDNIGHT, zone);
77 return new RecurrenceRule(start, null, Period.ofMonths(1));
78 }
79
80 private RecurrenceRule(Parcel source) {
81 start = convertZonedDateTime(source.readString());
82 end = convertZonedDateTime(source.readString());
83 period = convertPeriod(source.readString());
84 }
85
86 @Override
87 public int describeContents() {
88 return 0;
89 }
90
91 @Override
92 public void writeToParcel(Parcel dest, int flags) {
93 dest.writeString(convertZonedDateTime(start));
94 dest.writeString(convertZonedDateTime(end));
95 dest.writeString(convertPeriod(period));
96 }
97
98 public RecurrenceRule(DataInputStream in) throws IOException {
99 final int version = in.readInt();
100 switch (version) {
101 case VERSION_INIT:
102 start = convertZonedDateTime(BackupUtils.readString(in));
103 end = convertZonedDateTime(BackupUtils.readString(in));
104 period = convertPeriod(BackupUtils.readString(in));
Annie Meng47f5c9c2018-02-27 14:48:21 +0000105 break;
Jeff Sharkey17bebd22017-07-19 21:00:38 -0600106 default:
107 throw new ProtocolException("Unknown version " + version);
108 }
109 }
110
111 public void writeToStream(DataOutputStream out) throws IOException {
112 out.writeInt(VERSION_INIT);
113 BackupUtils.writeString(out, convertZonedDateTime(start));
114 BackupUtils.writeString(out, convertZonedDateTime(end));
115 BackupUtils.writeString(out, convertPeriod(period));
116 }
117
118 @Override
119 public String toString() {
120 return new StringBuilder("RecurrenceRule{")
121 .append("start=").append(start)
122 .append(" end=").append(end)
123 .append(" period=").append(period)
124 .append("}").toString();
125 }
126
127 @Override
128 public int hashCode() {
129 return Objects.hash(start, end, period);
130 }
131
132 @Override
133 public boolean equals(Object obj) {
134 if (obj instanceof RecurrenceRule) {
135 final RecurrenceRule other = (RecurrenceRule) obj;
136 return Objects.equals(start, other.start)
137 && Objects.equals(end, other.end)
138 && Objects.equals(period, other.period);
139 }
140 return false;
141 }
142
143 public static final Parcelable.Creator<RecurrenceRule> CREATOR = new Parcelable.Creator<RecurrenceRule>() {
144 @Override
145 public RecurrenceRule createFromParcel(Parcel source) {
146 return new RecurrenceRule(source);
147 }
148
149 @Override
150 public RecurrenceRule[] newArray(int size) {
151 return new RecurrenceRule[size];
152 }
153 };
154
Jeff Sharkey0a5570d2018-04-10 12:38:29 -0600155 public boolean isRecurring() {
156 return period != null;
157 }
158
Jeff Sharkey17bebd22017-07-19 21:00:38 -0600159 @Deprecated
160 public boolean isMonthly() {
161 return start != null
162 && period != null
163 && period.getYears() == 0
164 && period.getMonths() == 1
165 && period.getDays() == 0;
166 }
167
Jeff Sharkey0fc6d032018-03-30 16:25:11 -0600168 public Iterator<Range<ZonedDateTime>> cycleIterator() {
Jeff Sharkey17bebd22017-07-19 21:00:38 -0600169 if (period != null) {
170 return new RecurringIterator();
171 } else {
172 return new NonrecurringIterator();
173 }
174 }
175
Jeff Sharkey0fc6d032018-03-30 16:25:11 -0600176 private class NonrecurringIterator implements Iterator<Range<ZonedDateTime>> {
Jeff Sharkey17bebd22017-07-19 21:00:38 -0600177 boolean hasNext;
178
179 public NonrecurringIterator() {
180 hasNext = (start != null) && (end != null);
181 }
182
183 @Override
184 public boolean hasNext() {
185 return hasNext;
186 }
187
188 @Override
Jeff Sharkey0fc6d032018-03-30 16:25:11 -0600189 public Range<ZonedDateTime> next() {
Jeff Sharkey17bebd22017-07-19 21:00:38 -0600190 hasNext = false;
Jeff Sharkey0fc6d032018-03-30 16:25:11 -0600191 return new Range<>(start, end);
Jeff Sharkey17bebd22017-07-19 21:00:38 -0600192 }
193 }
194
Jeff Sharkey0fc6d032018-03-30 16:25:11 -0600195 private class RecurringIterator implements Iterator<Range<ZonedDateTime>> {
Jeff Sharkey17bebd22017-07-19 21:00:38 -0600196 int i;
197 ZonedDateTime cycleStart;
198 ZonedDateTime cycleEnd;
199
200 public RecurringIterator() {
201 final ZonedDateTime anchor = (end != null) ? end
202 : ZonedDateTime.now(sClock).withZoneSameInstant(start.getZone());
Jeff Sharkey4e0d3072018-02-20 13:36:14 -0700203 if (LOGD) Log.d(TAG, "Resolving using anchor " + anchor);
Jeff Sharkey17bebd22017-07-19 21:00:38 -0600204
205 updateCycle();
206
207 // Walk forwards until we find first cycle after now
208 while (anchor.toEpochSecond() > cycleEnd.toEpochSecond()) {
209 i++;
210 updateCycle();
211 }
212
213 // Walk backwards until we find first cycle before now
214 while (anchor.toEpochSecond() <= cycleStart.toEpochSecond()) {
215 i--;
216 updateCycle();
217 }
218 }
219
220 private void updateCycle() {
221 cycleStart = roundBoundaryTime(start.plus(period.multipliedBy(i)));
222 cycleEnd = roundBoundaryTime(start.plus(period.multipliedBy(i + 1)));
223 }
224
225 private ZonedDateTime roundBoundaryTime(ZonedDateTime boundary) {
226 if (isMonthly() && (boundary.getDayOfMonth() < start.getDayOfMonth())) {
227 // When forced to end a monthly cycle early, we want to count
228 // that entire day against the boundary.
229 return ZonedDateTime.of(boundary.toLocalDate(), LocalTime.MAX, start.getZone());
230 } else {
231 return boundary;
232 }
233 }
234
235 @Override
236 public boolean hasNext() {
237 return cycleStart.toEpochSecond() >= start.toEpochSecond();
238 }
239
240 @Override
Jeff Sharkey0fc6d032018-03-30 16:25:11 -0600241 public Range<ZonedDateTime> next() {
Jeff Sharkey4e0d3072018-02-20 13:36:14 -0700242 if (LOGD) Log.d(TAG, "Cycle " + i + " from " + cycleStart + " to " + cycleEnd);
Jeff Sharkey0fc6d032018-03-30 16:25:11 -0600243 Range<ZonedDateTime> r = new Range<>(cycleStart, cycleEnd);
Jeff Sharkey17bebd22017-07-19 21:00:38 -0600244 i--;
245 updateCycle();
Jeff Sharkey0fc6d032018-03-30 16:25:11 -0600246 return r;
Jeff Sharkey17bebd22017-07-19 21:00:38 -0600247 }
248 }
249
250 public static String convertZonedDateTime(ZonedDateTime time) {
251 return time != null ? time.toString() : null;
252 }
253
254 public static ZonedDateTime convertZonedDateTime(String time) {
255 return time != null ? ZonedDateTime.parse(time) : null;
256 }
257
258 public static String convertPeriod(Period period) {
259 return period != null ? period.toString() : null;
260 }
261
262 public static Period convertPeriod(String period) {
263 return period != null ? Period.parse(period) : null;
264 }
265}