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