1 /* 2 * Copyright (C) 2010 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.server.content; 18 19 import android.accounts.Account; 20 import android.app.job.JobInfo; 21 import android.content.ContentResolver; 22 import android.content.ContentResolver.SyncExemption; 23 import android.content.pm.PackageManager; 24 import android.os.Bundle; 25 import android.os.PersistableBundle; 26 import android.os.SystemClock; 27 import android.os.UserHandle; 28 import android.util.Slog; 29 30 /** 31 * Value type that represents a sync operation. 32 * This holds all information related to a sync operation - both one off and periodic. 33 * Data stored in this is used to schedule a job with the JobScheduler. 34 * {@hide} 35 */ 36 public class SyncOperation { 37 public static final String TAG = "SyncManager"; 38 39 /** 40 * This is used in the {@link #sourcePeriodicId} field if the operation is not initiated by a failed 41 * periodic sync. 42 */ 43 public static final int NO_JOB_ID = -1; 44 45 public static final int REASON_BACKGROUND_DATA_SETTINGS_CHANGED = -1; 46 public static final int REASON_ACCOUNTS_UPDATED = -2; 47 public static final int REASON_SERVICE_CHANGED = -3; 48 public static final int REASON_PERIODIC = -4; 49 /** Sync started because it has just been set to isSyncable. */ 50 public static final int REASON_IS_SYNCABLE = -5; 51 /** Sync started because it has just been set to sync automatically. */ 52 public static final int REASON_SYNC_AUTO = -6; 53 /** Sync started because master sync automatically has been set to true. */ 54 public static final int REASON_MASTER_SYNC_AUTO = -7; 55 public static final int REASON_USER_START = -8; 56 57 private static String[] REASON_NAMES = new String[] { 58 "DataSettingsChanged", 59 "AccountsUpdated", 60 "ServiceChanged", 61 "Periodic", 62 "IsSyncable", 63 "AutoSync", 64 "MasterSyncAuto", 65 "UserStart", 66 }; 67 68 /** Identifying info for the target for this operation. */ 69 public final SyncStorageEngine.EndPoint target; 70 public final int owningUid; 71 public final String owningPackage; 72 /** Why this sync was kicked off. {@link #REASON_NAMES} */ 73 public final int reason; 74 /** Where this sync was initiated. */ 75 public final int syncSource; 76 public final boolean allowParallelSyncs; 77 public final Bundle extras; 78 public final boolean isPeriodic; 79 /** jobId of the periodic SyncOperation that initiated this one */ 80 public final int sourcePeriodicId; 81 /** Operations are considered duplicates if keys are equal */ 82 public final String key; 83 84 /** Poll frequency of periodic sync in milliseconds */ 85 public final long periodMillis; 86 /** Flex time of periodic sync in milliseconds */ 87 public final long flexMillis; 88 /** Descriptive string key for this operation */ 89 public String wakeLockName; 90 /** 91 * Used when duplicate pending syncs are present. The one with the lowest expectedRuntime 92 * is kept, others are discarded. 93 */ 94 public long expectedRuntime; 95 96 /** Stores the number of times this sync operation failed and had to be retried. */ 97 int retries; 98 99 /** jobId of the JobScheduler job corresponding to this sync */ 100 public int jobId; 101 102 @SyncExemption 103 public int syncExemptionFlag; 104 SyncOperation(Account account, int userId, int owningUid, String owningPackage, int reason, int source, String provider, Bundle extras, boolean allowParallelSyncs, @SyncExemption int syncExemptionFlag)105 public SyncOperation(Account account, int userId, int owningUid, String owningPackage, 106 int reason, int source, String provider, Bundle extras, 107 boolean allowParallelSyncs, @SyncExemption int syncExemptionFlag) { 108 this(new SyncStorageEngine.EndPoint(account, provider, userId), owningUid, owningPackage, 109 reason, source, extras, allowParallelSyncs, syncExemptionFlag); 110 } 111 SyncOperation(SyncStorageEngine.EndPoint info, int owningUid, String owningPackage, int reason, int source, Bundle extras, boolean allowParallelSyncs, @SyncExemption int syncExemptionFlag)112 private SyncOperation(SyncStorageEngine.EndPoint info, int owningUid, String owningPackage, 113 int reason, int source, Bundle extras, boolean allowParallelSyncs, 114 @SyncExemption int syncExemptionFlag) { 115 this(info, owningUid, owningPackage, reason, source, extras, allowParallelSyncs, false, 116 NO_JOB_ID, 0, 0, syncExemptionFlag); 117 } 118 SyncOperation(SyncOperation op, long periodMillis, long flexMillis)119 public SyncOperation(SyncOperation op, long periodMillis, long flexMillis) { 120 this(op.target, op.owningUid, op.owningPackage, op.reason, op.syncSource, 121 new Bundle(op.extras), op.allowParallelSyncs, op.isPeriodic, op.sourcePeriodicId, 122 periodMillis, flexMillis, ContentResolver.SYNC_EXEMPTION_NONE); 123 } 124 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)125 public SyncOperation(SyncStorageEngine.EndPoint info, int owningUid, String owningPackage, 126 int reason, int source, Bundle extras, boolean allowParallelSyncs, 127 boolean isPeriodic, int sourcePeriodicId, long periodMillis, 128 long flexMillis, @SyncExemption int syncExemptionFlag) { 129 this.target = info; 130 this.owningUid = owningUid; 131 this.owningPackage = owningPackage; 132 this.reason = reason; 133 this.syncSource = source; 134 this.extras = new Bundle(extras); 135 this.allowParallelSyncs = allowParallelSyncs; 136 this.isPeriodic = isPeriodic; 137 this.sourcePeriodicId = sourcePeriodicId; 138 this.periodMillis = periodMillis; 139 this.flexMillis = flexMillis; 140 this.jobId = NO_JOB_ID; 141 this.key = toKey(); 142 this.syncExemptionFlag = syncExemptionFlag; 143 } 144 145 /* Get a one off sync operation instance from a periodic sync. */ createOneTimeSyncOperation()146 public SyncOperation createOneTimeSyncOperation() { 147 if (!isPeriodic) { 148 return null; 149 } 150 SyncOperation op = new SyncOperation(target, owningUid, owningPackage, reason, syncSource, 151 new Bundle(extras), allowParallelSyncs, false, jobId /* sourcePeriodicId */, 152 periodMillis, flexMillis, ContentResolver.SYNC_EXEMPTION_NONE); 153 return op; 154 } 155 SyncOperation(SyncOperation other)156 public SyncOperation(SyncOperation other) { 157 target = other.target; 158 owningUid = other.owningUid; 159 owningPackage = other.owningPackage; 160 reason = other.reason; 161 syncSource = other.syncSource; 162 allowParallelSyncs = other.allowParallelSyncs; 163 extras = new Bundle(other.extras); 164 wakeLockName = other.wakeLockName(); 165 isPeriodic = other.isPeriodic; 166 sourcePeriodicId = other.sourcePeriodicId; 167 periodMillis = other.periodMillis; 168 flexMillis = other.flexMillis; 169 this.key = other.key; 170 syncExemptionFlag = other.syncExemptionFlag; 171 } 172 173 /** 174 * All fields are stored in a corresponding key in the persistable bundle. 175 * 176 * {@link #extras} is a Bundle and can contain parcelable objects. But only the type Account 177 * is allowed {@link ContentResolver#validateSyncExtrasBundle(Bundle)} that can't be stored in 178 * a PersistableBundle. For every value of type Account with key 'key', we store a 179 * PersistableBundle containing account information at key 'ACCOUNT:key'. The Account object 180 * can be reconstructed using this. 181 * 182 * We put a flag with key 'SyncManagerJob', to identify while reconstructing a sync operation 183 * from a bundle whether the bundle actually contains information about a sync. 184 * @return A persistable bundle containing all information to re-construct the sync operation. 185 */ toJobInfoExtras()186 PersistableBundle toJobInfoExtras() { 187 // This will be passed as extras bundle to a JobScheduler job. 188 PersistableBundle jobInfoExtras = new PersistableBundle(); 189 190 PersistableBundle syncExtrasBundle = new PersistableBundle(); 191 for (String key: extras.keySet()) { 192 Object value = extras.get(key); 193 if (value instanceof Account) { 194 Account account = (Account) value; 195 PersistableBundle accountBundle = new PersistableBundle(); 196 accountBundle.putString("accountName", account.name); 197 accountBundle.putString("accountType", account.type); 198 // This is stored in jobInfoExtras so that we don't override a user specified 199 // sync extra with the same key. 200 jobInfoExtras.putPersistableBundle("ACCOUNT:" + key, accountBundle); 201 } else if (value instanceof Long) { 202 syncExtrasBundle.putLong(key, (Long) value); 203 } else if (value instanceof Integer) { 204 syncExtrasBundle.putInt(key, (Integer) value); 205 } else if (value instanceof Boolean) { 206 syncExtrasBundle.putBoolean(key, (Boolean) value); 207 } else if (value instanceof Float) { 208 syncExtrasBundle.putDouble(key, (double) (float) value); 209 } else if (value instanceof Double) { 210 syncExtrasBundle.putDouble(key, (Double) value); 211 } else if (value instanceof String) { 212 syncExtrasBundle.putString(key, (String) value); 213 } else if (value == null) { 214 syncExtrasBundle.putString(key, null); 215 } else { 216 Slog.e(TAG, "Unknown extra type."); 217 } 218 } 219 jobInfoExtras.putPersistableBundle("syncExtras", syncExtrasBundle); 220 221 jobInfoExtras.putBoolean("SyncManagerJob", true); 222 223 jobInfoExtras.putString("provider", target.provider); 224 jobInfoExtras.putString("accountName", target.account.name); 225 jobInfoExtras.putString("accountType", target.account.type); 226 jobInfoExtras.putInt("userId", target.userId); 227 jobInfoExtras.putInt("owningUid", owningUid); 228 jobInfoExtras.putString("owningPackage", owningPackage); 229 jobInfoExtras.putInt("reason", reason); 230 jobInfoExtras.putInt("source", syncSource); 231 jobInfoExtras.putBoolean("allowParallelSyncs", allowParallelSyncs); 232 jobInfoExtras.putInt("jobId", jobId); 233 jobInfoExtras.putBoolean("isPeriodic", isPeriodic); 234 jobInfoExtras.putInt("sourcePeriodicId", sourcePeriodicId); 235 jobInfoExtras.putLong("periodMillis", periodMillis); 236 jobInfoExtras.putLong("flexMillis", flexMillis); 237 jobInfoExtras.putLong("expectedRuntime", expectedRuntime); 238 jobInfoExtras.putInt("retries", retries); 239 jobInfoExtras.putInt("syncExemptionFlag", syncExemptionFlag); 240 return jobInfoExtras; 241 } 242 243 /** 244 * Reconstructs a sync operation from an extras Bundle. Returns null if the bundle doesn't 245 * contain a valid sync operation. 246 */ maybeCreateFromJobExtras(PersistableBundle jobExtras)247 static SyncOperation maybeCreateFromJobExtras(PersistableBundle jobExtras) { 248 if (jobExtras == null) { 249 return null; 250 } 251 String accountName, accountType; 252 String provider; 253 int userId, owningUid; 254 String owningPackage; 255 int reason, source; 256 int initiatedBy; 257 Bundle extras; 258 boolean allowParallelSyncs, isPeriodic; 259 long periodMillis, flexMillis; 260 int syncExemptionFlag; 261 262 if (!jobExtras.getBoolean("SyncManagerJob", false)) { 263 return null; 264 } 265 266 accountName = jobExtras.getString("accountName"); 267 accountType = jobExtras.getString("accountType"); 268 provider = jobExtras.getString("provider"); 269 userId = jobExtras.getInt("userId", Integer.MAX_VALUE); 270 owningUid = jobExtras.getInt("owningUid"); 271 owningPackage = jobExtras.getString("owningPackage"); 272 reason = jobExtras.getInt("reason", Integer.MAX_VALUE); 273 source = jobExtras.getInt("source", Integer.MAX_VALUE); 274 allowParallelSyncs = jobExtras.getBoolean("allowParallelSyncs", false); 275 isPeriodic = jobExtras.getBoolean("isPeriodic", false); 276 initiatedBy = jobExtras.getInt("sourcePeriodicId", NO_JOB_ID); 277 periodMillis = jobExtras.getLong("periodMillis"); 278 flexMillis = jobExtras.getLong("flexMillis"); 279 syncExemptionFlag = jobExtras.getInt("syncExemptionFlag", 280 ContentResolver.SYNC_EXEMPTION_NONE); 281 extras = new Bundle(); 282 283 PersistableBundle syncExtras = jobExtras.getPersistableBundle("syncExtras"); 284 if (syncExtras != null) { 285 extras.putAll(syncExtras); 286 } 287 288 for (String key: jobExtras.keySet()) { 289 if (key!= null && key.startsWith("ACCOUNT:")) { 290 String newKey = key.substring(8); // Strip off the 'ACCOUNT:' prefix. 291 PersistableBundle accountsBundle = jobExtras.getPersistableBundle(key); 292 Account account = new Account(accountsBundle.getString("accountName"), 293 accountsBundle.getString("accountType")); 294 extras.putParcelable(newKey, account); 295 } 296 } 297 298 Account account = new Account(accountName, accountType); 299 SyncStorageEngine.EndPoint target = 300 new SyncStorageEngine.EndPoint(account, provider, userId); 301 SyncOperation op = new SyncOperation(target, owningUid, owningPackage, reason, source, 302 extras, allowParallelSyncs, isPeriodic, initiatedBy, periodMillis, flexMillis, 303 syncExemptionFlag); 304 op.jobId = jobExtras.getInt("jobId"); 305 op.expectedRuntime = jobExtras.getLong("expectedRuntime"); 306 op.retries = jobExtras.getInt("retries"); 307 return op; 308 } 309 310 /** 311 * Determine whether if this sync operation is running, the provided operation would conflict 312 * with it. 313 * Parallel syncs allow multiple accounts to be synced at the same time. 314 */ isConflict(SyncOperation toRun)315 boolean isConflict(SyncOperation toRun) { 316 final SyncStorageEngine.EndPoint other = toRun.target; 317 return target.account.type.equals(other.account.type) 318 && target.provider.equals(other.provider) 319 && target.userId == other.userId 320 && (!allowParallelSyncs 321 || target.account.name.equals(other.account.name)); 322 } 323 isReasonPeriodic()324 boolean isReasonPeriodic() { 325 return reason == REASON_PERIODIC; 326 } 327 matchesPeriodicOperation(SyncOperation other)328 boolean matchesPeriodicOperation(SyncOperation other) { 329 return target.matchesSpec(other.target) 330 && SyncManager.syncExtrasEquals(extras, other.extras, true) 331 && periodMillis == other.periodMillis && flexMillis == other.flexMillis; 332 } 333 isDerivedFromFailedPeriodicSync()334 boolean isDerivedFromFailedPeriodicSync() { 335 return sourcePeriodicId != NO_JOB_ID; 336 } 337 findPriority()338 int findPriority() { 339 if (isInitialization()) { 340 return JobInfo.PRIORITY_SYNC_INITIALIZATION; 341 } else if (isExpedited()) { 342 return JobInfo.PRIORITY_SYNC_EXPEDITED; 343 } 344 return JobInfo.PRIORITY_DEFAULT; 345 } 346 toKey()347 private String toKey() { 348 StringBuilder sb = new StringBuilder(); 349 sb.append("provider: ").append(target.provider); 350 sb.append(" account {name=" + target.account.name 351 + ", user=" 352 + target.userId 353 + ", type=" 354 + target.account.type 355 + "}"); 356 sb.append(" isPeriodic: ").append(isPeriodic); 357 sb.append(" period: ").append(periodMillis); 358 sb.append(" flex: ").append(flexMillis); 359 sb.append(" extras: "); 360 extrasToStringBuilder(extras, sb); 361 return sb.toString(); 362 } 363 364 @Override toString()365 public String toString() { 366 return dump(null, true, null); 367 } 368 dump(PackageManager pm, boolean shorter, SyncAdapterStateFetcher appStates)369 String dump(PackageManager pm, boolean shorter, SyncAdapterStateFetcher appStates) { 370 StringBuilder sb = new StringBuilder(); 371 sb.append("JobId=").append(jobId) 372 .append(" ") 373 .append(target.account.name) 374 .append("/") 375 .append(target.account.type) 376 .append(" u") 377 .append(target.userId) 378 .append(" [") 379 .append(target.provider) 380 .append("] "); 381 sb.append(SyncStorageEngine.SOURCES[syncSource]); 382 if (expectedRuntime != 0) { 383 sb.append(" ExpectedIn="); 384 SyncManager.formatDurationHMS(sb, 385 (expectedRuntime - SystemClock.elapsedRealtime())); 386 } 387 if (extras.getBoolean(ContentResolver.SYNC_EXTRAS_EXPEDITED, false)) { 388 sb.append(" EXPEDITED"); 389 } 390 switch (syncExemptionFlag) { 391 case ContentResolver.SYNC_EXEMPTION_NONE: 392 break; 393 case ContentResolver.SYNC_EXEMPTION_PROMOTE_BUCKET: 394 sb.append(" STANDBY-EXEMPTED"); 395 break; 396 case ContentResolver.SYNC_EXEMPTION_PROMOTE_BUCKET_WITH_TEMP: 397 sb.append(" STANDBY-EXEMPTED(TOP)"); 398 break; 399 default: 400 sb.append(" ExemptionFlag=" + syncExemptionFlag); 401 break; 402 } 403 sb.append(" Reason="); 404 sb.append(reasonToString(pm, reason)); 405 if (isPeriodic) { 406 sb.append(" (period="); 407 SyncManager.formatDurationHMS(sb, periodMillis); 408 sb.append(" flex="); 409 SyncManager.formatDurationHMS(sb, flexMillis); 410 sb.append(")"); 411 } 412 if (retries > 0) { 413 sb.append(" Retries="); 414 sb.append(retries); 415 } 416 if (!shorter) { 417 sb.append(" Owner={"); 418 UserHandle.formatUid(sb, owningUid); 419 sb.append(" "); 420 sb.append(owningPackage); 421 if (appStates != null) { 422 sb.append(" ["); 423 sb.append(appStates.getStandbyBucket( 424 UserHandle.getUserId(owningUid), owningPackage)); 425 sb.append("]"); 426 427 if (appStates.isAppActive(owningUid)) { 428 sb.append(" [ACTIVE]"); 429 } 430 } 431 sb.append("}"); 432 if (!extras.keySet().isEmpty()) { 433 sb.append(" "); 434 extrasToStringBuilder(extras, sb); 435 } 436 } 437 return sb.toString(); 438 } 439 reasonToString(PackageManager pm, int reason)440 static String reasonToString(PackageManager pm, int reason) { 441 if (reason >= 0) { 442 if (pm != null) { 443 final String[] packages = pm.getPackagesForUid(reason); 444 if (packages != null && packages.length == 1) { 445 return packages[0]; 446 } 447 final String name = pm.getNameForUid(reason); 448 if (name != null) { 449 return name; 450 } 451 return String.valueOf(reason); 452 } else { 453 return String.valueOf(reason); 454 } 455 } else { 456 final int index = -reason - 1; 457 if (index >= REASON_NAMES.length) { 458 return String.valueOf(reason); 459 } else { 460 return REASON_NAMES[index]; 461 } 462 } 463 } 464 isInitialization()465 boolean isInitialization() { 466 return extras.getBoolean(ContentResolver.SYNC_EXTRAS_INITIALIZE, false); 467 } 468 isExpedited()469 boolean isExpedited() { 470 return extras.getBoolean(ContentResolver.SYNC_EXTRAS_EXPEDITED, false); 471 } 472 ignoreBackoff()473 boolean ignoreBackoff() { 474 return extras.getBoolean(ContentResolver.SYNC_EXTRAS_IGNORE_BACKOFF, false); 475 } 476 isNotAllowedOnMetered()477 boolean isNotAllowedOnMetered() { 478 return extras.getBoolean(ContentResolver.SYNC_EXTRAS_DISALLOW_METERED, false); 479 } 480 isManual()481 boolean isManual() { 482 return extras.getBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, false); 483 } 484 isIgnoreSettings()485 boolean isIgnoreSettings() { 486 return extras.getBoolean(ContentResolver.SYNC_EXTRAS_IGNORE_SETTINGS, false); 487 } 488 isAppStandbyExempted()489 boolean isAppStandbyExempted() { 490 return syncExemptionFlag != ContentResolver.SYNC_EXEMPTION_NONE; 491 } 492 extrasToStringBuilder(Bundle bundle, StringBuilder sb)493 static void extrasToStringBuilder(Bundle bundle, StringBuilder sb) { 494 if (bundle == null) { 495 sb.append("null"); 496 return; 497 } 498 sb.append("["); 499 for (String key : bundle.keySet()) { 500 sb.append(key).append("=").append(bundle.get(key)).append(" "); 501 } 502 sb.append("]"); 503 } 504 extrasToString(Bundle bundle)505 static String extrasToString(Bundle bundle) { 506 final StringBuilder sb = new StringBuilder(); 507 extrasToStringBuilder(bundle, sb); 508 return sb.toString(); 509 } 510 wakeLockName()511 String wakeLockName() { 512 if (wakeLockName != null) { 513 return wakeLockName; 514 } 515 return (wakeLockName = target.provider 516 + "/" + target.account.type 517 + "/" + target.account.name); 518 } 519 520 // TODO: Test this to make sure that casting to object doesn't lose the type info for EventLog. toEventLog(int event)521 public Object[] toEventLog(int event) { 522 Object[] logArray = new Object[4]; 523 logArray[1] = event; 524 logArray[2] = syncSource; 525 logArray[0] = target.provider; 526 logArray[3] = target.account.name.hashCode(); 527 return logArray; 528 } 529 } 530