Home | History | Annotate | Download | only in content
      1 /*
      2  * Copyright (C) 2009 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.accounts.AccountAndUser;
     21 import android.accounts.AccountManager;
     22 import android.app.backup.BackupManager;
     23 import android.content.ComponentName;
     24 import android.content.ContentResolver;
     25 import android.content.ContentResolver.SyncExemption;
     26 import android.content.Context;
     27 import android.content.ISyncStatusObserver;
     28 import android.content.PeriodicSync;
     29 import android.content.SyncInfo;
     30 import android.content.SyncRequest;
     31 import android.content.SyncStatusInfo;
     32 import android.content.pm.PackageManager;
     33 import android.database.Cursor;
     34 import android.database.sqlite.SQLiteDatabase;
     35 import android.database.sqlite.SQLiteException;
     36 import android.database.sqlite.SQLiteQueryBuilder;
     37 import android.os.Bundle;
     38 import android.os.Environment;
     39 import android.os.Handler;
     40 import android.os.Looper;
     41 import android.os.Message;
     42 import android.os.Parcel;
     43 import android.os.RemoteCallbackList;
     44 import android.os.RemoteException;
     45 import android.os.UserHandle;
     46 import android.util.ArrayMap;
     47 import android.util.AtomicFile;
     48 import android.util.EventLog;
     49 import android.util.Log;
     50 import android.util.Pair;
     51 import android.util.Slog;
     52 import android.util.SparseArray;
     53 import android.util.Xml;
     54 
     55 import com.android.internal.annotations.VisibleForTesting;
     56 import com.android.internal.util.ArrayUtils;
     57 import com.android.internal.util.FastXmlSerializer;
     58 
     59 import org.xmlpull.v1.XmlPullParser;
     60 import org.xmlpull.v1.XmlPullParserException;
     61 import org.xmlpull.v1.XmlSerializer;
     62 
     63 import java.io.File;
     64 import java.io.FileInputStream;
     65 import java.io.FileOutputStream;
     66 import java.nio.charset.StandardCharsets;
     67 import java.util.ArrayList;
     68 import java.util.Calendar;
     69 import java.util.HashMap;
     70 import java.util.Iterator;
     71 import java.util.List;
     72 import java.util.Random;
     73 import java.util.TimeZone;
     74 
     75 /**
     76  * Singleton that tracks the sync data and overall sync
     77  * history on the device.
     78  *
     79  * @hide
     80  */
     81 public class SyncStorageEngine {
     82 
     83     private static final String TAG = "SyncManager";
     84     private static final String TAG_FILE = "SyncManagerFile";
     85 
     86     private static final String XML_ATTR_NEXT_AUTHORITY_ID = "nextAuthorityId";
     87     private static final String XML_ATTR_LISTEN_FOR_TICKLES = "listen-for-tickles";
     88     private static final String XML_ATTR_SYNC_RANDOM_OFFSET = "offsetInSeconds";
     89     private static final String XML_ATTR_ENABLED = "enabled";
     90     private static final String XML_ATTR_USER = "user";
     91     private static final String XML_TAG_LISTEN_FOR_TICKLES = "listenForTickles";
     92 
     93     /** Default time for a periodic sync. */
     94     private static final long DEFAULT_POLL_FREQUENCY_SECONDS = 60 * 60 * 24; // One day
     95 
     96     /** Percentage of period that is flex by default, if no flexMillis is set. */
     97     private static final double DEFAULT_FLEX_PERCENT_SYNC = 0.04;
     98 
     99     /** Lower bound on sync time from which we assign a default flex time. */
    100     private static final long DEFAULT_MIN_FLEX_ALLOWED_SECS = 5;
    101 
    102     @VisibleForTesting
    103     static final long MILLIS_IN_4WEEKS = 1000L * 60 * 60 * 24 * 7 * 4;
    104 
    105     /** Enum value for a sync start event. */
    106     public static final int EVENT_START = 0;
    107 
    108     /** Enum value for a sync stop event. */
    109     public static final int EVENT_STOP = 1;
    110 
    111     /** Enum value for a sync with other sources. */
    112     public static final int SOURCE_OTHER = 0;
    113 
    114     /** Enum value for a local-initiated sync. */
    115     public static final int SOURCE_LOCAL = 1;
    116 
    117     /** Enum value for a poll-based sync (e.g., upon connection to network) */
    118     public static final int SOURCE_POLL = 2;
    119 
    120     /** Enum value for a user-initiated sync. */
    121     public static final int SOURCE_USER = 3;
    122 
    123     /** Enum value for a periodic sync. */
    124     public static final int SOURCE_PERIODIC = 4;
    125 
    126     /** Enum a sync with a "feed" extra */
    127     public static final int SOURCE_FEED = 5;
    128 
    129     public static final long NOT_IN_BACKOFF_MODE = -1;
    130 
    131     /**
    132      * String names for the sync source types.
    133      *
    134      * KEEP THIS AND {@link SyncStatusInfo#SOURCE_COUNT} IN SYNC.
    135      */
    136     public static final String[] SOURCES = {
    137             "OTHER",
    138             "LOCAL",
    139             "POLL",
    140             "USER",
    141             "PERIODIC",
    142             "FEED"};
    143 
    144     // The MESG column will contain one of these or one of the Error types.
    145     public static final String MESG_SUCCESS = "success";
    146     public static final String MESG_CANCELED = "canceled";
    147 
    148     public static final int MAX_HISTORY = 100;
    149 
    150     private static final int MSG_WRITE_STATUS = 1;
    151     private static final long WRITE_STATUS_DELAY = 1000*60*10; // 10 minutes
    152 
    153     private static final int MSG_WRITE_STATISTICS = 2;
    154     private static final long WRITE_STATISTICS_DELAY = 1000*60*30; // 1/2 hour
    155 
    156     private static final boolean SYNC_ENABLED_DEFAULT = false;
    157 
    158     // the version of the accounts xml file format
    159     private static final int ACCOUNTS_VERSION = 3;
    160 
    161     private static HashMap<String, String> sAuthorityRenames;
    162     private static PeriodicSyncAddedListener mPeriodicSyncAddedListener;
    163 
    164     private volatile boolean mIsClockValid;
    165 
    166     static {
    167         sAuthorityRenames = new HashMap<String, String>();
    168         sAuthorityRenames.put("contacts", "com.android.contacts");
    169         sAuthorityRenames.put("calendar", "com.android.calendar");
    170     }
    171 
    172     static class AccountInfo {
    173         final AccountAndUser accountAndUser;
    174         final HashMap<String, AuthorityInfo> authorities =
    175                 new HashMap<String, AuthorityInfo>();
    176 
    177         AccountInfo(AccountAndUser accountAndUser) {
    178             this.accountAndUser = accountAndUser;
    179         }
    180     }
    181 
    182     /**  Bare bones representation of a sync target. */
    183     public static class EndPoint {
    184         public final static EndPoint USER_ALL_PROVIDER_ALL_ACCOUNTS_ALL =
    185                 new EndPoint(null, null, UserHandle.USER_ALL);
    186         final Account account;
    187         final int userId;
    188         final String provider;
    189 
    190         public EndPoint(Account account, String provider, int userId) {
    191             this.account = account;
    192             this.provider = provider;
    193             this.userId = userId;
    194         }
    195 
    196         /**
    197          * An Endpoint for a sync matches if it targets the same sync adapter for the same user.
    198          *
    199          * @param spec the Endpoint to match. If the spec has null fields, they indicate a wildcard
    200          * and match any.
    201          */
    202         public boolean matchesSpec(EndPoint spec) {
    203             if (userId != spec.userId
    204                     && userId != UserHandle.USER_ALL
    205                     && spec.userId != UserHandle.USER_ALL) {
    206                 return false;
    207             }
    208             boolean accountsMatch;
    209             if (spec.account == null) {
    210                 accountsMatch = true;
    211             } else {
    212                 accountsMatch = account.equals(spec.account);
    213             }
    214             boolean providersMatch;
    215             if (spec.provider == null) {
    216                 providersMatch = true;
    217             } else {
    218                 providersMatch = provider.equals(spec.provider);
    219             }
    220             return accountsMatch && providersMatch;
    221         }
    222 
    223         public String toString() {
    224             StringBuilder sb = new StringBuilder();
    225             sb.append(account == null ? "ALL ACCS" : account.name)
    226                     .append("/")
    227                     .append(provider == null ? "ALL PDRS" : provider);
    228             sb.append(":u" + userId);
    229             return sb.toString();
    230         }
    231     }
    232 
    233     public static class AuthorityInfo {
    234         // Legal values of getIsSyncable
    235 
    236         /**
    237          * The syncable state is undefined.
    238          */
    239         public static final int UNDEFINED = -2;
    240 
    241         /**
    242          * Default state for a newly installed adapter. An uninitialized adapter will receive an
    243          * initialization sync which are governed by a different set of rules to that of regular
    244          * syncs.
    245          */
    246         public static final int NOT_INITIALIZED = -1;
    247         /**
    248          * The adapter will not receive any syncs. This is behaviourally equivalent to
    249          * setSyncAutomatically -> false. However setSyncAutomatically is surfaced to the user
    250          * while this is generally meant to be controlled by the developer.
    251          */
    252         public static final int NOT_SYNCABLE = 0;
    253         /**
    254          * The adapter is initialized and functioning. This is the normal state for an adapter.
    255          */
    256         public static final int SYNCABLE = 1;
    257         /**
    258          * The adapter is syncable but still requires an initialization sync. For example an adapter
    259          * than has been restored from a previous device will be in this state. Not meant for
    260          * external use.
    261          */
    262         public static final int SYNCABLE_NOT_INITIALIZED = 2;
    263 
    264         /**
    265          * The adapter is syncable but does not have access to the synced account and needs a
    266          * user access approval.
    267          */
    268         public static final int SYNCABLE_NO_ACCOUNT_ACCESS = 3;
    269 
    270         final EndPoint target;
    271         final int ident;
    272         boolean enabled;
    273         int syncable;
    274         /** Time at which this sync will run, taking into account backoff. */
    275         long backoffTime;
    276         /** Amount of delay due to backoff. */
    277         long backoffDelay;
    278         /** Time offset to add to any requests coming to this target. */
    279         long delayUntil;
    280 
    281         final ArrayList<PeriodicSync> periodicSyncs;
    282 
    283         /**
    284          * Copy constructor for making deep-ish copies. Only the bundles stored
    285          * in periodic syncs can make unexpected changes.
    286          *
    287          * @param toCopy AuthorityInfo to be copied.
    288          */
    289         AuthorityInfo(AuthorityInfo toCopy) {
    290             target = toCopy.target;
    291             ident = toCopy.ident;
    292             enabled = toCopy.enabled;
    293             syncable = toCopy.syncable;
    294             backoffTime = toCopy.backoffTime;
    295             backoffDelay = toCopy.backoffDelay;
    296             delayUntil = toCopy.delayUntil;
    297             periodicSyncs = new ArrayList<PeriodicSync>();
    298             for (PeriodicSync sync : toCopy.periodicSyncs) {
    299                 // Still not a perfect copy, because we are just copying the mappings.
    300                 periodicSyncs.add(new PeriodicSync(sync));
    301             }
    302         }
    303 
    304         AuthorityInfo(EndPoint info, int id) {
    305             target = info;
    306             ident = id;
    307             enabled = SYNC_ENABLED_DEFAULT;
    308             periodicSyncs = new ArrayList<PeriodicSync>();
    309             defaultInitialisation();
    310         }
    311 
    312         private void defaultInitialisation() {
    313             syncable = NOT_INITIALIZED; // default to "unknown"
    314             backoffTime = -1; // if < 0 then we aren't in backoff mode
    315             backoffDelay = -1; // if < 0 then we aren't in backoff mode
    316 
    317             if (mPeriodicSyncAddedListener != null) {
    318                 mPeriodicSyncAddedListener.onPeriodicSyncAdded(target, new Bundle(),
    319                         DEFAULT_POLL_FREQUENCY_SECONDS,
    320                         calculateDefaultFlexTime(DEFAULT_POLL_FREQUENCY_SECONDS));
    321             }
    322         }
    323 
    324         @Override
    325         public String toString() {
    326             return target + ", enabled=" + enabled + ", syncable=" + syncable + ", backoff="
    327                     + backoffTime + ", delay=" + delayUntil;
    328         }
    329     }
    330 
    331     public static class SyncHistoryItem {
    332         int authorityId;
    333         int historyId;
    334         long eventTime;
    335         long elapsedTime;
    336         int source;
    337         int event;
    338         long upstreamActivity;
    339         long downstreamActivity;
    340         String mesg;
    341         boolean initialization;
    342         Bundle extras;
    343         int reason;
    344         int syncExemptionFlag;
    345     }
    346 
    347     public static class DayStats {
    348         public final int day;
    349         public int successCount;
    350         public long successTime;
    351         public int failureCount;
    352         public long failureTime;
    353 
    354         public DayStats(int day) {
    355             this.day = day;
    356         }
    357     }
    358 
    359     interface OnSyncRequestListener {
    360 
    361         /** Called when a sync is needed on an account(s) due to some change in state. */
    362         public void onSyncRequest(EndPoint info, int reason, Bundle extras,
    363                 @SyncExemption int syncExemptionFlag);
    364     }
    365 
    366     interface PeriodicSyncAddedListener {
    367         /** Called when a periodic sync is added. */
    368         void onPeriodicSyncAdded(EndPoint target, Bundle extras, long pollFrequency, long flex);
    369     }
    370 
    371     interface OnAuthorityRemovedListener {
    372         /** Called when an authority is removed. */
    373         void onAuthorityRemoved(EndPoint removedAuthority);
    374     }
    375 
    376     /**
    377      * Validator that maintains a lazy cache of accounts and providers to tell if an authority or
    378      * account is valid.
    379      */
    380     private static class AccountAuthorityValidator {
    381         final private AccountManager mAccountManager;
    382         final private PackageManager mPackageManager;
    383         final private SparseArray<Account[]> mAccountsCache;
    384         final private SparseArray<ArrayMap<String, Boolean>> mProvidersPerUserCache;
    385 
    386         AccountAuthorityValidator(Context context) {
    387             mAccountManager = context.getSystemService(AccountManager.class);
    388             mPackageManager = context.getPackageManager();
    389             mAccountsCache = new SparseArray<>();
    390             mProvidersPerUserCache = new SparseArray<>();
    391         }
    392 
    393         // An account is valid if an installed authenticator has previously created that account
    394         // on the device
    395         boolean isAccountValid(Account account, int userId) {
    396             Account[] accountsForUser = mAccountsCache.get(userId);
    397             if (accountsForUser == null) {
    398                 accountsForUser = mAccountManager.getAccountsAsUser(userId);
    399                 mAccountsCache.put(userId, accountsForUser);
    400             }
    401             return ArrayUtils.contains(accountsForUser, account);
    402         }
    403 
    404         // An authority is only valid if it has a content provider installed on the system
    405         boolean isAuthorityValid(String authority, int userId) {
    406             ArrayMap<String, Boolean> authorityMap = mProvidersPerUserCache.get(userId);
    407             if (authorityMap == null) {
    408                 authorityMap = new ArrayMap<>();
    409                 mProvidersPerUserCache.put(userId, authorityMap);
    410             }
    411             if (!authorityMap.containsKey(authority)) {
    412                 authorityMap.put(authority, mPackageManager.resolveContentProviderAsUser(authority,
    413                         PackageManager.MATCH_DIRECT_BOOT_AWARE
    414                                 | PackageManager.MATCH_DIRECT_BOOT_UNAWARE, userId) != null);
    415             }
    416             return authorityMap.get(authority);
    417         }
    418     }
    419 
    420     // Primary list of all syncable authorities.  Also our global lock.
    421     private final SparseArray<AuthorityInfo> mAuthorities =
    422             new SparseArray<AuthorityInfo>();
    423 
    424     private final HashMap<AccountAndUser, AccountInfo> mAccounts
    425             = new HashMap<AccountAndUser, AccountInfo>();
    426 
    427     private final SparseArray<ArrayList<SyncInfo>> mCurrentSyncs
    428             = new SparseArray<ArrayList<SyncInfo>>();
    429 
    430     private final SparseArray<SyncStatusInfo> mSyncStatus =
    431             new SparseArray<SyncStatusInfo>();
    432 
    433     private final ArrayList<SyncHistoryItem> mSyncHistory =
    434             new ArrayList<SyncHistoryItem>();
    435 
    436     private final RemoteCallbackList<ISyncStatusObserver> mChangeListeners
    437             = new RemoteCallbackList<ISyncStatusObserver>();
    438 
    439     /** Reverse mapping for component name -> <userid -> target id>. */
    440     private final ArrayMap<ComponentName, SparseArray<AuthorityInfo>> mServices =
    441             new ArrayMap<ComponentName, SparseArray<AuthorityInfo>>();
    442 
    443     private int mNextAuthorityId = 0;
    444 
    445     // We keep 4 weeks of stats.
    446     private final DayStats[] mDayStats = new DayStats[7*4];
    447     private final Calendar mCal;
    448     private int mYear;
    449     private int mYearInDays;
    450 
    451     private final Context mContext;
    452 
    453     private static volatile SyncStorageEngine sSyncStorageEngine = null;
    454 
    455     private int mSyncRandomOffset;
    456 
    457     /**
    458      * This file contains the core engine state: all accounts and the
    459      * settings for them.  It must never be lost, and should be changed
    460      * infrequently, so it is stored as an XML file.
    461      */
    462     private final AtomicFile mAccountInfoFile;
    463 
    464     /**
    465      * This file contains the current sync status.  We would like to retain
    466      * it across boots, but its loss is not the end of the world, so we store
    467      * this information as binary data.
    468      */
    469     private final AtomicFile mStatusFile;
    470 
    471     /**
    472      * This file contains sync statistics.  This is purely debugging information
    473      * so is written infrequently and can be thrown away at any time.
    474      */
    475     private final AtomicFile mStatisticsFile;
    476 
    477     private int mNextHistoryId = 0;
    478     private SparseArray<Boolean> mMasterSyncAutomatically = new SparseArray<Boolean>();
    479     private boolean mDefaultMasterSyncAutomatically;
    480 
    481     private OnSyncRequestListener mSyncRequestListener;
    482     private OnAuthorityRemovedListener mAuthorityRemovedListener;
    483 
    484     private boolean mGrantSyncAdaptersAccountAccess;
    485 
    486     private final MyHandler mHandler;
    487     private final SyncLogger mLogger;
    488 
    489     private SyncStorageEngine(Context context, File dataDir, Looper looper) {
    490         mHandler = new MyHandler(looper);
    491         mContext = context;
    492         sSyncStorageEngine = this;
    493         mLogger = SyncLogger.getInstance();
    494 
    495         mCal = Calendar.getInstance(TimeZone.getTimeZone("GMT+0"));
    496 
    497         mDefaultMasterSyncAutomatically = mContext.getResources().getBoolean(
    498                 com.android.internal.R.bool.config_syncstorageengine_masterSyncAutomatically);
    499 
    500         File systemDir = new File(dataDir, "system");
    501         File syncDir = new File(systemDir, "sync");
    502         syncDir.mkdirs();
    503 
    504         maybeDeleteLegacyPendingInfoLocked(syncDir);
    505 
    506         mAccountInfoFile = new AtomicFile(new File(syncDir, "accounts.xml"), "sync-accounts");
    507         mStatusFile = new AtomicFile(new File(syncDir, "status.bin"), "sync-status");
    508         mStatisticsFile = new AtomicFile(new File(syncDir, "stats.bin"), "sync-stats");
    509 
    510         readAccountInfoLocked();
    511         readStatusLocked();
    512         readStatisticsLocked();
    513         readAndDeleteLegacyAccountInfoLocked();
    514         writeAccountInfoLocked();
    515         writeStatusLocked();
    516         writeStatisticsLocked();
    517 
    518         if (mLogger.enabled()) {
    519             final int size = mAuthorities.size();
    520             mLogger.log("Loaded ", size, " items");
    521             for (int i = 0; i < size; i++) {
    522                 mLogger.log(mAuthorities.valueAt(i));
    523             }
    524         }
    525     }
    526 
    527     public static SyncStorageEngine newTestInstance(Context context) {
    528         return new SyncStorageEngine(context, context.getFilesDir(), Looper.getMainLooper());
    529     }
    530 
    531     public static void init(Context context, Looper looper) {
    532         if (sSyncStorageEngine != null) {
    533             return;
    534         }
    535         File dataDir = Environment.getDataDirectory();
    536         sSyncStorageEngine = new SyncStorageEngine(context, dataDir, looper);
    537     }
    538 
    539     public static SyncStorageEngine getSingleton() {
    540         if (sSyncStorageEngine == null) {
    541             throw new IllegalStateException("not initialized");
    542         }
    543         return sSyncStorageEngine;
    544     }
    545 
    546     protected void setOnSyncRequestListener(OnSyncRequestListener listener) {
    547         if (mSyncRequestListener == null) {
    548             mSyncRequestListener = listener;
    549         }
    550     }
    551 
    552     protected void setOnAuthorityRemovedListener(OnAuthorityRemovedListener listener) {
    553         if (mAuthorityRemovedListener == null) {
    554             mAuthorityRemovedListener = listener;
    555         }
    556     }
    557 
    558     protected void setPeriodicSyncAddedListener(PeriodicSyncAddedListener listener) {
    559         if (mPeriodicSyncAddedListener == null) {
    560             mPeriodicSyncAddedListener = listener;
    561         }
    562     }
    563 
    564     private class MyHandler extends Handler {
    565         public MyHandler(Looper looper) {
    566             super(looper);
    567         }
    568 
    569         @Override
    570         public void handleMessage(Message msg) {
    571             if (msg.what == MSG_WRITE_STATUS) {
    572                 synchronized (mAuthorities) {
    573                     writeStatusLocked();
    574                 }
    575             } else if (msg.what == MSG_WRITE_STATISTICS) {
    576                 synchronized (mAuthorities) {
    577                     writeStatisticsLocked();
    578                 }
    579             }
    580         }
    581     }
    582 
    583     public int getSyncRandomOffset() {
    584         return mSyncRandomOffset;
    585     }
    586 
    587     public void addStatusChangeListener(int mask, ISyncStatusObserver callback) {
    588         synchronized (mAuthorities) {
    589             mChangeListeners.register(callback, mask);
    590         }
    591     }
    592 
    593     public void removeStatusChangeListener(ISyncStatusObserver callback) {
    594         synchronized (mAuthorities) {
    595             mChangeListeners.unregister(callback);
    596         }
    597     }
    598 
    599     /**
    600      * Figure out a reasonable flex time for cases where none is provided (old api calls).
    601      * @param syncTimeSeconds requested sync time from now.
    602      * @return amount of seconds before syncTimeSeconds that the sync can occur.
    603      *      I.e.
    604      *      earliest_sync_time = syncTimeSeconds - calculateDefaultFlexTime(syncTimeSeconds)
    605      * The flex time is capped at a percentage of the {@link #DEFAULT_POLL_FREQUENCY_SECONDS}.
    606      */
    607     public static long calculateDefaultFlexTime(long syncTimeSeconds) {
    608         if (syncTimeSeconds < DEFAULT_MIN_FLEX_ALLOWED_SECS) {
    609             // Small enough sync request time that we don't add flex time - developer probably
    610             // wants to wait for an operation to occur before syncing so we honour the
    611             // request time.
    612             return 0L;
    613         } else if (syncTimeSeconds < DEFAULT_POLL_FREQUENCY_SECONDS) {
    614             return (long) (syncTimeSeconds * DEFAULT_FLEX_PERCENT_SYNC);
    615         } else {
    616             // Large enough sync request time that we cap the flex time.
    617             return (long) (DEFAULT_POLL_FREQUENCY_SECONDS * DEFAULT_FLEX_PERCENT_SYNC);
    618         }
    619     }
    620 
    621     void reportChange(int which) {
    622         ArrayList<ISyncStatusObserver> reports = null;
    623         synchronized (mAuthorities) {
    624             int i = mChangeListeners.beginBroadcast();
    625             while (i > 0) {
    626                 i--;
    627                 Integer mask = (Integer)mChangeListeners.getBroadcastCookie(i);
    628                 if ((which & mask.intValue()) == 0) {
    629                     continue;
    630                 }
    631                 if (reports == null) {
    632                     reports = new ArrayList<ISyncStatusObserver>(i);
    633                 }
    634                 reports.add(mChangeListeners.getBroadcastItem(i));
    635             }
    636             mChangeListeners.finishBroadcast();
    637         }
    638 
    639         if (Log.isLoggable(TAG, Log.VERBOSE)) {
    640             Slog.v(TAG, "reportChange " + which + " to: " + reports);
    641         }
    642 
    643         if (reports != null) {
    644             int i = reports.size();
    645             while (i > 0) {
    646                 i--;
    647                 try {
    648                     reports.get(i).onStatusChanged(which);
    649                 } catch (RemoteException e) {
    650                     // The remote callback list will take care of this for us.
    651                 }
    652             }
    653         }
    654     }
    655 
    656     public boolean getSyncAutomatically(Account account, int userId, String providerName) {
    657         synchronized (mAuthorities) {
    658             if (account != null) {
    659                 AuthorityInfo authority = getAuthorityLocked(
    660                         new EndPoint(account, providerName, userId),
    661                         "getSyncAutomatically");
    662                 return authority != null && authority.enabled;
    663             }
    664 
    665             int i = mAuthorities.size();
    666             while (i > 0) {
    667                 i--;
    668                 AuthorityInfo authorityInfo = mAuthorities.valueAt(i);
    669                 if (authorityInfo.target.matchesSpec(new EndPoint(account, providerName, userId))
    670                         && authorityInfo.enabled) {
    671                     return true;
    672                 }
    673             }
    674             return false;
    675         }
    676     }
    677 
    678     public void setSyncAutomatically(Account account, int userId, String providerName,
    679             boolean sync, @SyncExemption int syncExemptionFlag, int callingUid) {
    680         if (Log.isLoggable(TAG, Log.VERBOSE)) {
    681             Slog.d(TAG, "setSyncAutomatically: " + /* account + */" provider " + providerName
    682                     + ", user " + userId + " -> " + sync);
    683         }
    684         mLogger.log("Set sync auto account=", account,
    685                 " user=", userId,
    686                 " authority=", providerName,
    687                 " value=", Boolean.toString(sync),
    688                 " callingUid=", callingUid);
    689         synchronized (mAuthorities) {
    690             AuthorityInfo authority =
    691                     getOrCreateAuthorityLocked(
    692                             new EndPoint(account, providerName, userId),
    693                             -1 /* ident */,
    694                             false);
    695             if (authority.enabled == sync) {
    696                 if (Log.isLoggable(TAG, Log.VERBOSE)) {
    697                     Slog.d(TAG, "setSyncAutomatically: already set to " + sync + ", doing nothing");
    698                 }
    699                 return;
    700             }
    701             // If the adapter was syncable but missing its initialization sync, set it to
    702             // uninitialized now. This is to give it a chance to run any one-time initialization
    703             // logic.
    704             if (sync && authority.syncable == AuthorityInfo.SYNCABLE_NOT_INITIALIZED) {
    705                 authority.syncable = AuthorityInfo.NOT_INITIALIZED;
    706             }
    707             authority.enabled = sync;
    708             writeAccountInfoLocked();
    709         }
    710 
    711         if (sync) {
    712             requestSync(account, userId, SyncOperation.REASON_SYNC_AUTO, providerName,
    713                     new Bundle(),
    714                     syncExemptionFlag);
    715         }
    716         reportChange(ContentResolver.SYNC_OBSERVER_TYPE_SETTINGS);
    717         queueBackup();
    718     }
    719 
    720     public int getIsSyncable(Account account, int userId, String providerName) {
    721         synchronized (mAuthorities) {
    722             if (account != null) {
    723                 AuthorityInfo authority = getAuthorityLocked(
    724                         new EndPoint(account, providerName, userId),
    725                         "get authority syncable");
    726                 if (authority == null) {
    727                     return AuthorityInfo.NOT_INITIALIZED;
    728                 }
    729                 return authority.syncable;
    730             }
    731 
    732             int i = mAuthorities.size();
    733             while (i > 0) {
    734                 i--;
    735                 AuthorityInfo authorityInfo = mAuthorities.valueAt(i);
    736                 if (authorityInfo.target != null
    737                         && authorityInfo.target.provider.equals(providerName)) {
    738                     return authorityInfo.syncable;
    739                 }
    740             }
    741             return AuthorityInfo.NOT_INITIALIZED;
    742         }
    743     }
    744 
    745     public void setIsSyncable(Account account, int userId, String providerName, int syncable,
    746             int callingUid) {
    747         setSyncableStateForEndPoint(new EndPoint(account, providerName, userId), syncable,
    748                 callingUid);
    749     }
    750 
    751     /**
    752      * An enabled sync service and a syncable provider's adapter both get resolved to the same
    753      * persisted variable - namely the "syncable" attribute for an AuthorityInfo in accounts.xml.
    754      * @param target target to set value for.
    755      * @param syncable 0 indicates unsyncable, <0 unknown, >0 is active/syncable.
    756      */
    757     private void setSyncableStateForEndPoint(EndPoint target, int syncable, int callingUid) {
    758         AuthorityInfo aInfo;
    759         mLogger.log("Set syncable ", target, " value=", Integer.toString(syncable),
    760                 " callingUid=", callingUid);
    761         synchronized (mAuthorities) {
    762             aInfo = getOrCreateAuthorityLocked(target, -1, false);
    763             if (syncable < AuthorityInfo.NOT_INITIALIZED) {
    764                 syncable = AuthorityInfo.NOT_INITIALIZED;
    765             }
    766             if (Log.isLoggable(TAG, Log.VERBOSE)) {
    767                 Slog.d(TAG, "setIsSyncable: " + aInfo.toString() + " -> " + syncable);
    768             }
    769             if (aInfo.syncable == syncable) {
    770                 if (Log.isLoggable(TAG, Log.VERBOSE)) {
    771                     Slog.d(TAG, "setIsSyncable: already set to " + syncable + ", doing nothing");
    772                 }
    773                 return;
    774             }
    775             aInfo.syncable = syncable;
    776             writeAccountInfoLocked();
    777         }
    778         if (syncable == AuthorityInfo.SYNCABLE) {
    779             requestSync(aInfo, SyncOperation.REASON_IS_SYNCABLE, new Bundle(),
    780                     ContentResolver.SYNC_EXEMPTION_NONE);
    781         }
    782         reportChange(ContentResolver.SYNC_OBSERVER_TYPE_SETTINGS);
    783     }
    784 
    785     public Pair<Long, Long> getBackoff(EndPoint info) {
    786         synchronized (mAuthorities) {
    787             AuthorityInfo authority = getAuthorityLocked(info, "getBackoff");
    788             if (authority != null) {
    789                 return Pair.create(authority.backoffTime, authority.backoffDelay);
    790             }
    791             return null;
    792         }
    793     }
    794 
    795     /**
    796      * Update the backoff for the given endpoint. The endpoint may be for a provider/account and
    797      * the account or provider info be null, which signifies all accounts or providers.
    798      */
    799     public void setBackoff(EndPoint info, long nextSyncTime, long nextDelay) {
    800         if (Log.isLoggable(TAG, Log.VERBOSE)) {
    801             Slog.v(TAG, "setBackoff: " + info
    802                     + " -> nextSyncTime " + nextSyncTime + ", nextDelay " + nextDelay);
    803         }
    804         boolean changed;
    805         synchronized (mAuthorities) {
    806             if (info.account == null || info.provider == null) {
    807                 // Do more work for a provider sync if the provided info has specified all
    808                 // accounts/providers.
    809                 changed = setBackoffLocked(
    810                         info.account /* may be null */,
    811                         info.userId,
    812                         info.provider /* may be null */,
    813                         nextSyncTime, nextDelay);
    814             } else {
    815                 AuthorityInfo authorityInfo =
    816                         getOrCreateAuthorityLocked(info, -1 /* ident */, true);
    817                 if (authorityInfo.backoffTime == nextSyncTime
    818                         && authorityInfo.backoffDelay == nextDelay) {
    819                     changed = false;
    820                 } else {
    821                     authorityInfo.backoffTime = nextSyncTime;
    822                     authorityInfo.backoffDelay = nextDelay;
    823                     changed = true;
    824                 }
    825             }
    826         }
    827         if (changed) {
    828             reportChange(ContentResolver.SYNC_OBSERVER_TYPE_SETTINGS);
    829         }
    830     }
    831 
    832     /**
    833      * Either set backoff for a specific authority, or set backoff for all the
    834      * accounts on a specific adapter/all adapters.
    835      *
    836      * @param account account for which to set backoff. Null to specify all accounts.
    837      * @param userId id of the user making this request.
    838      * @param providerName provider for which to set backoff. Null to specify all providers.
    839      * @return true if a change occured.
    840      */
    841     private boolean setBackoffLocked(Account account, int userId, String providerName,
    842                                      long nextSyncTime, long nextDelay) {
    843         boolean changed = false;
    844         for (AccountInfo accountInfo : mAccounts.values()) {
    845             if (account != null && !account.equals(accountInfo.accountAndUser.account)
    846                     && userId != accountInfo.accountAndUser.userId) {
    847                 continue;
    848             }
    849             for (AuthorityInfo authorityInfo : accountInfo.authorities.values()) {
    850                 if (providerName != null
    851                         && !providerName.equals(authorityInfo.target.provider)) {
    852                     continue;
    853                 }
    854                 if (authorityInfo.backoffTime != nextSyncTime
    855                         || authorityInfo.backoffDelay != nextDelay) {
    856                     authorityInfo.backoffTime = nextSyncTime;
    857                     authorityInfo.backoffDelay = nextDelay;
    858                     changed = true;
    859                 }
    860             }
    861         }
    862         return changed;
    863     }
    864 
    865     public void clearAllBackoffsLocked() {
    866         boolean changed = false;
    867         synchronized (mAuthorities) {
    868             // Clear backoff for all sync adapters.
    869             for (AccountInfo accountInfo : mAccounts.values()) {
    870                 for (AuthorityInfo authorityInfo : accountInfo.authorities.values()) {
    871                     if (authorityInfo.backoffTime != NOT_IN_BACKOFF_MODE
    872                             || authorityInfo.backoffDelay != NOT_IN_BACKOFF_MODE) {
    873                         if (Log.isLoggable(TAG, Log.VERBOSE)) {
    874                             Slog.v(TAG, "clearAllBackoffsLocked:"
    875                                     + " authority:" + authorityInfo.target
    876                                     + " account:" + accountInfo.accountAndUser.account.name
    877                                     + " user:" + accountInfo.accountAndUser.userId
    878                                     + " backoffTime was: " + authorityInfo.backoffTime
    879                                     + " backoffDelay was: " + authorityInfo.backoffDelay);
    880                         }
    881                         authorityInfo.backoffTime = NOT_IN_BACKOFF_MODE;
    882                         authorityInfo.backoffDelay = NOT_IN_BACKOFF_MODE;
    883                         changed = true;
    884                     }
    885                 }
    886             }
    887         }
    888 
    889         if (changed) {
    890             reportChange(ContentResolver.SYNC_OBSERVER_TYPE_SETTINGS);
    891         }
    892     }
    893 
    894     public long getDelayUntilTime(EndPoint info) {
    895         synchronized (mAuthorities) {
    896             AuthorityInfo authority = getAuthorityLocked(info, "getDelayUntil");
    897             if (authority == null) {
    898                 return 0;
    899             }
    900             return authority.delayUntil;
    901         }
    902     }
    903 
    904     public void setDelayUntilTime(EndPoint info, long delayUntil) {
    905         if (Log.isLoggable(TAG, Log.VERBOSE)) {
    906             Slog.v(TAG, "setDelayUntil: " + info
    907                     + " -> delayUntil " + delayUntil);
    908         }
    909         synchronized (mAuthorities) {
    910             AuthorityInfo authority = getOrCreateAuthorityLocked(info, -1, true);
    911             if (authority.delayUntil == delayUntil) {
    912                 return;
    913             }
    914             authority.delayUntil = delayUntil;
    915         }
    916         reportChange(ContentResolver.SYNC_OBSERVER_TYPE_SETTINGS);
    917     }
    918 
    919     /**
    920      * Restore all periodic syncs read from persisted files. Used to restore periodic syncs
    921      * after an OS update.
    922      */
    923     boolean restoreAllPeriodicSyncs() {
    924         if (mPeriodicSyncAddedListener == null) {
    925             return false;
    926         }
    927         synchronized (mAuthorities) {
    928             for (int i=0; i<mAuthorities.size(); i++) {
    929                 AuthorityInfo authority = mAuthorities.valueAt(i);
    930                 for (PeriodicSync periodicSync: authority.periodicSyncs) {
    931                     mPeriodicSyncAddedListener.onPeriodicSyncAdded(authority.target,
    932                             periodicSync.extras, periodicSync.period, periodicSync.flexTime);
    933                 }
    934                 authority.periodicSyncs.clear();
    935             }
    936             writeAccountInfoLocked();
    937         }
    938         return true;
    939     }
    940 
    941     public void setMasterSyncAutomatically(boolean flag, int userId,
    942             @SyncExemption int syncExemptionFlag, int callingUid) {
    943         mLogger.log("Set master enabled=", flag, " user=", userId,
    944                 " caller=" + callingUid);
    945         synchronized (mAuthorities) {
    946             Boolean auto = mMasterSyncAutomatically.get(userId);
    947             if (auto != null && auto.equals(flag)) {
    948                 return;
    949             }
    950             mMasterSyncAutomatically.put(userId, flag);
    951             writeAccountInfoLocked();
    952         }
    953         if (flag) {
    954             requestSync(null, userId, SyncOperation.REASON_MASTER_SYNC_AUTO, null,
    955                     new Bundle(),
    956                     syncExemptionFlag);
    957         }
    958         reportChange(ContentResolver.SYNC_OBSERVER_TYPE_SETTINGS);
    959         mContext.sendBroadcast(ContentResolver.ACTION_SYNC_CONN_STATUS_CHANGED);
    960         queueBackup();
    961     }
    962 
    963     public boolean getMasterSyncAutomatically(int userId) {
    964         synchronized (mAuthorities) {
    965             Boolean auto = mMasterSyncAutomatically.get(userId);
    966             return auto == null ? mDefaultMasterSyncAutomatically : auto;
    967         }
    968     }
    969 
    970     public int getAuthorityCount() {
    971         synchronized (mAuthorities) {
    972             return mAuthorities.size();
    973         }
    974     }
    975 
    976     public AuthorityInfo getAuthority(int authorityId) {
    977         synchronized (mAuthorities) {
    978             return mAuthorities.get(authorityId);
    979         }
    980     }
    981 
    982     /**
    983      * Returns true if there is currently a sync operation being actively processed for the given
    984      * target.
    985      */
    986     public boolean isSyncActive(EndPoint info) {
    987         synchronized (mAuthorities) {
    988             for (SyncInfo syncInfo : getCurrentSyncs(info.userId)) {
    989                 AuthorityInfo ainfo = getAuthority(syncInfo.authorityId);
    990                 if (ainfo != null && ainfo.target.matchesSpec(info)) {
    991                     return true;
    992                 }
    993             }
    994         }
    995         return false;
    996     }
    997 
    998     public void markPending(EndPoint info, boolean pendingValue) {
    999         synchronized (mAuthorities) {
   1000             AuthorityInfo authority = getOrCreateAuthorityLocked(info,
   1001                     -1 /* desired identifier */,
   1002                     true /* write accounts to storage */);
   1003             if (authority == null) {
   1004                 return;
   1005             }
   1006             SyncStatusInfo status = getOrCreateSyncStatusLocked(authority.ident);
   1007             status.pending = pendingValue;
   1008         }
   1009         reportChange(ContentResolver.SYNC_OBSERVER_TYPE_PENDING);
   1010     }
   1011 
   1012     /**
   1013      * Called when the set of account has changed, given the new array of
   1014      * active accounts.
   1015      */
   1016     public void doDatabaseCleanup(Account[] accounts, int userId) {
   1017         synchronized (mAuthorities) {
   1018             if (Log.isLoggable(TAG, Log.VERBOSE)) {
   1019                 Slog.v(TAG, "Updating for new accounts...");
   1020             }
   1021             SparseArray<AuthorityInfo> removing = new SparseArray<AuthorityInfo>();
   1022             Iterator<AccountInfo> accIt = mAccounts.values().iterator();
   1023             while (accIt.hasNext()) {
   1024                 AccountInfo acc = accIt.next();
   1025                 if (!ArrayUtils.contains(accounts, acc.accountAndUser.account)
   1026                         && acc.accountAndUser.userId == userId) {
   1027                     // This account no longer exists...
   1028                     if (Log.isLoggable(TAG, Log.VERBOSE)) {
   1029                         Slog.v(TAG, "Account removed: " + acc.accountAndUser);
   1030                     }
   1031                     for (AuthorityInfo auth : acc.authorities.values()) {
   1032                         removing.put(auth.ident, auth);
   1033                     }
   1034                     accIt.remove();
   1035                 }
   1036             }
   1037 
   1038             // Clean out all data structures.
   1039             int i = removing.size();
   1040             if (i > 0) {
   1041                 while (i > 0) {
   1042                     i--;
   1043                     int ident = removing.keyAt(i);
   1044                     AuthorityInfo auth = removing.valueAt(i);
   1045                     if (mAuthorityRemovedListener != null) {
   1046                         mAuthorityRemovedListener.onAuthorityRemoved(auth.target);
   1047                     }
   1048                     mAuthorities.remove(ident);
   1049                     int j = mSyncStatus.size();
   1050                     while (j > 0) {
   1051                         j--;
   1052                         if (mSyncStatus.keyAt(j) == ident) {
   1053                             mSyncStatus.remove(mSyncStatus.keyAt(j));
   1054                         }
   1055                     }
   1056                     j = mSyncHistory.size();
   1057                     while (j > 0) {
   1058                         j--;
   1059                         if (mSyncHistory.get(j).authorityId == ident) {
   1060                             mSyncHistory.remove(j);
   1061                         }
   1062                     }
   1063                 }
   1064                 writeAccountInfoLocked();
   1065                 writeStatusLocked();
   1066                 writeStatisticsLocked();
   1067             }
   1068         }
   1069     }
   1070 
   1071     /**
   1072      * Called when a sync is starting. Supply a valid ActiveSyncContext with information
   1073      * about the sync.
   1074      */
   1075     public SyncInfo addActiveSync(SyncManager.ActiveSyncContext activeSyncContext) {
   1076         final SyncInfo syncInfo;
   1077         synchronized (mAuthorities) {
   1078             if (Log.isLoggable(TAG, Log.VERBOSE)) {
   1079                 Slog.v(TAG, "setActiveSync: account="
   1080                         + " auth=" + activeSyncContext.mSyncOperation.target
   1081                         + " src=" + activeSyncContext.mSyncOperation.syncSource
   1082                         + " extras=" + activeSyncContext.mSyncOperation.extras);
   1083             }
   1084             final EndPoint info = activeSyncContext.mSyncOperation.target;
   1085             AuthorityInfo authorityInfo = getOrCreateAuthorityLocked(
   1086                     info,
   1087                     -1 /* assign a new identifier if creating a new target */,
   1088                     true /* write to storage if this results in a change */);
   1089             syncInfo = new SyncInfo(
   1090                     authorityInfo.ident,
   1091                     authorityInfo.target.account,
   1092                     authorityInfo.target.provider,
   1093                     activeSyncContext.mStartTime);
   1094             getCurrentSyncs(authorityInfo.target.userId).add(syncInfo);
   1095         }
   1096         reportActiveChange();
   1097         return syncInfo;
   1098     }
   1099 
   1100     /**
   1101      * Called to indicate that a previously active sync is no longer active.
   1102      */
   1103     public void removeActiveSync(SyncInfo syncInfo, int userId) {
   1104         synchronized (mAuthorities) {
   1105             if (Log.isLoggable(TAG, Log.VERBOSE)) {
   1106                 Slog.v(TAG, "removeActiveSync: account=" + syncInfo.account
   1107                         + " user=" + userId
   1108                         + " auth=" + syncInfo.authority);
   1109             }
   1110             getCurrentSyncs(userId).remove(syncInfo);
   1111         }
   1112 
   1113         reportActiveChange();
   1114     }
   1115 
   1116     /**
   1117      * To allow others to send active change reports, to poke clients.
   1118      */
   1119     public void reportActiveChange() {
   1120         reportChange(ContentResolver.SYNC_OBSERVER_TYPE_ACTIVE);
   1121     }
   1122 
   1123     /**
   1124      * Note that sync has started for the given operation.
   1125      */
   1126     public long insertStartSyncEvent(SyncOperation op, long now) {
   1127         long id;
   1128         synchronized (mAuthorities) {
   1129             if (Log.isLoggable(TAG, Log.VERBOSE)) {
   1130                 Slog.v(TAG, "insertStartSyncEvent: " + op);
   1131             }
   1132             AuthorityInfo authority = getAuthorityLocked(op.target, "insertStartSyncEvent");
   1133             if (authority == null) {
   1134                 return -1;
   1135             }
   1136             SyncHistoryItem item = new SyncHistoryItem();
   1137             item.initialization = op.isInitialization();
   1138             item.authorityId = authority.ident;
   1139             item.historyId = mNextHistoryId++;
   1140             if (mNextHistoryId < 0) mNextHistoryId = 0;
   1141             item.eventTime = now;
   1142             item.source = op.syncSource;
   1143             item.reason = op.reason;
   1144             item.extras = op.extras;
   1145             item.event = EVENT_START;
   1146             item.syncExemptionFlag = op.syncExemptionFlag;
   1147             mSyncHistory.add(0, item);
   1148             while (mSyncHistory.size() > MAX_HISTORY) {
   1149                 mSyncHistory.remove(mSyncHistory.size()-1);
   1150             }
   1151             id = item.historyId;
   1152             if (Log.isLoggable(TAG, Log.VERBOSE)) Slog.v(TAG, "returning historyId " + id);
   1153         }
   1154 
   1155         reportChange(ContentResolver.SYNC_OBSERVER_TYPE_STATUS);
   1156         return id;
   1157     }
   1158 
   1159     public void stopSyncEvent(long historyId, long elapsedTime, String resultMessage,
   1160                               long downstreamActivity, long upstreamActivity) {
   1161         synchronized (mAuthorities) {
   1162             if (Log.isLoggable(TAG, Log.VERBOSE)) {
   1163                 Slog.v(TAG, "stopSyncEvent: historyId=" + historyId);
   1164             }
   1165             SyncHistoryItem item = null;
   1166             int i = mSyncHistory.size();
   1167             while (i > 0) {
   1168                 i--;
   1169                 item = mSyncHistory.get(i);
   1170                 if (item.historyId == historyId) {
   1171                     break;
   1172                 }
   1173                 item = null;
   1174             }
   1175 
   1176             if (item == null) {
   1177                 Slog.w(TAG, "stopSyncEvent: no history for id " + historyId);
   1178                 return;
   1179             }
   1180 
   1181             item.elapsedTime = elapsedTime;
   1182             item.event = EVENT_STOP;
   1183             item.mesg = resultMessage;
   1184             item.downstreamActivity = downstreamActivity;
   1185             item.upstreamActivity = upstreamActivity;
   1186 
   1187             SyncStatusInfo status = getOrCreateSyncStatusLocked(item.authorityId);
   1188 
   1189             status.maybeResetTodayStats(isClockValid(), /*force=*/ false);
   1190 
   1191             status.totalStats.numSyncs++;
   1192             status.todayStats.numSyncs++;
   1193             status.totalStats.totalElapsedTime += elapsedTime;
   1194             status.todayStats.totalElapsedTime += elapsedTime;
   1195             switch (item.source) {
   1196                 case SOURCE_LOCAL:
   1197                     status.totalStats.numSourceLocal++;
   1198                     status.todayStats.numSourceLocal++;
   1199                     break;
   1200                 case SOURCE_POLL:
   1201                     status.totalStats.numSourcePoll++;
   1202                     status.todayStats.numSourcePoll++;
   1203                     break;
   1204                 case SOURCE_USER:
   1205                     status.totalStats.numSourceUser++;
   1206                     status.todayStats.numSourceUser++;
   1207                     break;
   1208                 case SOURCE_OTHER:
   1209                     status.totalStats.numSourceOther++;
   1210                     status.todayStats.numSourceOther++;
   1211                     break;
   1212                 case SOURCE_PERIODIC:
   1213                     status.totalStats.numSourcePeriodic++;
   1214                     status.todayStats.numSourcePeriodic++;
   1215                     break;
   1216                 case SOURCE_FEED:
   1217                     status.totalStats.numSourceFeed++;
   1218                     status.todayStats.numSourceFeed++;
   1219                     break;
   1220             }
   1221 
   1222             boolean writeStatisticsNow = false;
   1223             int day = getCurrentDayLocked();
   1224             if (mDayStats[0] == null) {
   1225                 mDayStats[0] = new DayStats(day);
   1226             } else if (day != mDayStats[0].day) {
   1227                 System.arraycopy(mDayStats, 0, mDayStats, 1, mDayStats.length-1);
   1228                 mDayStats[0] = new DayStats(day);
   1229                 writeStatisticsNow = true;
   1230             } else if (mDayStats[0] == null) {
   1231             }
   1232             final DayStats ds = mDayStats[0];
   1233 
   1234             final long lastSyncTime = (item.eventTime + elapsedTime);
   1235             boolean writeStatusNow = false;
   1236             if (MESG_SUCCESS.equals(resultMessage)) {
   1237                 // - if successful, update the successful columns
   1238                 if (status.lastSuccessTime == 0 || status.lastFailureTime != 0) {
   1239                     writeStatusNow = true;
   1240                 }
   1241                 status.setLastSuccess(item.source, lastSyncTime);
   1242                 ds.successCount++;
   1243                 ds.successTime += elapsedTime;
   1244             } else if (!MESG_CANCELED.equals(resultMessage)) {
   1245                 if (status.lastFailureTime == 0) {
   1246                     writeStatusNow = true;
   1247                 }
   1248                 status.totalStats.numFailures++;
   1249                 status.todayStats.numFailures++;
   1250 
   1251                 status.setLastFailure(item.source, lastSyncTime, resultMessage);
   1252 
   1253                 ds.failureCount++;
   1254                 ds.failureTime += elapsedTime;
   1255             } else {
   1256                 // Cancel
   1257                 status.totalStats.numCancels++;
   1258                 status.todayStats.numCancels++;
   1259                 writeStatusNow = true;
   1260             }
   1261             final StringBuilder event = new StringBuilder();
   1262             event.append("" + resultMessage + " Source=" + SyncStorageEngine.SOURCES[item.source]
   1263                     + " Elapsed=");
   1264             SyncManager.formatDurationHMS(event, elapsedTime);
   1265             event.append(" Reason=");
   1266             event.append(SyncOperation.reasonToString(null, item.reason));
   1267             if (item.syncExemptionFlag != ContentResolver.SYNC_EXEMPTION_NONE) {
   1268                 event.append(" Exemption=");
   1269                 switch (item.syncExemptionFlag) {
   1270                     case ContentResolver.SYNC_EXEMPTION_PROMOTE_BUCKET:
   1271                         event.append("fg");
   1272                         break;
   1273                     case ContentResolver.SYNC_EXEMPTION_PROMOTE_BUCKET_WITH_TEMP:
   1274                         event.append("top");
   1275                         break;
   1276                     default:
   1277                         event.append(item.syncExemptionFlag);
   1278                         break;
   1279                 }
   1280             }
   1281             event.append(" Extras=");
   1282             SyncOperation.extrasToStringBuilder(item.extras, event);
   1283 
   1284             status.addEvent(event.toString());
   1285 
   1286             if (writeStatusNow) {
   1287                 writeStatusLocked();
   1288             } else if (!mHandler.hasMessages(MSG_WRITE_STATUS)) {
   1289                 mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG_WRITE_STATUS),
   1290                         WRITE_STATUS_DELAY);
   1291             }
   1292             if (writeStatisticsNow) {
   1293                 writeStatisticsLocked();
   1294             } else if (!mHandler.hasMessages(MSG_WRITE_STATISTICS)) {
   1295                 mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG_WRITE_STATISTICS),
   1296                         WRITE_STATISTICS_DELAY);
   1297             }
   1298         }
   1299 
   1300         reportChange(ContentResolver.SYNC_OBSERVER_TYPE_STATUS);
   1301     }
   1302 
   1303     /**
   1304      * Return a list of the currently active syncs. Note that the returned
   1305      * items are the real, live active sync objects, so be careful what you do
   1306      * with it.
   1307      */
   1308     private List<SyncInfo> getCurrentSyncs(int userId) {
   1309         synchronized (mAuthorities) {
   1310             return getCurrentSyncsLocked(userId);
   1311         }
   1312     }
   1313 
   1314     /**
   1315      * @param userId Id of user to return current sync info.
   1316      * @param canAccessAccounts Determines whether to redact Account information from the result.
   1317      * @return a copy of the current syncs data structure. Will not return null.
   1318      */
   1319     public List<SyncInfo> getCurrentSyncsCopy(int userId, boolean canAccessAccounts) {
   1320         synchronized (mAuthorities) {
   1321             final List<SyncInfo> syncs = getCurrentSyncsLocked(userId);
   1322             final List<SyncInfo> syncsCopy = new ArrayList<SyncInfo>();
   1323             for (SyncInfo sync : syncs) {
   1324                 SyncInfo copy;
   1325                 if (!canAccessAccounts) {
   1326                     copy = SyncInfo.createAccountRedacted(
   1327                         sync.authorityId, sync.authority, sync.startTime);
   1328                 } else {
   1329                     copy = new SyncInfo(sync);
   1330                 }
   1331                 syncsCopy.add(copy);
   1332             }
   1333             return syncsCopy;
   1334         }
   1335     }
   1336 
   1337     private List<SyncInfo> getCurrentSyncsLocked(int userId) {
   1338         ArrayList<SyncInfo> syncs = mCurrentSyncs.get(userId);
   1339         if (syncs == null) {
   1340             syncs = new ArrayList<SyncInfo>();
   1341             mCurrentSyncs.put(userId, syncs);
   1342         }
   1343         return syncs;
   1344     }
   1345 
   1346     /**
   1347      * Return a copy of the specified target with the corresponding sync status
   1348      */
   1349     public Pair<AuthorityInfo, SyncStatusInfo> getCopyOfAuthorityWithSyncStatus(EndPoint info) {
   1350         synchronized (mAuthorities) {
   1351             AuthorityInfo authorityInfo = getOrCreateAuthorityLocked(info,
   1352                     -1 /* assign a new identifier if creating a new target */,
   1353                     true /* write to storage if this results in a change */);
   1354             return createCopyPairOfAuthorityWithSyncStatusLocked(authorityInfo);
   1355         }
   1356     }
   1357 
   1358     /**
   1359      * Returns the status that matches the target.
   1360      *
   1361      * @param info the endpoint target we are querying status info for.
   1362      * @return the SyncStatusInfo for the endpoint.
   1363      */
   1364     public SyncStatusInfo getStatusByAuthority(EndPoint info) {
   1365         if (info.account == null || info.provider == null) {
   1366             return null;
   1367         }
   1368         synchronized (mAuthorities) {
   1369             final int N = mSyncStatus.size();
   1370             for (int i = 0; i < N; i++) {
   1371                 SyncStatusInfo cur = mSyncStatus.valueAt(i);
   1372                 AuthorityInfo ainfo = mAuthorities.get(cur.authorityId);
   1373                 if (ainfo != null
   1374                         && ainfo.target.matchesSpec(info)) {
   1375                     return cur;
   1376                 }
   1377             }
   1378             return null;
   1379         }
   1380     }
   1381 
   1382     /** Return true if the pending status is true of any matching authorities. */
   1383     public boolean isSyncPending(EndPoint info) {
   1384         synchronized (mAuthorities) {
   1385             final int N = mSyncStatus.size();
   1386             for (int i = 0; i < N; i++) {
   1387                 SyncStatusInfo cur = mSyncStatus.valueAt(i);
   1388                 AuthorityInfo ainfo = mAuthorities.get(cur.authorityId);
   1389                 if (ainfo == null) {
   1390                     continue;
   1391                 }
   1392                 if (!ainfo.target.matchesSpec(info)) {
   1393                     continue;
   1394                 }
   1395                 if (cur.pending) {
   1396                     return true;
   1397                 }
   1398             }
   1399             return false;
   1400         }
   1401     }
   1402 
   1403     /**
   1404      * Return an array of the current sync status for all authorities.  Note
   1405      * that the objects inside the array are the real, live status objects,
   1406      * so be careful what you do with them.
   1407      */
   1408     public ArrayList<SyncHistoryItem> getSyncHistory() {
   1409         synchronized (mAuthorities) {
   1410             final int N = mSyncHistory.size();
   1411             ArrayList<SyncHistoryItem> items = new ArrayList<SyncHistoryItem>(N);
   1412             for (int i=0; i<N; i++) {
   1413                 items.add(mSyncHistory.get(i));
   1414             }
   1415             return items;
   1416         }
   1417     }
   1418 
   1419     /**
   1420      * Return an array of the current per-day statistics.  Note
   1421      * that the objects inside the array are the real, live status objects,
   1422      * so be careful what you do with them.
   1423      */
   1424     public DayStats[] getDayStatistics() {
   1425         synchronized (mAuthorities) {
   1426             DayStats[] ds = new DayStats[mDayStats.length];
   1427             System.arraycopy(mDayStats, 0, ds, 0, ds.length);
   1428             return ds;
   1429         }
   1430     }
   1431 
   1432     private Pair<AuthorityInfo, SyncStatusInfo> createCopyPairOfAuthorityWithSyncStatusLocked(
   1433             AuthorityInfo authorityInfo) {
   1434         SyncStatusInfo syncStatusInfo = getOrCreateSyncStatusLocked(authorityInfo.ident);
   1435         return Pair.create(new AuthorityInfo(authorityInfo), new SyncStatusInfo(syncStatusInfo));
   1436     }
   1437 
   1438     private int getCurrentDayLocked() {
   1439         mCal.setTimeInMillis(System.currentTimeMillis());
   1440         final int dayOfYear = mCal.get(Calendar.DAY_OF_YEAR);
   1441         if (mYear != mCal.get(Calendar.YEAR)) {
   1442             mYear = mCal.get(Calendar.YEAR);
   1443             mCal.clear();
   1444             mCal.set(Calendar.YEAR, mYear);
   1445             mYearInDays = (int)(mCal.getTimeInMillis()/86400000);
   1446         }
   1447         return dayOfYear + mYearInDays;
   1448     }
   1449 
   1450     /**
   1451      * Retrieve a target's full info, returning null if one does not exist.
   1452      *
   1453      * @param info info of the target to look up.
   1454      * @param tag If non-null, this will be used in a log message if the
   1455      * requested target does not exist.
   1456      */
   1457     private AuthorityInfo getAuthorityLocked(EndPoint info, String tag) {
   1458         AccountAndUser au = new AccountAndUser(info.account, info.userId);
   1459         AccountInfo accountInfo = mAccounts.get(au);
   1460         if (accountInfo == null) {
   1461             if (tag != null) {
   1462                 if (Log.isLoggable(TAG, Log.VERBOSE)) {
   1463                     Slog.v(TAG, tag + ": unknown account " + au);
   1464                 }
   1465             }
   1466             return null;
   1467         }
   1468         AuthorityInfo authority = accountInfo.authorities.get(info.provider);
   1469         if (authority == null) {
   1470             if (tag != null) {
   1471                 if (Log.isLoggable(TAG, Log.VERBOSE)) {
   1472                     Slog.v(TAG, tag + ": unknown provider " + info.provider);
   1473                 }
   1474             }
   1475             return null;
   1476         }
   1477         return authority;
   1478     }
   1479 
   1480     /**
   1481      * @param info info identifying target.
   1482      * @param ident unique identifier for target. -1 for none.
   1483      * @param doWrite if true, update the accounts.xml file on the disk.
   1484      * @return the authority that corresponds to the provided sync target, creating it if none
   1485      * exists.
   1486      */
   1487     private AuthorityInfo getOrCreateAuthorityLocked(EndPoint info, int ident, boolean doWrite) {
   1488         AuthorityInfo authority = null;
   1489         AccountAndUser au = new AccountAndUser(info.account, info.userId);
   1490         AccountInfo account = mAccounts.get(au);
   1491         if (account == null) {
   1492             account = new AccountInfo(au);
   1493             mAccounts.put(au, account);
   1494         }
   1495         authority = account.authorities.get(info.provider);
   1496         if (authority == null) {
   1497             authority = createAuthorityLocked(info, ident, doWrite);
   1498             account.authorities.put(info.provider, authority);
   1499         }
   1500         return authority;
   1501     }
   1502 
   1503     private AuthorityInfo createAuthorityLocked(EndPoint info, int ident, boolean doWrite) {
   1504         AuthorityInfo authority;
   1505         if (ident < 0) {
   1506             ident = mNextAuthorityId;
   1507             mNextAuthorityId++;
   1508             doWrite = true;
   1509         }
   1510         if (Log.isLoggable(TAG, Log.VERBOSE)) {
   1511             Slog.v(TAG, "created a new AuthorityInfo for " + info);
   1512         }
   1513         authority = new AuthorityInfo(info, ident);
   1514         mAuthorities.put(ident, authority);
   1515         if (doWrite) {
   1516             writeAccountInfoLocked();
   1517         }
   1518         return authority;
   1519     }
   1520 
   1521     public void removeAuthority(EndPoint info) {
   1522         synchronized (mAuthorities) {
   1523             removeAuthorityLocked(info.account, info.userId, info.provider, true /* doWrite */);
   1524         }
   1525     }
   1526 
   1527 
   1528     /**
   1529      * Remove an authority associated with a provider. Needs to be a standalone function for
   1530      * backward compatibility.
   1531      */
   1532     private void removeAuthorityLocked(Account account, int userId, String authorityName,
   1533                                        boolean doWrite) {
   1534         AccountInfo accountInfo = mAccounts.get(new AccountAndUser(account, userId));
   1535         if (accountInfo != null) {
   1536             final AuthorityInfo authorityInfo = accountInfo.authorities.remove(authorityName);
   1537             if (authorityInfo != null) {
   1538                 if (mAuthorityRemovedListener != null) {
   1539                     mAuthorityRemovedListener.onAuthorityRemoved(authorityInfo.target);
   1540                 }
   1541                 mAuthorities.remove(authorityInfo.ident);
   1542                 if (doWrite) {
   1543                     writeAccountInfoLocked();
   1544                 }
   1545             }
   1546         }
   1547     }
   1548 
   1549     private SyncStatusInfo getOrCreateSyncStatusLocked(int authorityId) {
   1550         SyncStatusInfo status = mSyncStatus.get(authorityId);
   1551         if (status == null) {
   1552             status = new SyncStatusInfo(authorityId);
   1553             mSyncStatus.put(authorityId, status);
   1554         }
   1555         return status;
   1556     }
   1557 
   1558     public void writeAllState() {
   1559         synchronized (mAuthorities) {
   1560             // Account info is always written so no need to do it here.
   1561             writeStatusLocked();
   1562             writeStatisticsLocked();
   1563         }
   1564     }
   1565 
   1566     public boolean shouldGrantSyncAdaptersAccountAccess() {
   1567         return mGrantSyncAdaptersAccountAccess;
   1568     }
   1569 
   1570     /**
   1571      * public for testing
   1572      */
   1573     public void clearAndReadState() {
   1574         synchronized (mAuthorities) {
   1575             mAuthorities.clear();
   1576             mAccounts.clear();
   1577             mServices.clear();
   1578             mSyncStatus.clear();
   1579             mSyncHistory.clear();
   1580 
   1581             readAccountInfoLocked();
   1582             readStatusLocked();
   1583             readStatisticsLocked();
   1584             readAndDeleteLegacyAccountInfoLocked();
   1585             writeAccountInfoLocked();
   1586             writeStatusLocked();
   1587             writeStatisticsLocked();
   1588         }
   1589     }
   1590 
   1591     /**
   1592      * Read all account information back in to the initial engine state.
   1593      */
   1594     private void readAccountInfoLocked() {
   1595         int highestAuthorityId = -1;
   1596         FileInputStream fis = null;
   1597         try {
   1598             fis = mAccountInfoFile.openRead();
   1599             if (Log.isLoggable(TAG_FILE, Log.VERBOSE)) {
   1600                 Slog.v(TAG_FILE, "Reading " + mAccountInfoFile.getBaseFile());
   1601             }
   1602             XmlPullParser parser = Xml.newPullParser();
   1603             parser.setInput(fis, StandardCharsets.UTF_8.name());
   1604             int eventType = parser.getEventType();
   1605             while (eventType != XmlPullParser.START_TAG &&
   1606                     eventType != XmlPullParser.END_DOCUMENT) {
   1607                 eventType = parser.next();
   1608             }
   1609             if (eventType == XmlPullParser.END_DOCUMENT) {
   1610                 Slog.i(TAG, "No initial accounts");
   1611                 return;
   1612             }
   1613 
   1614             String tagName = parser.getName();
   1615             if ("accounts".equals(tagName)) {
   1616                 String listen = parser.getAttributeValue(null, XML_ATTR_LISTEN_FOR_TICKLES);
   1617                 String versionString = parser.getAttributeValue(null, "version");
   1618                 int version;
   1619                 try {
   1620                     version = (versionString == null) ? 0 : Integer.parseInt(versionString);
   1621                 } catch (NumberFormatException e) {
   1622                     version = 0;
   1623                 }
   1624 
   1625                 if (version < 3) {
   1626                     mGrantSyncAdaptersAccountAccess = true;
   1627                 }
   1628 
   1629                 String nextIdString = parser.getAttributeValue(null, XML_ATTR_NEXT_AUTHORITY_ID);
   1630                 try {
   1631                     int id = (nextIdString == null) ? 0 : Integer.parseInt(nextIdString);
   1632                     mNextAuthorityId = Math.max(mNextAuthorityId, id);
   1633                 } catch (NumberFormatException e) {
   1634                     // don't care
   1635                 }
   1636                 String offsetString = parser.getAttributeValue(null, XML_ATTR_SYNC_RANDOM_OFFSET);
   1637                 try {
   1638                     mSyncRandomOffset = (offsetString == null) ? 0 : Integer.parseInt(offsetString);
   1639                 } catch (NumberFormatException e) {
   1640                     mSyncRandomOffset = 0;
   1641                 }
   1642                 if (mSyncRandomOffset == 0) {
   1643                     Random random = new Random(System.currentTimeMillis());
   1644                     mSyncRandomOffset = random.nextInt(86400);
   1645                 }
   1646                 mMasterSyncAutomatically.put(0, listen == null || Boolean.parseBoolean(listen));
   1647                 eventType = parser.next();
   1648                 AuthorityInfo authority = null;
   1649                 PeriodicSync periodicSync = null;
   1650                 AccountAuthorityValidator validator = new AccountAuthorityValidator(mContext);
   1651                 do {
   1652                     if (eventType == XmlPullParser.START_TAG) {
   1653                         tagName = parser.getName();
   1654                         if (parser.getDepth() == 2) {
   1655                             if ("authority".equals(tagName)) {
   1656                                 authority = parseAuthority(parser, version, validator);
   1657                                 periodicSync = null;
   1658                                 if (authority != null) {
   1659                                     if (authority.ident > highestAuthorityId) {
   1660                                         highestAuthorityId = authority.ident;
   1661                                     }
   1662                                 } else {
   1663                                     EventLog.writeEvent(0x534e4554, "26513719", -1,
   1664                                             "Malformed authority");
   1665                                 }
   1666                             } else if (XML_TAG_LISTEN_FOR_TICKLES.equals(tagName)) {
   1667                                 parseListenForTickles(parser);
   1668                             }
   1669                         } else if (parser.getDepth() == 3) {
   1670                             if ("periodicSync".equals(tagName) && authority != null) {
   1671                                 periodicSync = parsePeriodicSync(parser, authority);
   1672                             }
   1673                         } else if (parser.getDepth() == 4 && periodicSync != null) {
   1674                             if ("extra".equals(tagName)) {
   1675                                 parseExtra(parser, periodicSync.extras);
   1676                             }
   1677                         }
   1678                     }
   1679                     eventType = parser.next();
   1680                 } while (eventType != XmlPullParser.END_DOCUMENT);
   1681             }
   1682         } catch (XmlPullParserException e) {
   1683             Slog.w(TAG, "Error reading accounts", e);
   1684             return;
   1685         } catch (java.io.IOException e) {
   1686             if (fis == null) Slog.i(TAG, "No initial accounts");
   1687             else Slog.w(TAG, "Error reading accounts", e);
   1688             return;
   1689         } finally {
   1690             mNextAuthorityId = Math.max(highestAuthorityId + 1, mNextAuthorityId);
   1691             if (fis != null) {
   1692                 try {
   1693                     fis.close();
   1694                 } catch (java.io.IOException e1) {
   1695                 }
   1696             }
   1697         }
   1698 
   1699         maybeMigrateSettingsForRenamedAuthorities();
   1700     }
   1701 
   1702     /**
   1703      * Ensure the old pending.bin is deleted, as it has been changed to pending.xml.
   1704      * pending.xml was used starting in KLP.
   1705      * @param syncDir directory where the sync files are located.
   1706      */
   1707     private void maybeDeleteLegacyPendingInfoLocked(File syncDir) {
   1708         File file = new File(syncDir, "pending.bin");
   1709         if (!file.exists()) {
   1710             return;
   1711         } else {
   1712             file.delete();
   1713         }
   1714     }
   1715 
   1716     /**
   1717      * some authority names have changed. copy over their settings and delete the old ones
   1718      * @return true if a change was made
   1719      */
   1720     private boolean maybeMigrateSettingsForRenamedAuthorities() {
   1721         boolean writeNeeded = false;
   1722 
   1723         ArrayList<AuthorityInfo> authoritiesToRemove = new ArrayList<AuthorityInfo>();
   1724         final int N = mAuthorities.size();
   1725         for (int i = 0; i < N; i++) {
   1726             AuthorityInfo authority = mAuthorities.valueAt(i);
   1727             // skip this authority if it isn't one of the renamed ones
   1728             final String newAuthorityName = sAuthorityRenames.get(authority.target.provider);
   1729             if (newAuthorityName == null) {
   1730                 continue;
   1731             }
   1732 
   1733             // remember this authority so we can remove it later. we can't remove it
   1734             // now without messing up this loop iteration
   1735             authoritiesToRemove.add(authority);
   1736 
   1737             // this authority isn't enabled, no need to copy it to the new authority name since
   1738             // the default is "disabled"
   1739             if (!authority.enabled) {
   1740                 continue;
   1741             }
   1742 
   1743             // if we already have a record of this new authority then don't copy over the settings
   1744             EndPoint newInfo =
   1745                     new EndPoint(authority.target.account,
   1746                             newAuthorityName,
   1747                             authority.target.userId);
   1748             if (getAuthorityLocked(newInfo, "cleanup") != null) {
   1749                 continue;
   1750             }
   1751 
   1752             AuthorityInfo newAuthority =
   1753                     getOrCreateAuthorityLocked(newInfo, -1 /* ident */, false /* doWrite */);
   1754             newAuthority.enabled = true;
   1755             writeNeeded = true;
   1756         }
   1757 
   1758         for (AuthorityInfo authorityInfo : authoritiesToRemove) {
   1759             removeAuthorityLocked(
   1760                     authorityInfo.target.account,
   1761                     authorityInfo.target.userId,
   1762                     authorityInfo.target.provider,
   1763                     false /* doWrite */);
   1764             writeNeeded = true;
   1765         }
   1766 
   1767         return writeNeeded;
   1768     }
   1769 
   1770     private void parseListenForTickles(XmlPullParser parser) {
   1771         String user = parser.getAttributeValue(null, XML_ATTR_USER);
   1772         int userId = 0;
   1773         try {
   1774             userId = Integer.parseInt(user);
   1775         } catch (NumberFormatException e) {
   1776             Slog.e(TAG, "error parsing the user for listen-for-tickles", e);
   1777         } catch (NullPointerException e) {
   1778             Slog.e(TAG, "the user in listen-for-tickles is null", e);
   1779         }
   1780         String enabled = parser.getAttributeValue(null, XML_ATTR_ENABLED);
   1781         boolean listen = enabled == null || Boolean.parseBoolean(enabled);
   1782         mMasterSyncAutomatically.put(userId, listen);
   1783     }
   1784 
   1785     private AuthorityInfo parseAuthority(XmlPullParser parser, int version,
   1786             AccountAuthorityValidator validator) {
   1787         AuthorityInfo authority = null;
   1788         int id = -1;
   1789         try {
   1790             id = Integer.parseInt(parser.getAttributeValue(null, "id"));
   1791         } catch (NumberFormatException e) {
   1792             Slog.e(TAG, "error parsing the id of the authority", e);
   1793         } catch (NullPointerException e) {
   1794             Slog.e(TAG, "the id of the authority is null", e);
   1795         }
   1796         if (id >= 0) {
   1797             String authorityName = parser.getAttributeValue(null, "authority");
   1798             String enabled = parser.getAttributeValue(null, XML_ATTR_ENABLED);
   1799             String syncable = parser.getAttributeValue(null, "syncable");
   1800             String accountName = parser.getAttributeValue(null, "account");
   1801             String accountType = parser.getAttributeValue(null, "type");
   1802             String user = parser.getAttributeValue(null, XML_ATTR_USER);
   1803             String packageName = parser.getAttributeValue(null, "package");
   1804             String className = parser.getAttributeValue(null, "class");
   1805             int userId = user == null ? 0 : Integer.parseInt(user);
   1806             if (accountType == null && packageName == null) {
   1807                 accountType = "com.google";
   1808                 syncable = String.valueOf(AuthorityInfo.NOT_INITIALIZED);
   1809             }
   1810             authority = mAuthorities.get(id);
   1811             if (Log.isLoggable(TAG_FILE, Log.VERBOSE)) {
   1812                 Slog.v(TAG_FILE, "Adding authority:"
   1813                         + " account=" + accountName
   1814                         + " accountType=" + accountType
   1815                         + " auth=" + authorityName
   1816                         + " package=" + packageName
   1817                         + " class=" + className
   1818                         + " user=" + userId
   1819                         + " enabled=" + enabled
   1820                         + " syncable=" + syncable);
   1821             }
   1822             if (authority == null) {
   1823                 if (Log.isLoggable(TAG_FILE, Log.VERBOSE)) {
   1824                     Slog.v(TAG_FILE, "Creating authority entry");
   1825                 }
   1826                 if (accountName != null && authorityName != null) {
   1827                     EndPoint info = new EndPoint(
   1828                             new Account(accountName, accountType),
   1829                             authorityName, userId);
   1830                     if (validator.isAccountValid(info.account, userId)
   1831                             && validator.isAuthorityValid(authorityName, userId)) {
   1832                         authority = getOrCreateAuthorityLocked(info, id, false);
   1833                         // If the version is 0 then we are upgrading from a file format that did not
   1834                         // know about periodic syncs. In that case don't clear the list since we
   1835                         // want the default, which is a daily periodic sync.
   1836                         // Otherwise clear out this default list since we will populate it later
   1837                         // with
   1838                         // the periodic sync descriptions that are read from the configuration file.
   1839                         if (version > 0) {
   1840                             authority.periodicSyncs.clear();
   1841                         }
   1842                     } else {
   1843                         EventLog.writeEvent(0x534e4554, "35028827", -1,
   1844                                 "account:" + info.account + " provider:" + authorityName + " user:"
   1845                                         + userId);
   1846                     }
   1847                 }
   1848             }
   1849             if (authority != null) {
   1850                 authority.enabled = enabled == null || Boolean.parseBoolean(enabled);
   1851                 try {
   1852                     authority.syncable = (syncable == null) ?
   1853                             AuthorityInfo.NOT_INITIALIZED : Integer.parseInt(syncable);
   1854                 } catch (NumberFormatException e) {
   1855                     // On L we stored this as {"unknown", "true", "false"} so fall back to this
   1856                     // format.
   1857                     if ("unknown".equals(syncable)) {
   1858                         authority.syncable = AuthorityInfo.NOT_INITIALIZED;
   1859                     } else {
   1860                         authority.syncable = Boolean.parseBoolean(syncable) ?
   1861                                 AuthorityInfo.SYNCABLE : AuthorityInfo.NOT_SYNCABLE;
   1862                     }
   1863 
   1864                 }
   1865             } else {
   1866                 Slog.w(TAG, "Failure adding authority: account="
   1867                         + accountName + " auth=" + authorityName
   1868                         + " enabled=" + enabled
   1869                         + " syncable=" + syncable);
   1870             }
   1871         }
   1872         return authority;
   1873     }
   1874 
   1875     /**
   1876      * Parse a periodic sync from accounts.xml. Sets the bundle to be empty.
   1877      */
   1878     private PeriodicSync parsePeriodicSync(XmlPullParser parser, AuthorityInfo authorityInfo) {
   1879         Bundle extras = new Bundle(); // Gets filled in later.
   1880         String periodValue = parser.getAttributeValue(null, "period");
   1881         String flexValue = parser.getAttributeValue(null, "flex");
   1882         final long period;
   1883         long flextime;
   1884         try {
   1885             period = Long.parseLong(periodValue);
   1886         } catch (NumberFormatException e) {
   1887             Slog.e(TAG, "error parsing the period of a periodic sync", e);
   1888             return null;
   1889         } catch (NullPointerException e) {
   1890             Slog.e(TAG, "the period of a periodic sync is null", e);
   1891             return null;
   1892         }
   1893         try {
   1894             flextime = Long.parseLong(flexValue);
   1895         } catch (NumberFormatException e) {
   1896             flextime = calculateDefaultFlexTime(period);
   1897             Slog.e(TAG, "Error formatting value parsed for periodic sync flex: " + flexValue
   1898                     + ", using default: "
   1899                     + flextime);
   1900         } catch (NullPointerException expected) {
   1901             flextime = calculateDefaultFlexTime(period);
   1902             Slog.d(TAG, "No flex time specified for this sync, using a default. period: "
   1903                     + period + " flex: " + flextime);
   1904         }
   1905         PeriodicSync periodicSync;
   1906         periodicSync =
   1907                 new PeriodicSync(authorityInfo.target.account,
   1908                         authorityInfo.target.provider,
   1909                         extras,
   1910                         period, flextime);
   1911         authorityInfo.periodicSyncs.add(periodicSync);
   1912         return periodicSync;
   1913     }
   1914 
   1915     private void parseExtra(XmlPullParser parser, Bundle extras) {
   1916         String name = parser.getAttributeValue(null, "name");
   1917         String type = parser.getAttributeValue(null, "type");
   1918         String value1 = parser.getAttributeValue(null, "value1");
   1919         String value2 = parser.getAttributeValue(null, "value2");
   1920 
   1921         try {
   1922             if ("long".equals(type)) {
   1923                 extras.putLong(name, Long.parseLong(value1));
   1924             } else if ("integer".equals(type)) {
   1925                 extras.putInt(name, Integer.parseInt(value1));
   1926             } else if ("double".equals(type)) {
   1927                 extras.putDouble(name, Double.parseDouble(value1));
   1928             } else if ("float".equals(type)) {
   1929                 extras.putFloat(name, Float.parseFloat(value1));
   1930             } else if ("boolean".equals(type)) {
   1931                 extras.putBoolean(name, Boolean.parseBoolean(value1));
   1932             } else if ("string".equals(type)) {
   1933                 extras.putString(name, value1);
   1934             } else if ("account".equals(type)) {
   1935                 extras.putParcelable(name, new Account(value1, value2));
   1936             }
   1937         } catch (NumberFormatException e) {
   1938             Slog.e(TAG, "error parsing bundle value", e);
   1939         } catch (NullPointerException e) {
   1940             Slog.e(TAG, "error parsing bundle value", e);
   1941         }
   1942     }
   1943 
   1944     /**
   1945      * Write all account information to the account file.
   1946      */
   1947     private void writeAccountInfoLocked() {
   1948         if (Log.isLoggable(TAG_FILE, Log.VERBOSE)) {
   1949             Slog.v(TAG_FILE, "Writing new " + mAccountInfoFile.getBaseFile());
   1950         }
   1951         FileOutputStream fos = null;
   1952 
   1953         try {
   1954             fos = mAccountInfoFile.startWrite();
   1955             XmlSerializer out = new FastXmlSerializer();
   1956             out.setOutput(fos, StandardCharsets.UTF_8.name());
   1957             out.startDocument(null, true);
   1958             out.setFeature("http://xmlpull.org/v1/doc/features.html#indent-output", true);
   1959 
   1960             out.startTag(null, "accounts");
   1961             out.attribute(null, "version", Integer.toString(ACCOUNTS_VERSION));
   1962             out.attribute(null, XML_ATTR_NEXT_AUTHORITY_ID, Integer.toString(mNextAuthorityId));
   1963             out.attribute(null, XML_ATTR_SYNC_RANDOM_OFFSET, Integer.toString(mSyncRandomOffset));
   1964 
   1965             // Write the Sync Automatically flags for each user
   1966             final int M = mMasterSyncAutomatically.size();
   1967             for (int m = 0; m < M; m++) {
   1968                 int userId = mMasterSyncAutomatically.keyAt(m);
   1969                 Boolean listen = mMasterSyncAutomatically.valueAt(m);
   1970                 out.startTag(null, XML_TAG_LISTEN_FOR_TICKLES);
   1971                 out.attribute(null, XML_ATTR_USER, Integer.toString(userId));
   1972                 out.attribute(null, XML_ATTR_ENABLED, Boolean.toString(listen));
   1973                 out.endTag(null, XML_TAG_LISTEN_FOR_TICKLES);
   1974             }
   1975 
   1976             final int N = mAuthorities.size();
   1977             for (int i = 0; i < N; i++) {
   1978                 AuthorityInfo authority = mAuthorities.valueAt(i);
   1979                 EndPoint info = authority.target;
   1980                 out.startTag(null, "authority");
   1981                 out.attribute(null, "id", Integer.toString(authority.ident));
   1982                 out.attribute(null, XML_ATTR_USER, Integer.toString(info.userId));
   1983                 out.attribute(null, XML_ATTR_ENABLED, Boolean.toString(authority.enabled));
   1984                 out.attribute(null, "account", info.account.name);
   1985                 out.attribute(null, "type", info.account.type);
   1986                 out.attribute(null, "authority", info.provider);
   1987                 out.attribute(null, "syncable", Integer.toString(authority.syncable));
   1988                 out.endTag(null, "authority");
   1989             }
   1990             out.endTag(null, "accounts");
   1991             out.endDocument();
   1992             mAccountInfoFile.finishWrite(fos);
   1993         } catch (java.io.IOException e1) {
   1994             Slog.w(TAG, "Error writing accounts", e1);
   1995             if (fos != null) {
   1996                 mAccountInfoFile.failWrite(fos);
   1997             }
   1998         }
   1999     }
   2000 
   2001     static int getIntColumn(Cursor c, String name) {
   2002         return c.getInt(c.getColumnIndex(name));
   2003     }
   2004 
   2005     static long getLongColumn(Cursor c, String name) {
   2006         return c.getLong(c.getColumnIndex(name));
   2007     }
   2008 
   2009     /**
   2010      * TODO Remove it. It's super old code that was used to migrate the information from a sqlite
   2011      * database that we used a long time ago, and is no longer relevant.
   2012      */
   2013     private void readAndDeleteLegacyAccountInfoLocked() {
   2014         // Look for old database to initialize from.
   2015         File file = mContext.getDatabasePath("syncmanager.db");
   2016         if (!file.exists()) {
   2017             return;
   2018         }
   2019         String path = file.getPath();
   2020         SQLiteDatabase db = null;
   2021         try {
   2022             db = SQLiteDatabase.openDatabase(path, null,
   2023                     SQLiteDatabase.OPEN_READONLY);
   2024         } catch (SQLiteException e) {
   2025         }
   2026 
   2027         if (db != null) {
   2028             final boolean hasType = db.getVersion() >= 11;
   2029 
   2030             // Copy in all of the status information, as well as accounts.
   2031             if (Log.isLoggable(TAG_FILE, Log.VERBOSE)) {
   2032                 Slog.v(TAG_FILE, "Reading legacy sync accounts db");
   2033             }
   2034             SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
   2035             qb.setTables("stats, status");
   2036             HashMap<String,String> map = new HashMap<String,String>();
   2037             map.put("_id", "status._id as _id");
   2038             map.put("account", "stats.account as account");
   2039             if (hasType) {
   2040                 map.put("account_type", "stats.account_type as account_type");
   2041             }
   2042             map.put("authority", "stats.authority as authority");
   2043             map.put("totalElapsedTime", "totalElapsedTime");
   2044             map.put("numSyncs", "numSyncs");
   2045             map.put("numSourceLocal", "numSourceLocal");
   2046             map.put("numSourcePoll", "numSourcePoll");
   2047             map.put("numSourceServer", "numSourceServer");
   2048             map.put("numSourceUser", "numSourceUser");
   2049             map.put("lastSuccessSource", "lastSuccessSource");
   2050             map.put("lastSuccessTime", "lastSuccessTime");
   2051             map.put("lastFailureSource", "lastFailureSource");
   2052             map.put("lastFailureTime", "lastFailureTime");
   2053             map.put("lastFailureMesg", "lastFailureMesg");
   2054             map.put("pending", "pending");
   2055             qb.setProjectionMap(map);
   2056             qb.appendWhere("stats._id = status.stats_id");
   2057             Cursor c = qb.query(db, null, null, null, null, null, null);
   2058             while (c.moveToNext()) {
   2059                 String accountName = c.getString(c.getColumnIndex("account"));
   2060                 String accountType = hasType
   2061                         ? c.getString(c.getColumnIndex("account_type")) : null;
   2062                 if (accountType == null) {
   2063                     accountType = "com.google";
   2064                 }
   2065                 String authorityName = c.getString(c.getColumnIndex("authority"));
   2066                 AuthorityInfo authority =
   2067                         this.getOrCreateAuthorityLocked(
   2068                                 new EndPoint(new Account(accountName, accountType),
   2069                                         authorityName,
   2070                                         0 /* legacy is single-user */)
   2071                                 , -1,
   2072                                 false);
   2073                 if (authority != null) {
   2074                     int i = mSyncStatus.size();
   2075                     boolean found = false;
   2076                     SyncStatusInfo st = null;
   2077                     while (i > 0) {
   2078                         i--;
   2079                         st = mSyncStatus.valueAt(i);
   2080                         if (st.authorityId == authority.ident) {
   2081                             found = true;
   2082                             break;
   2083                         }
   2084                     }
   2085                     if (!found) {
   2086                         st = new SyncStatusInfo(authority.ident);
   2087                         mSyncStatus.put(authority.ident, st);
   2088                     }
   2089                     st.totalStats.totalElapsedTime = getLongColumn(c, "totalElapsedTime");
   2090                     st.totalStats.numSyncs = getIntColumn(c, "numSyncs");
   2091                     st.totalStats.numSourceLocal = getIntColumn(c, "numSourceLocal");
   2092                     st.totalStats.numSourcePoll = getIntColumn(c, "numSourcePoll");
   2093                     st.totalStats.numSourceOther = getIntColumn(c, "numSourceServer");
   2094                     st.totalStats.numSourceUser = getIntColumn(c, "numSourceUser");
   2095                     st.totalStats.numSourcePeriodic = 0;
   2096                     st.lastSuccessSource = getIntColumn(c, "lastSuccessSource");
   2097                     st.lastSuccessTime = getLongColumn(c, "lastSuccessTime");
   2098                     st.lastFailureSource = getIntColumn(c, "lastFailureSource");
   2099                     st.lastFailureTime = getLongColumn(c, "lastFailureTime");
   2100                     st.lastFailureMesg = c.getString(c.getColumnIndex("lastFailureMesg"));
   2101                     st.pending = getIntColumn(c, "pending") != 0;
   2102                 }
   2103             }
   2104 
   2105             c.close();
   2106 
   2107             // Retrieve the settings.
   2108             qb = new SQLiteQueryBuilder();
   2109             qb.setTables("settings");
   2110             c = qb.query(db, null, null, null, null, null, null);
   2111             while (c.moveToNext()) {
   2112                 String name = c.getString(c.getColumnIndex("name"));
   2113                 String value = c.getString(c.getColumnIndex("value"));
   2114                 if (name == null) continue;
   2115                 if (name.equals("listen_for_tickles")) {
   2116                     setMasterSyncAutomatically(value == null || Boolean.parseBoolean(value), 0,
   2117                             ContentResolver.SYNC_EXEMPTION_NONE, SyncLogger.CALLING_UID_SELF);
   2118                 } else if (name.startsWith("sync_provider_")) {
   2119                     String provider = name.substring("sync_provider_".length(),
   2120                             name.length());
   2121                     int i = mAuthorities.size();
   2122                     while (i > 0) {
   2123                         i--;
   2124                         AuthorityInfo authority = mAuthorities.valueAt(i);
   2125                         if (authority.target.provider.equals(provider)) {
   2126                             authority.enabled = value == null || Boolean.parseBoolean(value);
   2127                             authority.syncable = 1;
   2128                         }
   2129                     }
   2130                 }
   2131             }
   2132 
   2133             c.close();
   2134 
   2135             db.close();
   2136 
   2137             (new File(path)).delete();
   2138         }
   2139     }
   2140 
   2141     public static final int STATUS_FILE_END = 0;
   2142     public static final int STATUS_FILE_ITEM = 100;
   2143 
   2144     /**
   2145      * Read all sync status back in to the initial engine state.
   2146      */
   2147     private void readStatusLocked() {
   2148         if (Log.isLoggable(TAG_FILE, Log.VERBOSE)) {
   2149             Slog.v(TAG_FILE, "Reading " + mStatusFile.getBaseFile());
   2150         }
   2151         try {
   2152             byte[] data = mStatusFile.readFully();
   2153             Parcel in = Parcel.obtain();
   2154             in.unmarshall(data, 0, data.length);
   2155             in.setDataPosition(0);
   2156             int token;
   2157             while ((token=in.readInt()) != STATUS_FILE_END) {
   2158                 if (token == STATUS_FILE_ITEM) {
   2159                     SyncStatusInfo status = new SyncStatusInfo(in);
   2160                     if (mAuthorities.indexOfKey(status.authorityId) >= 0) {
   2161                         status.pending = false;
   2162                         if (Log.isLoggable(TAG_FILE, Log.VERBOSE)) {
   2163                             Slog.v(TAG_FILE, "Adding status for id " + status.authorityId);
   2164                         }
   2165                         mSyncStatus.put(status.authorityId, status);
   2166                     }
   2167                 } else {
   2168                     // Ooops.
   2169                     Slog.w(TAG, "Unknown status token: " + token);
   2170                     break;
   2171                 }
   2172             }
   2173         } catch (java.io.IOException e) {
   2174             Slog.i(TAG, "No initial status");
   2175         }
   2176     }
   2177 
   2178     /**
   2179      * Write all sync status to the sync status file.
   2180      */
   2181     private void writeStatusLocked() {
   2182         if (Log.isLoggable(TAG_FILE, Log.VERBOSE)) {
   2183             Slog.v(TAG_FILE, "Writing new " + mStatusFile.getBaseFile());
   2184         }
   2185 
   2186         // The file is being written, so we don't need to have a scheduled
   2187         // write until the next change.
   2188         mHandler.removeMessages(MSG_WRITE_STATUS);
   2189 
   2190         FileOutputStream fos = null;
   2191         try {
   2192             fos = mStatusFile.startWrite();
   2193             Parcel out = Parcel.obtain();
   2194             final int N = mSyncStatus.size();
   2195             for (int i=0; i<N; i++) {
   2196                 SyncStatusInfo status = mSyncStatus.valueAt(i);
   2197                 out.writeInt(STATUS_FILE_ITEM);
   2198                 status.writeToParcel(out, 0);
   2199             }
   2200             out.writeInt(STATUS_FILE_END);
   2201             fos.write(out.marshall());
   2202             out.recycle();
   2203 
   2204             mStatusFile.finishWrite(fos);
   2205         } catch (java.io.IOException e1) {
   2206             Slog.w(TAG, "Error writing status", e1);
   2207             if (fos != null) {
   2208                 mStatusFile.failWrite(fos);
   2209             }
   2210         }
   2211     }
   2212 
   2213     private void requestSync(AuthorityInfo authorityInfo, int reason, Bundle extras,
   2214             @SyncExemption int syncExemptionFlag) {
   2215         if (android.os.Process.myUid() == android.os.Process.SYSTEM_UID
   2216                 && mSyncRequestListener != null) {
   2217             mSyncRequestListener.onSyncRequest(authorityInfo.target, reason, extras,
   2218                     syncExemptionFlag);
   2219         } else {
   2220             SyncRequest.Builder req =
   2221                     new SyncRequest.Builder()
   2222                             .syncOnce()
   2223                             .setExtras(extras);
   2224             req.setSyncAdapter(authorityInfo.target.account, authorityInfo.target.provider);
   2225             ContentResolver.requestSync(req.build());
   2226         }
   2227     }
   2228 
   2229     private void requestSync(Account account, int userId, int reason, String authority,
   2230             Bundle extras, @SyncExemption int syncExemptionFlag) {
   2231         // If this is happening in the system process, then call the syncrequest listener
   2232         // to make a request back to the SyncManager directly.
   2233         // If this is probably a test instance, then call back through the ContentResolver
   2234         // which will know which userId to apply based on the Binder id.
   2235         if (android.os.Process.myUid() == android.os.Process.SYSTEM_UID
   2236                 && mSyncRequestListener != null) {
   2237             mSyncRequestListener.onSyncRequest(
   2238                     new EndPoint(account, authority, userId),
   2239                     reason, extras, syncExemptionFlag);
   2240         } else {
   2241             ContentResolver.requestSync(account, authority, extras);
   2242         }
   2243     }
   2244 
   2245     public static final int STATISTICS_FILE_END = 0;
   2246     public static final int STATISTICS_FILE_ITEM_OLD = 100;
   2247     public static final int STATISTICS_FILE_ITEM = 101;
   2248 
   2249     /**
   2250      * Read all sync statistics back in to the initial engine state.
   2251      */
   2252     private void readStatisticsLocked() {
   2253         try {
   2254             byte[] data = mStatisticsFile.readFully();
   2255             Parcel in = Parcel.obtain();
   2256             in.unmarshall(data, 0, data.length);
   2257             in.setDataPosition(0);
   2258             int token;
   2259             int index = 0;
   2260             while ((token=in.readInt()) != STATISTICS_FILE_END) {
   2261                 if (token == STATISTICS_FILE_ITEM
   2262                         || token == STATISTICS_FILE_ITEM_OLD) {
   2263                     int day = in.readInt();
   2264                     if (token == STATISTICS_FILE_ITEM_OLD) {
   2265                         day = day - 2009 + 14245;  // Magic!
   2266                     }
   2267                     DayStats ds = new DayStats(day);
   2268                     ds.successCount = in.readInt();
   2269                     ds.successTime = in.readLong();
   2270                     ds.failureCount = in.readInt();
   2271                     ds.failureTime = in.readLong();
   2272                     if (index < mDayStats.length) {
   2273                         mDayStats[index] = ds;
   2274                         index++;
   2275                     }
   2276                 } else {
   2277                     // Ooops.
   2278                     Slog.w(TAG, "Unknown stats token: " + token);
   2279                     break;
   2280                 }
   2281             }
   2282         } catch (java.io.IOException e) {
   2283             Slog.i(TAG, "No initial statistics");
   2284         }
   2285     }
   2286 
   2287     /**
   2288      * Write all sync statistics to the sync status file.
   2289      */
   2290     private void writeStatisticsLocked() {
   2291         if (Log.isLoggable(TAG_FILE, Log.VERBOSE)) {
   2292             Slog.v(TAG, "Writing new " + mStatisticsFile.getBaseFile());
   2293         }
   2294 
   2295         // The file is being written, so we don't need to have a scheduled
   2296         // write until the next change.
   2297         mHandler.removeMessages(MSG_WRITE_STATISTICS);
   2298 
   2299         FileOutputStream fos = null;
   2300         try {
   2301             fos = mStatisticsFile.startWrite();
   2302             Parcel out = Parcel.obtain();
   2303             final int N = mDayStats.length;
   2304             for (int i=0; i<N; i++) {
   2305                 DayStats ds = mDayStats[i];
   2306                 if (ds == null) {
   2307                     break;
   2308                 }
   2309                 out.writeInt(STATISTICS_FILE_ITEM);
   2310                 out.writeInt(ds.day);
   2311                 out.writeInt(ds.successCount);
   2312                 out.writeLong(ds.successTime);
   2313                 out.writeInt(ds.failureCount);
   2314                 out.writeLong(ds.failureTime);
   2315             }
   2316             out.writeInt(STATISTICS_FILE_END);
   2317             fos.write(out.marshall());
   2318             out.recycle();
   2319 
   2320             mStatisticsFile.finishWrite(fos);
   2321         } catch (java.io.IOException e1) {
   2322             Slog.w(TAG, "Error writing stats", e1);
   2323             if (fos != null) {
   2324                 mStatisticsFile.failWrite(fos);
   2325             }
   2326         }
   2327     }
   2328 
   2329     /**
   2330      * Let the BackupManager know that account sync settings have changed. This will trigger
   2331      * {@link com.android.server.backup.SystemBackupAgent} to run.
   2332      */
   2333     public void queueBackup() {
   2334         BackupManager.dataChanged("android");
   2335     }
   2336 
   2337     public void setClockValid() {
   2338         if (!mIsClockValid) {
   2339             mIsClockValid = true;
   2340             Slog.w(TAG, "Clock is valid now.");
   2341         }
   2342     }
   2343 
   2344     public boolean isClockValid() {
   2345         return mIsClockValid;
   2346     }
   2347 
   2348     public void resetTodayStats(boolean force) {
   2349         if (force) {
   2350             Log.w(TAG, "Force resetting today stats.");
   2351         }
   2352         synchronized (mAuthorities) {
   2353             final int N = mSyncStatus.size();
   2354             for (int i = 0; i < N; i++) {
   2355                 SyncStatusInfo cur = mSyncStatus.valueAt(i);
   2356                 cur.maybeResetTodayStats(isClockValid(), force);
   2357             }
   2358             writeStatusLocked();
   2359         }
   2360     }
   2361 }
   2362