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.pm.PackageManager;
22 import android.content.ContentResolver;
23 import android.os.Bundle;
24 import android.os.PersistableBundle;
25 import android.os.UserHandle;
26 import 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  */
34 public 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 
SyncOperation(Account account, int userId, int owningUid, String owningPackage, int reason, int source, String provider, Bundle extras, boolean allowParallelSyncs)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 
SyncOperation(SyncStorageEngine.EndPoint info, int owningUid, String owningPackage, int reason, int source, Bundle extras, boolean allowParallelSyncs)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 
SyncOperation(SyncOperation op, long periodMillis, long flexMillis)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 
SyncOperation(SyncStorageEngine.EndPoint info, int owningUid, String owningPackage, int reason, int source, Bundle extras, boolean allowParallelSyncs, boolean isPeriodic, int sourcePeriodicId, long periodMillis, long flexMillis)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. */
createOneTimeSyncOperation()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 
SyncOperation(SyncOperation other)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      */
toJobInfoExtras()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      */
maybeCreateFromJobExtras(PersistableBundle jobExtras)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      */
isConflict(SyncOperation toRun)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 
isReasonPeriodic()308     boolean isReasonPeriodic() {
309         return reason == REASON_PERIODIC;
310     }
311 
matchesPeriodicOperation(SyncOperation other)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 
isDerivedFromFailedPeriodicSync()318     boolean isDerivedFromFailedPeriodicSync() {
319         return sourcePeriodicId != NO_JOB_ID;
320     }
321 
findPriority()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 
toKey()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
toString()349     public String toString() {
350         return dump(null, true);
351     }
352 
dump(PackageManager pm, boolean useOneLine)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 
reasonToString(PackageManager pm, int reason)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 
isInitialization()413     boolean isInitialization() {
414         return extras.getBoolean(ContentResolver.SYNC_EXTRAS_INITIALIZE, false);
415     }
416 
isExpedited()417     boolean isExpedited() {
418         return extras.getBoolean(ContentResolver.SYNC_EXTRAS_EXPEDITED, false);
419     }
420 
ignoreBackoff()421     boolean ignoreBackoff() {
422         return extras.getBoolean(ContentResolver.SYNC_EXTRAS_IGNORE_BACKOFF, false);
423     }
424 
isNotAllowedOnMetered()425     boolean isNotAllowedOnMetered() {
426         return extras.getBoolean(ContentResolver.SYNC_EXTRAS_DISALLOW_METERED, false);
427     }
428 
isManual()429     boolean isManual() {
430         return extras.getBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, false);
431     }
432 
isIgnoreSettings()433     boolean isIgnoreSettings() {
434         return extras.getBoolean(ContentResolver.SYNC_EXTRAS_IGNORE_SETTINGS, false);
435     }
436 
extrasToStringBuilder(Bundle bundle, StringBuilder sb)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 
wakeLockName()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.
toEventLog(int event)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