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