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