content: /*
   * Copyright (C) 2010 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 com.android.server.content;
  
  import android.accounts.Account;
  import android.app.job.JobInfo;
  import android.content.ContentResolver;
  import android.content.ContentResolver.SyncExemption;
  import android.content.pm.PackageManager;
  import android.os.Bundle;
  import android.os.PersistableBundle;
  import android.os.SystemClock;
  import android.os.UserHandle;
  import android.util.Slog;
  
  /**
   * Value type that represents a sync operation.
   * This holds all information related to a sync operation - both one off and periodic.
   * Data stored in this is used to schedule a job with the JobScheduler.
   * {@hide}
   */
  public class SyncOperation {
      public static final String TAG = "SyncManager";
  
      /**
       * This is used in the {@link #sourcePeriodicId} field if the operation is not initiated by a failed
       * periodic sync.
       */
      public static final int NO_JOB_ID = -1;
  
      public static final int REASON_BACKGROUND_DATA_SETTINGS_CHANGED = -1;
      public static final int REASON_ACCOUNTS_UPDATED = -2;
      public static final int REASON_SERVICE_CHANGED = -3;
      public static final int REASON_PERIODIC = -4;
      /** Sync started because it has just been set to isSyncable. */
      public static final int REASON_IS_SYNCABLE = -5;
      /** Sync started because it has just been set to sync automatically. */
      public static final int REASON_SYNC_AUTO = -6;
      /** Sync started because master sync automatically has been set to true. */
      public static final int REASON_MASTER_SYNC_AUTO = -7;
      public static final int REASON_USER_START = -8;
  
      private static String[] REASON_NAMES = new String[] {
              "DataSettingsChanged",
              "AccountsUpdated",
              "ServiceChanged",
              "Periodic",
              "IsSyncable",
              "AutoSync",
              "MasterSyncAuto",
              "UserStart",
      };
  
      /** Identifying info for the target for this operation. */
      public final SyncStorageEngine.EndPoint target;
      public final int owningUid;
      public final String owningPackage;
      /** Why this sync was kicked off. {@link #REASON_NAMES} */
      public final int reason;
      /** Where this sync was initiated. */
      public final int syncSource;
      public final boolean allowParallelSyncs;
      public final Bundle extras;
      public final boolean isPeriodic;
      /** jobId of the periodic SyncOperation that initiated this one */
      public final int sourcePeriodicId;
      /** Operations are considered duplicates if keys are equal */
      public final String key;
  
      /** Poll frequency of periodic sync in milliseconds */
      public final long periodMillis;
      /** Flex time of periodic sync in milliseconds */
      public final long flexMillis;
      /** Descriptive string key for this operation */
      public String wakeLockName;
      /**
       * Used when duplicate pending syncs are present. The one with the lowest expectedRuntime
       * is kept, others are discarded.
       */
      public long expectedRuntime;
  
      /** Stores the number of times this sync operation failed and had to be retried. */
      int retries;
  
      /** jobId of the JobScheduler job corresponding to this sync */
      public int jobId;
  
      @SyncExemption
      public int syncExemptionFlag;
  
      public SyncOperation(Account account, int userId, int owningUid, String owningPackage,
                           int reason, int source, String provider, Bundle extras,
                           boolean allowParallelSyncs, @SyncExemption int syncExemptionFlag) {
          this(new SyncStorageEngine.EndPoint(account, provider, userId), owningUid, owningPackage,
                  reason, source, extras, allowParallelSyncs, syncExemptionFlag);
      }
  
      private SyncOperation(SyncStorageEngine.EndPoint info, int owningUid, String owningPackage,
              int reason, int source, Bundle extras, boolean allowParallelSyncs,
              @SyncExemption int syncExemptionFlag) {
          this(info, owningUid, owningPackage, reason, source, extras, allowParallelSyncs, false,
                  NO_JOB_ID, 0, 0, syncExemptionFlag);
      }
  
      public SyncOperation(SyncOperation op, long periodMillis, long flexMillis) {
          this(op.target, op.owningUid, op.owningPackage, op.reason, op.syncSource,
                  new Bundle(op.extras), op.allowParallelSyncs, op.isPeriodic, op.sourcePeriodicId,
                  periodMillis, flexMillis, ContentResolver.SYNC_EXEMPTION_NONE);
      }
  
      public SyncOperation(SyncStorageEngine.EndPoint info, int owningUid, String owningPackage,
                           int reason, int source, Bundle extras, boolean allowParallelSyncs,
                           boolean isPeriodic, int sourcePeriodicId, long periodMillis,
                           long flexMillis, @SyncExemption int syncExemptionFlag) {
          this.target = info;
          this.owningUid = owningUid;
          this.owningPackage = owningPackage;
          this.reason = reason;
          this.syncSource = source;
          this.extras = new Bundle(extras);
          this.allowParallelSyncs = allowParallelSyncs;
          this.isPeriodic = isPeriodic;
          this.sourcePeriodicId = sourcePeriodicId;
          this.periodMillis = periodMillis;
          this.flexMillis = flexMillis;
          this.jobId = NO_JOB_ID;
          this.key = toKey();
          this.syncExemptionFlag = syncExemptionFlag;
      }
  
      /* Get a one off sync operation instance from a periodic sync. */
      public SyncOperation createOneTimeSyncOperation() {
          if (!isPeriodic) {
              return null;
          }
          SyncOperation op = new SyncOperation(target, owningUid, owningPackage, reason, syncSource,
                  new Bundle(extras), allowParallelSyncs, false, jobId /* sourcePeriodicId */,
                  periodMillis, flexMillis, ContentResolver.SYNC_EXEMPTION_NONE);
          return op;
      }
  
      public SyncOperation(SyncOperation other) {
          target = other.target;
          owningUid = other.owningUid;
          owningPackage = other.owningPackage;
          reason = other.reason;
          syncSource = other.syncSource;
          allowParallelSyncs = other.allowParallelSyncs;
          extras = new Bundle(other.extras);
          wakeLockName = other.wakeLockName();
          isPeriodic = other.isPeriodic;
          sourcePeriodicId = other.sourcePeriodicId;
          periodMillis = other.periodMillis;
          flexMillis = other.flexMillis;
          this.key = other.key;
          syncExemptionFlag = other.syncExemptionFlag;
      }
  
      /**
       * All fields are stored in a corresponding key in the persistable bundle.
       *
       * {@link #extras} is a Bundle and can contain parcelable objects. But only the type Account
       * is allowed {@link ContentResolver#validateSyncExtrasBundle(Bundle)} that can't be stored in
       * a PersistableBundle. For every value of type Account with key 'key', we store a
       * PersistableBundle containing account information at key 'ACCOUNT:key'. The Account object
       * can be reconstructed using this.
       *
       * We put a flag with key 'SyncManagerJob', to identify while reconstructing a sync operation
       * from a bundle whether the bundle actually contains information about a sync.
       * @return A persistable bundle containing all information to re-construct the sync operation.
       */
      PersistableBundle toJobInfoExtras() {
          // This will be passed as extras bundle to a JobScheduler job.
          PersistableBundle jobInfoExtras = new PersistableBundle();
  
          PersistableBundle syncExtrasBundle = new PersistableBundle();
          for (String key: extras.keySet()) {
              Object value = extras.get(key);
              if (value instanceof Account) {
                  Account account = (Account) value;
                  PersistableBundle accountBundle = new PersistableBundle();
                  accountBundle.putString("accountName", account.name);
                  accountBundle.putString("accountType", account.type);
                  // This is stored in jobInfoExtras so that we don't override a user specified
                  // sync extra with the same key.
                  jobInfoExtras.putPersistableBundle("ACCOUNT:" + key, accountBundle);
              } else if (value instanceof Long) {
                  syncExtrasBundle.putLong(key, (Long) value);
              } else if (value instanceof Integer) {
                  syncExtrasBundle.putInt(key, (Integer) value);
              } else if (value instanceof Boolean) {
                  syncExtrasBundle.putBoolean(key, (Boolean) value);
              } else if (value instanceof Float) {
                  syncExtrasBundle.putDouble(key, (double) (float) value);
              } else if (value instanceof Double) {
                  syncExtrasBundle.putDouble(key, (Double) value);
              } else if (value instanceof String) {
                  syncExtrasBundle.putString(key, (String) value);
              } else if (value == null) {
                  syncExtrasBundle.putString(key, null);
              } else {
                  Slog.e(TAG, "Unknown extra type.");
              }
          }
          jobInfoExtras.putPersistableBundle("syncExtras", syncExtrasBundle);
  
          jobInfoExtras.putBoolean("SyncManagerJob", true);
  
          jobInfoExtras.putString("provider", target.provider);
          jobInfoExtras.putString("accountName", target.account.name);
          jobInfoExtras.putString("accountType", target.account.type);
          jobInfoExtras.putInt("userId", target.userId);
          jobInfoExtras.putInt("owningUid", owningUid);
          jobInfoExtras.putString("owningPackage", owningPackage);
          jobInfoExtras.putInt("reason", reason);
          jobInfoExtras.putInt("source", syncSource);
          jobInfoExtras.putBoolean("allowParallelSyncs", allowParallelSyncs);
          jobInfoExtras.putInt("jobId", jobId);
          jobInfoExtras.putBoolean("isPeriodic", isPeriodic);
          jobInfoExtras.putInt("sourcePeriodicId", sourcePeriodicId);
          jobInfoExtras.putLong("periodMillis", periodMillis);
          jobInfoExtras.putLong("flexMillis", flexMillis);
          jobInfoExtras.putLong("expectedRuntime", expectedRuntime);
          jobInfoExtras.putInt("retries", retries);
          jobInfoExtras.putInt("syncExemptionFlag", syncExemptionFlag);
          return jobInfoExtras;
      }
  
      /**
       * Reconstructs a sync operation from an extras Bundle. Returns null if the bundle doesn't
       * contain a valid sync operation.
       */
      static SyncOperation maybeCreateFromJobExtras(PersistableBundle jobExtras) {
          if (jobExtras == null) {
              return null;
          }
          String accountName, accountType;
          String provider;
          int userId, owningUid;
          String owningPackage;
          int reason, source;
          int initiatedBy;
          Bundle extras;
          boolean allowParallelSyncs, isPeriodic;
          long periodMillis, flexMillis;
          int syncExemptionFlag;
  
          if (!jobExtras.getBoolean("SyncManagerJob", false)) {
              return null;
          }
  
          accountName = jobExtras.getString("accountName");
          accountType = jobExtras.getString("accountType");
          provider = jobExtras.getString("provider");
          userId = jobExtras.getInt("userId", Integer.MAX_VALUE);
          owningUid = jobExtras.getInt("owningUid");
          owningPackage = jobExtras.getString("owningPackage");
          reason = jobExtras.getInt("reason", Integer.MAX_VALUE);
          source = jobExtras.getInt("source", Integer.MAX_VALUE);
          allowParallelSyncs = jobExtras.getBoolean("allowParallelSyncs", false);
          isPeriodic = jobExtras.getBoolean("isPeriodic", false);
          initiatedBy = jobExtras.getInt("sourcePeriodicId", NO_JOB_ID);
          periodMillis = jobExtras.getLong("periodMillis");
          flexMillis = jobExtras.getLong("flexMillis");
          syncExemptionFlag = jobExtras.getInt("syncExemptionFlag",
                  ContentResolver.SYNC_EXEMPTION_NONE);
          extras = new Bundle();
  
          PersistableBundle syncExtras = jobExtras.getPersistableBundle("syncExtras");
          if (syncExtras != null) {
              extras.putAll(syncExtras);
          }
  
          for (String key: jobExtras.keySet()) {
              if (key!= null && key.startsWith("ACCOUNT:")) {
                  String newKey = key.substring(8); // Strip off the 'ACCOUNT:' prefix.
                  PersistableBundle accountsBundle = jobExtras.getPersistableBundle(key);
                  Account account = new Account(accountsBundle.getString("accountName"),
                          accountsBundle.getString("accountType"));
                  extras.putParcelable(newKey, account);
              }
          }
  
          Account account = new Account(accountName, accountType);
          SyncStorageEngine.EndPoint target =
                  new SyncStorageEngine.EndPoint(account, provider, userId);
          SyncOperation op = new SyncOperation(target, owningUid, owningPackage, reason, source,
                  extras, allowParallelSyncs, isPeriodic, initiatedBy, periodMillis, flexMillis,
                  syncExemptionFlag);
          op.jobId = jobExtras.getInt("jobId");
          op.expectedRuntime = jobExtras.getLong("expectedRuntime");
          op.retries = jobExtras.getInt("retries");
          return op;
      }
  
      /**
       * Determine whether if this sync operation is running, the provided operation would conflict
       * with it.
       * Parallel syncs allow multiple accounts to be synced at the same time.
       */
      boolean isConflict(SyncOperation toRun) {
          final SyncStorageEngine.EndPoint other = toRun.target;
          return target.account.type.equals(other.account.type)
                  && target.provider.equals(other.provider)
                  && target.userId == other.userId
                  && (!allowParallelSyncs
                  || target.account.name.equals(other.account.name));
      }
  
      boolean isReasonPeriodic() {
          return reason == REASON_PERIODIC;
      }
  
      boolean matchesPeriodicOperation(SyncOperation other) {
          return target.matchesSpec(other.target)
                  && SyncManager.syncExtrasEquals(extras, other.extras, true)
                  && periodMillis == other.periodMillis && flexMillis == other.flexMillis;
      }
  
      boolean isDerivedFromFailedPeriodicSync() {
          return sourcePeriodicId != NO_JOB_ID;
      }
  
      int findPriority() {
          if (isInitialization()) {
              return JobInfo.PRIORITY_SYNC_INITIALIZATION;
          } else if (isExpedited()) {
              return JobInfo.PRIORITY_SYNC_EXPEDITED;
          }
          return JobInfo.PRIORITY_DEFAULT;
      }
  
      private String toKey() {
          StringBuilder sb = new StringBuilder();
          sb.append("provider: ").append(target.provider);
          sb.append(" account {name=" + target.account.name
                  + ", user="
                  + target.userId
                  + ", type="
                  + target.account.type
                  + "}");
          sb.append(" isPeriodic: ").append(isPeriodic);
          sb.append(" period: ").append(periodMillis);
          sb.append(" flex: ").append(flexMillis);
          sb.append(" extras: ");
          extrasToStringBuilder(extras, sb);
          return sb.toString();
      }
  
      @Override
      public String toString() {
          return dump(null, true, null, false);
      }
  
      public String toSafeString() {
          return dump(null, true, null, true);
      }
  
      String dump(PackageManager pm, boolean shorter, SyncAdapterStateFetcher appStates,
              boolean logSafe) {
          StringBuilder sb = new StringBuilder();
          sb.append("JobId=").append(jobId)
                  .append(" ")
                  .append(logSafe ? "***" : target.account.name)
                  .append("/")
                  .append(target.account.type)
                  .append(" u")
                  .append(target.userId)
                  .append(" [")
                  .append(target.provider)
                  .append("] ");
          sb.append(SyncStorageEngine.SOURCES[syncSource]);
          if (expectedRuntime != 0) {
              sb.append(" ExpectedIn=");
              SyncManager.formatDurationHMS(sb,
                      (expectedRuntime - SystemClock.elapsedRealtime()));
          }
          if (extras.getBoolean(ContentResolver.SYNC_EXTRAS_EXPEDITED, false)) {
              sb.append(" EXPEDITED");
          }
          switch (syncExemptionFlag) {
              case ContentResolver.SYNC_EXEMPTION_NONE:
                  break;
              case ContentResolver.SYNC_EXEMPTION_PROMOTE_BUCKET:
                  sb.append(" STANDBY-EXEMPTED");
                  break;
              case ContentResolver.SYNC_EXEMPTION_PROMOTE_BUCKET_WITH_TEMP:
                  sb.append(" STANDBY-EXEMPTED(TOP)");
                  break;
              default:
                  sb.append(" ExemptionFlag=" + syncExemptionFlag);
                  break;
          }
          sb.append(" Reason=");
          sb.append(reasonToString(pm, reason));
          if (isPeriodic) {
              sb.append(" (period=");
              SyncManager.formatDurationHMS(sb, periodMillis);
              sb.append(" flex=");
              SyncManager.formatDurationHMS(sb, flexMillis);
              sb.append(")");
          }
          if (retries > 0) {
              sb.append(" Retries=");
              sb.append(retries);
          }
          if (!shorter) {
              sb.append(" Owner={");
              UserHandle.formatUid(sb, owningUid);
              sb.append(" ");
              sb.append(owningPackage);
              if (appStates != null) {
                  sb.append(" [");
                  sb.append(appStates.getStandbyBucket(
                          UserHandle.getUserId(owningUid), owningPackage));
                  sb.append("]");
  
                  if (appStates.isAppActive(owningUid)) {
                      sb.append(" [ACTIVE]");
                  }
              }
              sb.append("}");
              if (!extras.keySet().isEmpty()) {
                  sb.append(" ");
                  extrasToStringBuilder(extras, sb);
              }
          }
          return sb.toString();
      }
  
      static String reasonToString(PackageManager pm, int reason) {
          if (reason >= 0) {
              if (pm != null) {
                  final String[] packages = pm.getPackagesForUid(reason);
                  if (packages != null && packages.length == 1) {
                      return packages[0];
                  }
                  final String name = pm.getNameForUid(reason);
                  if (name != null) {
                      return name;
                  }
                  return String.valueOf(reason);
              } else {
                  return String.valueOf(reason);
              }
          } else {
              final int index = -reason - 1;
              if (index >= REASON_NAMES.length) {
                  return String.valueOf(reason);
              } else {
                  return REASON_NAMES[index];
              }
          }
      }
  
      boolean isInitialization() {
          return extras.getBoolean(ContentResolver.SYNC_EXTRAS_INITIALIZE, false);
      }
  
      boolean isExpedited() {
          return extras.getBoolean(ContentResolver.SYNC_EXTRAS_EXPEDITED, false);
      }
  
      boolean ignoreBackoff() {
          return extras.getBoolean(ContentResolver.SYNC_EXTRAS_IGNORE_BACKOFF, false);
      }
  
      boolean isNotAllowedOnMetered() {
          return extras.getBoolean(ContentResolver.SYNC_EXTRAS_DISALLOW_METERED, false);
      }
  
      boolean isManual() {
          return extras.getBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, false);
      }
  
      boolean isIgnoreSettings() {
          return extras.getBoolean(ContentResolver.SYNC_EXTRAS_IGNORE_SETTINGS, false);
      }
  
      boolean isAppStandbyExempted() {
          return syncExemptionFlag != ContentResolver.SYNC_EXEMPTION_NONE;
      }
  
      static void extrasToStringBuilder(Bundle bundle, StringBuilder sb) {
          if (bundle == null) {
              sb.append("null");
              return;
          }
          sb.append("[");
          for (String key : bundle.keySet()) {
              sb.append(key).append("=").append(bundle.get(key)).append(" ");
          }
          sb.append("]");
      }
  
      static String extrasToString(Bundle bundle) {
          final StringBuilder sb = new StringBuilder();
          extrasToStringBuilder(bundle, sb);
          return sb.toString();
      }
  
      String wakeLockName() {
          if (wakeLockName != null) {
              return wakeLockName;
          }
          return (wakeLockName = target.provider
                  + "/" + target.account.type
                  + "/" + target.account.name);
      }
  
      // TODO: Test this to make sure that casting to object doesn't lose the type info for EventLog.
      public Object[] toEventLog(int event) {
          Object[] logArray = new Object[4];
          logArray[1] = event;
          logArray[2] = syncSource;
          logArray[0] = target.provider;
          logArray[3] = target.account.name.hashCode();
          return logArray;
      }
  }
  