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