/*
|
* 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;
|
}
|
}
|