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 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 63 public static RecurrenceRule buildNever() { 64 return new RecurrenceRule(null, null, null); 65 } 66 67 @Deprecated 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 77 private RecurrenceRule(Parcel source) { 78 start = convertZonedDateTime(source.readString()); 79 end = convertZonedDateTime(source.readString()); 80 period = convertPeriod(source.readString()); 81 } 82 83 @Override 84 public int describeContents() { 85 return 0; 86 } 87 88 @Override 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 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 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 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 125 public int hashCode() { 126 return Objects.hash(start, end, period); 127 } 128 129 @Override 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 152 public boolean isRecurring() { 153 return period != null; 154 } 155 156 @Deprecated 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 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 176 public NonrecurringIterator() { 177 hasNext = (start != null) && (end != null); 178 } 179 180 @Override 181 public boolean hasNext() { 182 return hasNext; 183 } 184 185 @Override 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 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 217 private void updateCycle() { 218 cycleStart = roundBoundaryTime(start.plus(period.multipliedBy(i))); 219 cycleEnd = roundBoundaryTime(start.plus(period.multipliedBy(i + 1))); 220 } 221 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 233 public boolean hasNext() { 234 return cycleStart.toEpochSecond() >= start.toEpochSecond(); 235 } 236 237 @Override 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 247 public static String convertZonedDateTime(ZonedDateTime time) { 248 return time != null ? time.toString() : null; 249 } 250 251 public static ZonedDateTime convertZonedDateTime(String time) { 252 return time != null ? ZonedDateTime.parse(time) : null; 253 } 254 255 public static String convertPeriod(Period period) { 256 return period != null ? period.toString() : null; 257 } 258 259 public static Period convertPeriod(String period) { 260 return period != null ? Period.parse(period) : null; 261 } 262 } 263