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