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, false);
367     }
368 
toSafeString()369     public String toSafeString() {
370         return dump(null, true, null, true);
371     }
372 
dump(PackageManager pm, boolean shorter, SyncAdapterStateFetcher appStates, boolean logSafe)373     String dump(PackageManager pm, boolean shorter, SyncAdapterStateFetcher appStates,
374             boolean logSafe) {
375         StringBuilder sb = new StringBuilder();
376         sb.append("JobId=").append(jobId)
377                 .append(" ")
378                 .append(logSafe ? "***" : target.account.name)
379                 .append("/")
380                 .append(target.account.type)
381                 .append(" u")
382                 .append(target.userId)
383                 .append(" [")
384                 .append(target.provider)
385                 .append("] ");
386         sb.append(SyncStorageEngine.SOURCES[syncSource]);
387         if (expectedRuntime != 0) {
388             sb.append(" ExpectedIn=");
389             SyncManager.formatDurationHMS(sb,
390                     (expectedRuntime - SystemClock.elapsedRealtime()));
391         }
392         if (extras.getBoolean(ContentResolver.SYNC_EXTRAS_EXPEDITED, false)) {
393             sb.append(" EXPEDITED");
394         }
395         switch (syncExemptionFlag) {
396             case ContentResolver.SYNC_EXEMPTION_NONE:
397                 break;
398             case ContentResolver.SYNC_EXEMPTION_PROMOTE_BUCKET:
399                 sb.append(" STANDBY-EXEMPTED");
400                 break;
401             case ContentResolver.SYNC_EXEMPTION_PROMOTE_BUCKET_WITH_TEMP:
402                 sb.append(" STANDBY-EXEMPTED(TOP)");
403                 break;
404             default:
405                 sb.append(" ExemptionFlag=" + syncExemptionFlag);
406                 break;
407         }
408         sb.append(" Reason=");
409         sb.append(reasonToString(pm, reason));
410         if (isPeriodic) {
411             sb.append(" (period=");
412             SyncManager.formatDurationHMS(sb, periodMillis);
413             sb.append(" flex=");
414             SyncManager.formatDurationHMS(sb, flexMillis);
415             sb.append(")");
416         }
417         if (retries > 0) {
418             sb.append(" Retries=");
419             sb.append(retries);
420         }
421         if (!shorter) {
422             sb.append(" Owner={");
423             UserHandle.formatUid(sb, owningUid);
424             sb.append(" ");
425             sb.append(owningPackage);
426             if (appStates != null) {
427                 sb.append(" [");
428                 sb.append(appStates.getStandbyBucket(
429                         UserHandle.getUserId(owningUid), owningPackage));
430                 sb.append("]");
431 
432                 if (appStates.isAppActive(owningUid)) {
433                     sb.append(" [ACTIVE]");
434                 }
435             }
436             sb.append("}");
437             if (!extras.keySet().isEmpty()) {
438                 sb.append(" ");
439                 extrasToStringBuilder(extras, sb);
440             }
441         }
442         return sb.toString();
443     }
444 
reasonToString(PackageManager pm, int reason)445     static String reasonToString(PackageManager pm, int reason) {
446         if (reason >= 0) {
447             if (pm != null) {
448                 final String[] packages = pm.getPackagesForUid(reason);
449                 if (packages != null && packages.length == 1) {
450                     return packages[0];
451                 }
452                 final String name = pm.getNameForUid(reason);
453                 if (name != null) {
454                     return name;
455                 }
456                 return String.valueOf(reason);
457             } else {
458                 return String.valueOf(reason);
459             }
460         } else {
461             final int index = -reason - 1;
462             if (index >= REASON_NAMES.length) {
463                 return String.valueOf(reason);
464             } else {
465                 return REASON_NAMES[index];
466             }
467         }
468     }
469 
isInitialization()470     boolean isInitialization() {
471         return extras.getBoolean(ContentResolver.SYNC_EXTRAS_INITIALIZE, false);
472     }
473 
isExpedited()474     boolean isExpedited() {
475         return extras.getBoolean(ContentResolver.SYNC_EXTRAS_EXPEDITED, false);
476     }
477 
ignoreBackoff()478     boolean ignoreBackoff() {
479         return extras.getBoolean(ContentResolver.SYNC_EXTRAS_IGNORE_BACKOFF, false);
480     }
481 
isNotAllowedOnMetered()482     boolean isNotAllowedOnMetered() {
483         return extras.getBoolean(ContentResolver.SYNC_EXTRAS_DISALLOW_METERED, false);
484     }
485 
isManual()486     boolean isManual() {
487         return extras.getBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, false);
488     }
489 
isIgnoreSettings()490     boolean isIgnoreSettings() {
491         return extras.getBoolean(ContentResolver.SYNC_EXTRAS_IGNORE_SETTINGS, false);
492     }
493 
isAppStandbyExempted()494     boolean isAppStandbyExempted() {
495         return syncExemptionFlag != ContentResolver.SYNC_EXEMPTION_NONE;
496     }
497 
extrasToStringBuilder(Bundle bundle, StringBuilder sb)498     static void extrasToStringBuilder(Bundle bundle, StringBuilder sb) {
499         if (bundle == null) {
500             sb.append("null");
501             return;
502         }
503         sb.append("[");
504         for (String key : bundle.keySet()) {
505             sb.append(key).append("=").append(bundle.get(key)).append(" ");
506         }
507         sb.append("]");
508     }
509 
extrasToString(Bundle bundle)510     static String extrasToString(Bundle bundle) {
511         final StringBuilder sb = new StringBuilder();
512         extrasToStringBuilder(bundle, sb);
513         return sb.toString();
514     }
515 
wakeLockName()516     String wakeLockName() {
517         if (wakeLockName != null) {
518             return wakeLockName;
519         }
520         return (wakeLockName = target.provider
521                 + "/" + target.account.type
522                 + "/" + target.account.name);
523     }
524 
525     // TODO: Test this to make sure that casting to object doesn't lose the type info for EventLog.
toEventLog(int event)526     public Object[] toEventLog(int event) {
527         Object[] logArray = new Object[4];
528         logArray[1] = event;
529         logArray[2] = syncSource;
530         logArray[0] = target.provider;
531         logArray[3] = target.account.name.hashCode();
532         return logArray;
533     }
534 }
535