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.compat.annotation.UnsupportedAppUsage; 20 import android.os.Parcel; 21 import android.os.Parcelable; 22 23 import com.android.internal.annotations.VisibleForTesting; 24 25 import java.io.DataInputStream; 26 import java.io.DataOutputStream; 27 import java.io.IOException; 28 import java.net.ProtocolException; 29 import java.time.Clock; 30 import java.time.LocalTime; 31 import java.time.Period; 32 import java.time.ZoneId; 33 import java.time.ZonedDateTime; 34 import java.util.Iterator; 35 import 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 */ 43 public class RecurrenceRule implements Parcelable { 44 private static final String TAG = "RecurrenceRule"; 45 private static final boolean LOGD = Log.isLoggable(TAG, Log.DEBUG); 46 47 private static final int VERSION_INIT = 0; 48 49 /** {@hide} */ 50 @VisibleForTesting 51 public static Clock sClock = Clock.systemDefaultZone(); 52 53 @UnsupportedAppUsage 54 public final ZonedDateTime start; 55 public final ZonedDateTime end; 56 public final Period period; 57 RecurrenceRule(ZonedDateTime start, ZonedDateTime end, Period period)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 buildNever()65 public static RecurrenceRule buildNever() { 66 return new RecurrenceRule(null, null, null); 67 } 68 69 @Deprecated 70 @UnsupportedAppUsage buildRecurringMonthly(int dayOfMonth, ZoneId zone)71 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 RecurrenceRule(Parcel source)80 private RecurrenceRule(Parcel source) { 81 start = convertZonedDateTime(source.readString()); 82 end = convertZonedDateTime(source.readString()); 83 period = convertPeriod(source.readString()); 84 } 85 86 @Override describeContents()87 public int describeContents() { 88 return 0; 89 } 90 91 @Override writeToParcel(Parcel dest, int flags)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 RecurrenceRule(DataInputStream in)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)); 105 break; 106 default: 107 throw new ProtocolException("Unknown version " + version); 108 } 109 } 110 writeToStream(DataOutputStream out)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 toString()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 hashCode()128 public int hashCode() { 129 return Objects.hash(start, end, period); 130 } 131 132 @Override equals(Object obj)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 @android.annotation.NonNull 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 isRecurring()155 public boolean isRecurring() { 156 return period != null; 157 } 158 159 @Deprecated isMonthly()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 cycleIterator()168 public Iterator<Range<ZonedDateTime>> cycleIterator() { 169 if (period != null) { 170 return new RecurringIterator(); 171 } else { 172 return new NonrecurringIterator(); 173 } 174 } 175 176 private class NonrecurringIterator implements Iterator<Range<ZonedDateTime>> { 177 boolean hasNext; 178 NonrecurringIterator()179 public NonrecurringIterator() { 180 hasNext = (start != null) && (end != null); 181 } 182 183 @Override hasNext()184 public boolean hasNext() { 185 return hasNext; 186 } 187 188 @Override next()189 public Range<ZonedDateTime> next() { 190 hasNext = false; 191 return new Range<>(start, end); 192 } 193 } 194 195 private class RecurringIterator implements Iterator<Range<ZonedDateTime>> { 196 int i; 197 ZonedDateTime cycleStart; 198 ZonedDateTime cycleEnd; 199 RecurringIterator()200 public RecurringIterator() { 201 final ZonedDateTime anchor = (end != null) ? end 202 : ZonedDateTime.now(sClock).withZoneSameInstant(start.getZone()); 203 if (LOGD) Log.d(TAG, "Resolving using anchor " + anchor); 204 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 updateCycle()220 private void updateCycle() { 221 cycleStart = roundBoundaryTime(start.plus(period.multipliedBy(i))); 222 cycleEnd = roundBoundaryTime(start.plus(period.multipliedBy(i + 1))); 223 } 224 roundBoundaryTime(ZonedDateTime boundary)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 hasNext()236 public boolean hasNext() { 237 return cycleStart.toEpochSecond() >= start.toEpochSecond(); 238 } 239 240 @Override next()241 public Range<ZonedDateTime> next() { 242 if (LOGD) Log.d(TAG, "Cycle " + i + " from " + cycleStart + " to " + cycleEnd); 243 Range<ZonedDateTime> r = new Range<>(cycleStart, cycleEnd); 244 i--; 245 updateCycle(); 246 return r; 247 } 248 } 249 convertZonedDateTime(ZonedDateTime time)250 public static String convertZonedDateTime(ZonedDateTime time) { 251 return time != null ? time.toString() : null; 252 } 253 convertZonedDateTime(String time)254 public static ZonedDateTime convertZonedDateTime(String time) { 255 return time != null ? ZonedDateTime.parse(time) : null; 256 } 257 convertPeriod(Period period)258 public static String convertPeriod(Period period) { 259 return period != null ? period.toString() : null; 260 } 261 convertPeriod(String period)262 public static Period convertPeriod(String period) { 263 return period != null ? Period.parse(period) : null; 264 } 265 } 266