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.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