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