content: /*
   * Copyright (c) 2017 The Android Open Source Project
   *
   * Licensed under the Apache License, Version 2.0 (the "License");
   * you may not use this file except in compliance with the License.
   * You may obtain a copy of the License at
   *
   *      http://www.apache.org/licenses/LICENSE-2.0
   *
   * Unless required by applicable law or agreed to in writing, software
   * distributed under the License is distributed on an "AS IS" BASIS,
   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   * See the License for the specific language governing permissions and
   * limitations under the License.
   */
  
  package android.service.notification;
  
  import android.service.notification.ZenModeConfig.ScheduleInfo;
  import android.util.ArraySet;
  import android.util.Log;
  
  import java.util.Calendar;
  import java.util.Objects;
  import java.util.TimeZone;
  
  /**
   * @hide
   */
  public class ScheduleCalendar {
      public static final String TAG = "ScheduleCalendar";
      public static final boolean DEBUG = Log.isLoggable("ConditionProviders", Log.DEBUG);
      private final ArraySet<Integer> mDays = new ArraySet<Integer>();
      private final Calendar mCalendar = Calendar.getInstance();
  
      private ScheduleInfo mSchedule;
  
      @Override
      public String toString() {
          return "ScheduleCalendar[mDays=" + mDays + ", mSchedule=" + mSchedule + "]";
      }
  
      /**
       * @return true if schedule will exit on alarm, else false
       */
      public boolean exitAtAlarm() {
          return mSchedule.exitAtAlarm;
      }
  
      /**
       * Sets schedule information
       */
      public void setSchedule(ScheduleInfo schedule) {
          if (Objects.equals(mSchedule, schedule)) return;
          mSchedule = schedule;
          updateDays();
      }
  
      /**
       * Sets next alarm of the schedule if the saved next alarm has passed or is further
       * in the future than given nextAlarm
       * @param now current time in milliseconds
       * @param nextAlarm time of next alarm in milliseconds
       */
      public void maybeSetNextAlarm(long now, long nextAlarm) {
          if (mSchedule != null && mSchedule.exitAtAlarm) {
              // alarm canceled
              if (nextAlarm == 0) {
                  mSchedule.nextAlarm = 0;
              }
              // only allow alarms in the future
              if (nextAlarm > now) {
                  if (mSchedule.nextAlarm == 0 || mSchedule.nextAlarm < now) {
                      mSchedule.nextAlarm = nextAlarm;
                  } else {
                      // store earliest alarm
                      mSchedule.nextAlarm = Math.min(mSchedule.nextAlarm, nextAlarm);
                  }
              } else if (mSchedule.nextAlarm < now) {
                  if (DEBUG) {
                      Log.d(TAG, "All alarms are in the past " + mSchedule.nextAlarm);
                  }
                  mSchedule.nextAlarm = 0;
              }
          }
      }
  
      /**
       * Set calendar time zone to tz
       * @param tz current time zone
       */
      public void setTimeZone(TimeZone tz) {
          mCalendar.setTimeZone(tz);
      }
  
      /**
       * @param now current time in milliseconds
       * @return next time this rule changes (starts or ends)
       */
      public long getNextChangeTime(long now) {
          if (mSchedule == null) return 0;
          final long nextStart = getNextTime(now, mSchedule.startHour, mSchedule.startMinute);
          final long nextEnd = getNextTime(now, mSchedule.endHour, mSchedule.endMinute);
          long nextScheduleTime = Math.min(nextStart, nextEnd);
  
          return nextScheduleTime;
      }
  
      private long getNextTime(long now, int hr, int min) {
          final long time = getTime(now, hr, min);
          return time <= now ? addDays(time, 1) : time;
      }
  
      private long getTime(long millis, int hour, int min) {
          mCalendar.setTimeInMillis(millis);
          mCalendar.set(Calendar.HOUR_OF_DAY, hour);
          mCalendar.set(Calendar.MINUTE, min);
          mCalendar.set(Calendar.SECOND, 0);
          mCalendar.set(Calendar.MILLISECOND, 0);
          return mCalendar.getTimeInMillis();
      }
  
      /**
       * @param time milliseconds since Epoch
       * @return true if time is within the schedule, else false
       */
      public boolean isInSchedule(long time) {
          if (mSchedule == null || mDays.size() == 0) return false;
          final long start = getTime(time, mSchedule.startHour, mSchedule.startMinute);
          long end = getTime(time, mSchedule.endHour, mSchedule.endMinute);
          if (end <= start) {
              end = addDays(end, 1);
          }
          return isInSchedule(-1, time, start, end) || isInSchedule(0, time, start, end);
      }
  
      /**
       * @param alarm milliseconds since Epoch
       * @param now milliseconds since Epoch
       * @return true if alarm and now is within the schedule, else false
       */
      public boolean isAlarmInSchedule(long alarm, long now) {
          if (mSchedule == null || mDays.size() == 0) return false;
          final long start = getTime(alarm, mSchedule.startHour, mSchedule.startMinute);
          long end = getTime(alarm, mSchedule.endHour, mSchedule.endMinute);
          if (end <= start) {
              end = addDays(end, 1);
          }
          return (isInSchedule(-1, alarm, start, end)
                  && isInSchedule(-1, now, start, end))
                  || (isInSchedule(0, alarm, start, end)
                  && isInSchedule(0, now, start, end));
      }
  
      /**
       * @param time milliseconds since Epoch
       * @return true if should exit at time for next alarm, else false
       */
      public boolean shouldExitForAlarm(long time) {
          if (mSchedule == null) {
              return false;
          }
          return mSchedule.exitAtAlarm
                  && mSchedule.nextAlarm != 0
                  && time >= mSchedule.nextAlarm
                  && isAlarmInSchedule(mSchedule.nextAlarm, time);
      }
  
      private boolean isInSchedule(int daysOffset, long time, long start, long end) {
          final int n = Calendar.SATURDAY;
          final int day = ((getDayOfWeek(time) - 1) + (daysOffset % n) + n) % n + 1;
          start = addDays(start, daysOffset);
          end = addDays(end, daysOffset);
          return mDays.contains(day) && time >= start && time < end;
      }
  
      private int getDayOfWeek(long time) {
          mCalendar.setTimeInMillis(time);
          return mCalendar.get(Calendar.DAY_OF_WEEK);
      }
  
      private void updateDays() {
          mDays.clear();
          if (mSchedule != null && mSchedule.days != null) {
              for (int i = 0; i < mSchedule.days.length; i++) {
                  mDays.add(mSchedule.days[i]);
              }
          }
      }
  
      private long addDays(long time, int days) {
          mCalendar.setTimeInMillis(time);
          mCalendar.add(Calendar.DATE, days);
          return mCalendar.getTimeInMillis();
      }
  }
  