blob: 2abc2e60a47b03336c3d7d7a65df87695318ebd1 [file] [log] [blame]
/*
* 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;
}
}