content: /*
   * Copyright (C) 2009 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.content;
  
  import android.annotation.UnsupportedAppUsage;
  import android.os.Parcel;
  import android.os.Parcelable;
  import android.util.Log;
  
  import java.util.ArrayList;
  import java.util.Calendar;
  import java.util.GregorianCalendar;
  
  /** @hide */
  public class SyncStatusInfo implements Parcelable {
      private static final String TAG = "Sync";
  
      static final int VERSION = 6;
  
      private static final int MAX_EVENT_COUNT = 10;
  
      /**
       * Number of sync sources. KEEP THIS AND SyncStorageEngine.SOURCES IN SYNC.
       */
      private static final int SOURCE_COUNT = 6;
  
      @UnsupportedAppUsage
      public final int authorityId;
  
      /**
       * # of syncs for each sync source, etc.
       */
      public static class Stats {
          public long totalElapsedTime;
          public int numSyncs;
          public int numSourcePoll;
          public int numSourceOther;
          public int numSourceLocal;
          public int numSourceUser;
          public int numSourcePeriodic;
          public int numSourceFeed;
          public int numFailures;
          public int numCancels;
  
          /** Copy all the stats to another instance. */
          public void copyTo(Stats to) {
              to.totalElapsedTime = totalElapsedTime;
              to.numSyncs = numSyncs;
              to.numSourcePoll = numSourcePoll;
              to.numSourceOther = numSourceOther;
              to.numSourceLocal = numSourceLocal;
              to.numSourceUser = numSourceUser;
              to.numSourcePeriodic = numSourcePeriodic;
              to.numSourceFeed = numSourceFeed;
              to.numFailures = numFailures;
              to.numCancels = numCancels;
          }
  
          /** Clear all the stats. */
          public void clear() {
              totalElapsedTime = 0;
              numSyncs = 0;
              numSourcePoll = 0;
              numSourceOther = 0;
              numSourceLocal = 0;
              numSourceUser = 0;
              numSourcePeriodic = 0;
              numSourceFeed = 0;
              numFailures = 0;
              numCancels = 0;
          }
  
          /** Write all the stats to a parcel. */
          public void writeToParcel(Parcel parcel) {
              parcel.writeLong(totalElapsedTime);
              parcel.writeInt(numSyncs);
              parcel.writeInt(numSourcePoll);
              parcel.writeInt(numSourceOther);
              parcel.writeInt(numSourceLocal);
              parcel.writeInt(numSourceUser);
              parcel.writeInt(numSourcePeriodic);
              parcel.writeInt(numSourceFeed);
              parcel.writeInt(numFailures);
              parcel.writeInt(numCancels);
          }
  
          /** Read all the stats from a parcel. */
          public void readFromParcel(Parcel parcel) {
              totalElapsedTime = parcel.readLong();
              numSyncs = parcel.readInt();
              numSourcePoll = parcel.readInt();
              numSourceOther = parcel.readInt();
              numSourceLocal = parcel.readInt();
              numSourceUser = parcel.readInt();
              numSourcePeriodic = parcel.readInt();
              numSourceFeed = parcel.readInt();
              numFailures = parcel.readInt();
              numCancels = parcel.readInt();
          }
      }
  
      public long lastTodayResetTime;
  
      public final Stats totalStats = new Stats();
      public final Stats todayStats = new Stats();
      public final Stats yesterdayStats = new Stats();
  
      @UnsupportedAppUsage
      public long lastSuccessTime;
      @UnsupportedAppUsage
      public int lastSuccessSource;
      @UnsupportedAppUsage
      public long lastFailureTime;
      @UnsupportedAppUsage
      public int lastFailureSource;
      @UnsupportedAppUsage
      public String lastFailureMesg;
      @UnsupportedAppUsage
      public long initialFailureTime;
      @UnsupportedAppUsage
      public boolean pending;
      @UnsupportedAppUsage
      public boolean initialize;
  
      public final long[] perSourceLastSuccessTimes = new long[SOURCE_COUNT];
      public final long[] perSourceLastFailureTimes = new long[SOURCE_COUNT];
  
    // Warning: It is up to the external caller to ensure there are
    // no race conditions when accessing this list
    @UnsupportedAppUsage
    private ArrayList<Long> periodicSyncTimes;
  
      private final ArrayList<Long> mLastEventTimes = new ArrayList<>();
      private final ArrayList<String> mLastEvents = new ArrayList<>();
  
      @UnsupportedAppUsage
      public SyncStatusInfo(int authorityId) {
          this.authorityId = authorityId;
      }
  
      @UnsupportedAppUsage
      public int getLastFailureMesgAsInt(int def) {
          final int i = ContentResolver.syncErrorStringToInt(lastFailureMesg);
          if (i > 0) {
              return i;
          } else {
              Log.d(TAG, "Unknown lastFailureMesg:" + lastFailureMesg);
              return def;
          }
      }
  
      public int describeContents() {
          return 0;
      }
  
      public void writeToParcel(Parcel parcel, int flags) {
          parcel.writeInt(VERSION);
          parcel.writeInt(authorityId);
  
          // Note we can't use Stats.writeToParcel() here; see the below constructor for the reason.
          parcel.writeLong(totalStats.totalElapsedTime);
          parcel.writeInt(totalStats.numSyncs);
          parcel.writeInt(totalStats.numSourcePoll);
          parcel.writeInt(totalStats.numSourceOther);
          parcel.writeInt(totalStats.numSourceLocal);
          parcel.writeInt(totalStats.numSourceUser);
  
          parcel.writeLong(lastSuccessTime);
          parcel.writeInt(lastSuccessSource);
          parcel.writeLong(lastFailureTime);
          parcel.writeInt(lastFailureSource);
          parcel.writeString(lastFailureMesg);
          parcel.writeLong(initialFailureTime);
          parcel.writeInt(pending ? 1 : 0);
          parcel.writeInt(initialize ? 1 : 0);
          if (periodicSyncTimes != null) {
              parcel.writeInt(periodicSyncTimes.size());
              for (long periodicSyncTime : periodicSyncTimes) {
                  parcel.writeLong(periodicSyncTime);
              }
          } else {
              parcel.writeInt(-1);
          }
          parcel.writeInt(mLastEventTimes.size());
          for (int i = 0; i < mLastEventTimes.size(); i++) {
              parcel.writeLong(mLastEventTimes.get(i));
              parcel.writeString(mLastEvents.get(i));
          }
          // Version 4
          parcel.writeInt(totalStats.numSourcePeriodic);
  
          // Version 5
          parcel.writeInt(totalStats.numSourceFeed);
          parcel.writeInt(totalStats.numFailures);
          parcel.writeInt(totalStats.numCancels);
  
          parcel.writeLong(lastTodayResetTime);
  
          todayStats.writeToParcel(parcel);
          yesterdayStats.writeToParcel(parcel);
  
          // Version 6.
          parcel.writeLongArray(perSourceLastSuccessTimes);
          parcel.writeLongArray(perSourceLastFailureTimes);
      }
  
      @UnsupportedAppUsage
      public SyncStatusInfo(Parcel parcel) {
          int version = parcel.readInt();
          if (version != VERSION && version != 1) {
              Log.w("SyncStatusInfo", "Unknown version: " + version);
          }
          authorityId = parcel.readInt();
  
          // Note we can't use Stats.writeToParcel() here because the data is persisted and we need
          // to be able to read from the old format too.
          totalStats.totalElapsedTime = parcel.readLong();
          totalStats.numSyncs = parcel.readInt();
          totalStats.numSourcePoll = parcel.readInt();
          totalStats.numSourceOther = parcel.readInt();
          totalStats.numSourceLocal = parcel.readInt();
          totalStats.numSourceUser = parcel.readInt();
          lastSuccessTime = parcel.readLong();
          lastSuccessSource = parcel.readInt();
          lastFailureTime = parcel.readLong();
          lastFailureSource = parcel.readInt();
          lastFailureMesg = parcel.readString();
          initialFailureTime = parcel.readLong();
          pending = parcel.readInt() != 0;
          initialize = parcel.readInt() != 0;
          if (version == 1) {
              periodicSyncTimes = null;
          } else {
              final int count = parcel.readInt();
              if (count < 0) {
                  periodicSyncTimes = null;
              } else {
                  periodicSyncTimes = new ArrayList<Long>();
                  for (int i = 0; i < count; i++) {
                      periodicSyncTimes.add(parcel.readLong());
                  }
              }
              if (version >= 3) {
                  mLastEventTimes.clear();
                  mLastEvents.clear();
                  final int nEvents = parcel.readInt();
                  for (int i = 0; i < nEvents; i++) {
                      mLastEventTimes.add(parcel.readLong());
                      mLastEvents.add(parcel.readString());
                  }
              }
          }
          if (version < 4) {
              // Before version 4, numSourcePeriodic wasn't persisted.
              totalStats.numSourcePeriodic =
                      totalStats.numSyncs - totalStats.numSourceLocal - totalStats.numSourcePoll
                              - totalStats.numSourceOther
                              - totalStats.numSourceUser;
              if (totalStats.numSourcePeriodic < 0) { // Sanity check.
                  totalStats.numSourcePeriodic = 0;
              }
          } else {
              totalStats.numSourcePeriodic = parcel.readInt();
          }
          if (version >= 5) {
              totalStats.numSourceFeed = parcel.readInt();
              totalStats.numFailures = parcel.readInt();
              totalStats.numCancels = parcel.readInt();
  
              lastTodayResetTime = parcel.readLong();
  
              todayStats.readFromParcel(parcel);
              yesterdayStats.readFromParcel(parcel);
          }
          if (version >= 6) {
              parcel.readLongArray(perSourceLastSuccessTimes);
              parcel.readLongArray(perSourceLastFailureTimes);
          }
      }
  
      public SyncStatusInfo(SyncStatusInfo other) {
          authorityId = other.authorityId;
  
          other.totalStats.copyTo(totalStats);
          other.todayStats.copyTo(todayStats);
          other.yesterdayStats.copyTo(yesterdayStats);
  
          lastTodayResetTime = other.lastTodayResetTime;
  
          lastSuccessTime = other.lastSuccessTime;
          lastSuccessSource = other.lastSuccessSource;
          lastFailureTime = other.lastFailureTime;
          lastFailureSource = other.lastFailureSource;
          lastFailureMesg = other.lastFailureMesg;
          initialFailureTime = other.initialFailureTime;
          pending = other.pending;
          initialize = other.initialize;
          if (other.periodicSyncTimes != null) {
              periodicSyncTimes = new ArrayList<Long>(other.periodicSyncTimes);
          }
          mLastEventTimes.addAll(other.mLastEventTimes);
          mLastEvents.addAll(other.mLastEvents);
  
          copy(perSourceLastSuccessTimes, other.perSourceLastSuccessTimes);
          copy(perSourceLastFailureTimes, other.perSourceLastFailureTimes);
      }
  
      private static void copy(long[] to, long[] from) {
          System.arraycopy(from, 0, to, 0, to.length);
      }
  
      @UnsupportedAppUsage
      public void setPeriodicSyncTime(int index, long when) {
          // The list is initialized lazily when scheduling occurs so we need to make sure
          // we initialize elements < index to zero (zero is ignore for scheduling purposes)
          ensurePeriodicSyncTimeSize(index);
          periodicSyncTimes.set(index, when);
      }
  
      @UnsupportedAppUsage
      public long getPeriodicSyncTime(int index) {
          if (periodicSyncTimes != null && index < periodicSyncTimes.size()) {
              return periodicSyncTimes.get(index);
          } else {
              return 0;
          }
      }
  
      @UnsupportedAppUsage
      public void removePeriodicSyncTime(int index) {
          if (periodicSyncTimes != null && index < periodicSyncTimes.size()) {
              periodicSyncTimes.remove(index);
          }
      }
  
      /** */
      public void addEvent(String message) {
          if (mLastEventTimes.size() >= MAX_EVENT_COUNT) {
              mLastEventTimes.remove(MAX_EVENT_COUNT - 1);
              mLastEvents.remove(MAX_EVENT_COUNT - 1);
          }
          mLastEventTimes.add(0, System.currentTimeMillis());
          mLastEvents.add(0, message);
      }
  
      /** */
      public int getEventCount() {
          return mLastEventTimes.size();
      }
  
      /** */
      public long getEventTime(int i) {
          return mLastEventTimes.get(i);
      }
  
      /** */
      public String getEvent(int i) {
          return mLastEvents.get(i);
      }
  
      /** Call this when a sync has succeeded. */
      public void setLastSuccess(int source, long lastSyncTime) {
          lastSuccessTime = lastSyncTime;
          lastSuccessSource = source;
          lastFailureTime = 0;
          lastFailureSource = -1;
          lastFailureMesg = null;
          initialFailureTime = 0;
  
          if (0 <= source && source < perSourceLastSuccessTimes.length) {
              perSourceLastSuccessTimes[source] = lastSyncTime;
          }
      }
  
      /** Call this when a sync has failed. */
      public void setLastFailure(int source, long lastSyncTime, String failureMessage) {
          lastFailureTime = lastSyncTime;
          lastFailureSource = source;
          lastFailureMesg = failureMessage;
          if (initialFailureTime == 0) {
              initialFailureTime = lastSyncTime;
          }
  
          if (0 <= source && source < perSourceLastFailureTimes.length) {
              perSourceLastFailureTimes[source] = lastSyncTime;
          }
      }
  
      @UnsupportedAppUsage
      public static final @android.annotation.NonNull Creator<SyncStatusInfo> CREATOR = new Creator<SyncStatusInfo>() {
          public SyncStatusInfo createFromParcel(Parcel in) {
              return new SyncStatusInfo(in);
          }
  
          public SyncStatusInfo[] newArray(int size) {
              return new SyncStatusInfo[size];
          }
      };
  
      @UnsupportedAppUsage
      private void ensurePeriodicSyncTimeSize(int index) {
          if (periodicSyncTimes == null) {
              periodicSyncTimes = new ArrayList<Long>(0);
          }
  
          final int requiredSize = index + 1;
          if (periodicSyncTimes.size() < requiredSize) {
              for (int i = periodicSyncTimes.size(); i < requiredSize; i++) {
                  periodicSyncTimes.add((long) 0);
              }
          }
      }
  
      /**
       * If the last reset was not today, move today's stats to yesterday's and clear today's.
       */
      public void maybeResetTodayStats(boolean clockValid, boolean force) {
          final long now = System.currentTimeMillis();
  
          if (!force) {
              // Last reset was the same day, nothing to do.
              if (areSameDates(now, lastTodayResetTime)) {
                  return;
              }
  
              // Hack -- on devices with no RTC, until the NTP kicks in, the device won't have the
              // correct time. So if the time goes back, don't reset, unless we're sure the current
              // time is correct.
              if (now < lastTodayResetTime && !clockValid) {
                  return;
              }
          }
  
          lastTodayResetTime = now;
  
          todayStats.copyTo(yesterdayStats);
          todayStats.clear();
      }
  
      private static boolean areSameDates(long time1, long time2) {
          final Calendar c1 = new GregorianCalendar();
          final Calendar c2 = new GregorianCalendar();
  
          c1.setTimeInMillis(time1);
          c2.setTimeInMillis(time2);
  
          return c1.get(Calendar.YEAR) == c2.get(Calendar.YEAR)
                  && c1.get(Calendar.DAY_OF_YEAR) == c2.get(Calendar.DAY_OF_YEAR);
      }
  }
  