| /* |
| * 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.content.pm.PackageManager; |
| import android.content.ComponentName; |
| import android.content.ContentResolver; |
| import android.os.Bundle; |
| import android.os.SystemClock; |
| import android.util.Log; |
| |
| /** |
| * Value type that represents a sync operation. |
| * TODO: This is the class to flesh out with all the scheduling data - metered/unmetered, |
| * transfer-size, etc. |
| * {@hide} |
| */ |
| public class SyncOperation implements Comparable { |
| public static final String TAG = "SyncManager"; |
| |
| 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", |
| }; |
| |
| public static final int SYNC_TARGET_UNKNOWN = 0; |
| public static final int SYNC_TARGET_ADAPTER = 1; |
| public static final int SYNC_TARGET_SERVICE = 2; |
| |
| /** Identifying info for the target for this operation. */ |
| public final SyncStorageEngine.EndPoint target; |
| /** Why this sync was kicked off. {@link #REASON_NAMES} */ |
| public final int reason; |
| /** Where this sync was initiated. */ |
| public int syncSource; |
| public final boolean allowParallelSyncs; |
| public Bundle extras; |
| public final String key; |
| public boolean expedited; |
| /** Bare-bones version of this operation that is persisted across reboots. */ |
| public SyncStorageEngine.PendingOperation pendingOperation; |
| /** Elapsed real time in millis at which to run this sync. */ |
| public long latestRunTime; |
| /** Set by the SyncManager in order to delay retries. */ |
| public Long backoff; |
| /** Specified by the adapter to delay subsequent sync operations. */ |
| public long delayUntil; |
| /** |
| * Elapsed real time in millis when this sync will be run. |
| * Depends on max(backoff, latestRunTime, and delayUntil). |
| */ |
| public long effectiveRunTime; |
| /** Amount of time before {@link #effectiveRunTime} from which this sync can run. */ |
| public long flexTime; |
| |
| public SyncOperation(Account account, int userId, int reason, int source, String provider, |
| Bundle extras, long runTimeFromNow, long flexTime, long backoff, |
| long delayUntil, boolean allowParallelSyncs) { |
| this.target = new SyncStorageEngine.EndPoint(account, provider, userId); |
| this.reason = reason; |
| this.allowParallelSyncs = allowParallelSyncs; |
| this.key = initialiseOperation(this.target, source, extras, runTimeFromNow, flexTime, |
| backoff, delayUntil); |
| } |
| |
| public SyncOperation(ComponentName service, int userId, int reason, int source, |
| Bundle extras, long runTimeFromNow, long flexTime, long backoff, |
| long delayUntil) { |
| this.target = new SyncStorageEngine.EndPoint(service, userId); |
| // Default to true for sync service. The service itself decides how to handle this. |
| this.allowParallelSyncs = true; |
| this.reason = reason; |
| this.key = |
| initialiseOperation(this.target, |
| source, extras, runTimeFromNow, flexTime, backoff, delayUntil); |
| } |
| |
| /** Used to reschedule a sync at a new point in time. */ |
| SyncOperation(SyncOperation other, long newRunTimeFromNow) { |
| this.target = other.target; |
| this.reason = other.reason; |
| this.expedited = other.expedited; |
| this.allowParallelSyncs = other.allowParallelSyncs; |
| // re-use old flex, but only |
| long newFlexTime = Math.min(other.flexTime, newRunTimeFromNow); |
| this.key = |
| initialiseOperation(this.target, |
| other.syncSource, other.extras, |
| newRunTimeFromNow /* runTimeFromNow*/, |
| newFlexTime /* flexTime */, |
| other.backoff, |
| 0L /* delayUntil */); |
| } |
| |
| private String initialiseOperation(SyncStorageEngine.EndPoint info, int source, Bundle extras, |
| long runTimeFromNow, long flexTime, long backoff, long delayUntil) { |
| this.syncSource = source; |
| this.extras = new Bundle(extras); |
| cleanBundle(this.extras); |
| this.delayUntil = delayUntil; |
| this.backoff = backoff; |
| final long now = SystemClock.elapsedRealtime(); |
| if (runTimeFromNow < 0 || isExpedited()) { |
| this.expedited = true; |
| this.latestRunTime = now; |
| this.flexTime = 0; |
| } else { |
| this.expedited = false; |
| this.latestRunTime = now + runTimeFromNow; |
| this.flexTime = flexTime; |
| } |
| updateEffectiveRunTime(); |
| return toKey(info, this.extras); |
| } |
| |
| public boolean matchesAuthority(SyncOperation other) { |
| return this.target.matchesSpec(other.target); |
| } |
| |
| /** |
| * Make sure the bundle attached to this SyncOperation doesn't have unnecessary |
| * flags set. |
| * @param bundle to clean. |
| */ |
| private void cleanBundle(Bundle bundle) { |
| removeFalseExtra(bundle, ContentResolver.SYNC_EXTRAS_UPLOAD); |
| removeFalseExtra(bundle, ContentResolver.SYNC_EXTRAS_MANUAL); |
| removeFalseExtra(bundle, ContentResolver.SYNC_EXTRAS_IGNORE_SETTINGS); |
| removeFalseExtra(bundle, ContentResolver.SYNC_EXTRAS_IGNORE_BACKOFF); |
| removeFalseExtra(bundle, ContentResolver.SYNC_EXTRAS_DO_NOT_RETRY); |
| removeFalseExtra(bundle, ContentResolver.SYNC_EXTRAS_DISCARD_LOCAL_DELETIONS); |
| removeFalseExtra(bundle, ContentResolver.SYNC_EXTRAS_EXPEDITED); |
| removeFalseExtra(bundle, ContentResolver.SYNC_EXTRAS_OVERRIDE_TOO_MANY_DELETIONS); |
| removeFalseExtra(bundle, ContentResolver.SYNC_EXTRAS_DISALLOW_METERED); |
| } |
| |
| private void removeFalseExtra(Bundle bundle, String extraName) { |
| if (!bundle.getBoolean(extraName, false)) { |
| bundle.remove(extraName); |
| } |
| } |
| |
| /** |
| * 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. |
| */ |
| public boolean isConflict(SyncOperation toRun) { |
| final SyncStorageEngine.EndPoint other = toRun.target; |
| if (target.target_provider) { |
| 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)); |
| } else { |
| // Ops that target a service default to allow parallel syncs, which is handled by the |
| // service returning SYNC_IN_PROGRESS if they don't. |
| return target.service.equals(other.service) && !allowParallelSyncs; |
| } |
| } |
| |
| @Override |
| public String toString() { |
| return dump(null, true); |
| } |
| |
| public String dump(PackageManager pm, boolean useOneLine) { |
| StringBuilder sb = new StringBuilder(); |
| if (target.target_provider) { |
| sb.append(target.account.name) |
| .append(" u") |
| .append(target.userId).append(" (") |
| .append(target.account.type) |
| .append(")") |
| .append(", ") |
| .append(target.provider) |
| .append(", "); |
| } else if (target.target_service) { |
| sb.append(target.service.getPackageName()) |
| .append(" u") |
| .append(target.userId).append(" (") |
| .append(target.service.getClassName()).append(")") |
| .append(", "); |
| } |
| sb.append(SyncStorageEngine.SOURCES[syncSource]) |
| .append(", currentRunTime ") |
| .append(effectiveRunTime); |
| if (expedited) { |
| sb.append(", EXPEDITED"); |
| } |
| sb.append(", reason: "); |
| sb.append(reasonToString(pm, reason)); |
| if (!useOneLine && !extras.keySet().isEmpty()) { |
| sb.append("\n "); |
| extrasToStringBuilder(extras, sb); |
| } |
| return sb.toString(); |
| } |
| |
| public 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]; |
| } |
| } |
| } |
| |
| public boolean isInitialization() { |
| return extras.getBoolean(ContentResolver.SYNC_EXTRAS_INITIALIZE, false); |
| } |
| |
| public boolean isExpedited() { |
| return extras.getBoolean(ContentResolver.SYNC_EXTRAS_EXPEDITED, false) || expedited; |
| } |
| |
| public boolean ignoreBackoff() { |
| return extras.getBoolean(ContentResolver.SYNC_EXTRAS_IGNORE_BACKOFF, false); |
| } |
| |
| public boolean isNotAllowedOnMetered() { |
| return extras.getBoolean(ContentResolver.SYNC_EXTRAS_DISALLOW_METERED, false); |
| } |
| |
| /** Changed in V3. */ |
| public static String toKey(SyncStorageEngine.EndPoint info, Bundle extras) { |
| StringBuilder sb = new StringBuilder(); |
| if (info.target_provider) { |
| sb.append("provider: ").append(info.provider); |
| sb.append(" account {name=" + info.account.name |
| + ", user=" |
| + info.userId |
| + ", type=" |
| + info.account.type |
| + "}"); |
| } else if (info.target_service) { |
| sb.append("service {package=" ) |
| .append(info.service.getPackageName()) |
| .append(" user=") |
| .append(info.userId) |
| .append(", class=") |
| .append(info.service.getClassName()) |
| .append("}"); |
| } else { |
| Log.v(TAG, "Converting SyncOperaton to key, invalid target: " + info.toString()); |
| return ""; |
| } |
| sb.append(" extras: "); |
| extrasToStringBuilder(extras, sb); |
| return sb.toString(); |
| } |
| |
| private static void extrasToStringBuilder(Bundle bundle, StringBuilder sb) { |
| sb.append("["); |
| for (String key : bundle.keySet()) { |
| sb.append(key).append("=").append(bundle.get(key)).append(" "); |
| } |
| sb.append("]"); |
| } |
| |
| public String wakeLockKey() { |
| if (target.target_provider) { |
| return target.account.name + "/" + target.account.type + ":" + target.provider; |
| } else if (target.target_service) { |
| return target.service.getPackageName() + "/" + target.service.getClassName(); |
| } else { |
| Log.wtf(TAG, "Invalid target getting wakelock for operation - " + key); |
| return null; |
| } |
| } |
| |
| public String wakeLockName() { |
| if (target.target_provider) { |
| return "/" + target.provider |
| + "/" + target.account.type |
| + "/" + target.account.name; |
| } else if (target.target_service) { |
| return "/" + target.service.getPackageName() |
| + "/" + target.service.getClassName(); |
| } else { |
| Log.wtf(TAG, "Invalid target getting wakelock name for operation - " + key); |
| return null; |
| } |
| } |
| |
| /** |
| * Update the effective run time of this Operation based on latestRunTime (specified at |
| * creation time of sync), delayUntil (specified by SyncAdapter), or backoff (specified by |
| * SyncManager on soft failures). |
| */ |
| public void updateEffectiveRunTime() { |
| // Regardless of whether we're in backoff or honouring a delayUntil, we still incorporate |
| // the flex time provided by the developer. |
| effectiveRunTime = ignoreBackoff() ? |
| latestRunTime : |
| Math.max(Math.max(latestRunTime, delayUntil), backoff); |
| } |
| |
| /** |
| * SyncOperations are sorted based on their earliest effective run time. |
| * This comparator is used to sort the SyncOps at a given time when |
| * deciding which to run, so earliest run time is the best criteria. |
| */ |
| @Override |
| public int compareTo(Object o) { |
| SyncOperation other = (SyncOperation) o; |
| if (expedited != other.expedited) { |
| return expedited ? -1 : 1; |
| } |
| long thisIntervalStart = Math.max(effectiveRunTime - flexTime, 0); |
| long otherIntervalStart = Math.max( |
| other.effectiveRunTime - other.flexTime, 0); |
| if (thisIntervalStart < otherIntervalStart) { |
| return -1; |
| } else if (otherIntervalStart < thisIntervalStart) { |
| return 1; |
| } else { |
| return 0; |
| } |
| } |
| |
| // 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; |
| if (target.target_provider) { |
| logArray[0] = target.provider; |
| logArray[3] = target.account.name.hashCode(); |
| } else if (target.target_service) { |
| logArray[0] = target.service.getPackageName(); |
| logArray[3] = target.service.hashCode(); |
| } else { |
| Log.wtf(TAG, "sync op with invalid target: " + key); |
| } |
| return logArray; |
| } |
| } |