Home | History | Annotate | Download | only in util
      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.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 
     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
     65     public static RecurrenceRule buildNever() {
     66         return new RecurrenceRule(null, null, null);
     67     }
     68 
     69     @Deprecated
     70     @UnsupportedAppUsage
     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 
     80     private RecurrenceRule(Parcel source) {
     81         start = convertZonedDateTime(source.readString());
     82         end = convertZonedDateTime(source.readString());
     83         period = convertPeriod(source.readString());
     84     }
     85 
     86     @Override
     87     public int describeContents() {
     88         return 0;
     89     }
     90 
     91     @Override
     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 
     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 
    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
    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
    128     public int hashCode() {
    129         return Objects.hash(start, end, period);
    130     }
    131 
    132     @Override
    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 
    155     public boolean isRecurring() {
    156         return period != null;
    157     }
    158 
    159     @Deprecated
    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 
    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 
    179         public NonrecurringIterator() {
    180             hasNext = (start != null) && (end != null);
    181         }
    182 
    183         @Override
    184         public boolean hasNext() {
    185             return hasNext;
    186         }
    187 
    188         @Override
    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 
    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 
    220         private void updateCycle() {
    221             cycleStart = roundBoundaryTime(start.plus(period.multipliedBy(i)));
    222             cycleEnd = roundBoundaryTime(start.plus(period.multipliedBy(i + 1)));
    223         }
    224 
    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
    236         public boolean hasNext() {
    237             return cycleStart.toEpochSecond() >= start.toEpochSecond();
    238         }
    239 
    240         @Override
    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 
    250     public static String convertZonedDateTime(ZonedDateTime time) {
    251         return time != null ? time.toString() : null;
    252     }
    253 
    254     public static ZonedDateTime convertZonedDateTime(String time) {
    255         return time != null ? ZonedDateTime.parse(time) : null;
    256     }
    257 
    258     public static String convertPeriod(Period period) {
    259         return period != null ? period.toString() : null;
    260     }
    261 
    262     public static Period convertPeriod(String period) {
    263         return period != null ? Period.parse(period) : null;
    264     }
    265 }
    266