Home | History | Annotate | Download | only in service
      1 /*
      2  * Copyright (C) 2014 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.exchange.service;
     18 
     19 import android.app.AlarmManager;
     20 import android.app.PendingIntent;
     21 import android.app.Service;
     22 import android.content.ContentResolver;
     23 import android.content.Context;
     24 import android.content.Intent;
     25 import android.os.Bundle;
     26 import android.os.SystemClock;
     27 import android.support.v4.util.LongSparseArray;
     28 import android.text.format.DateUtils;
     29 
     30 import com.android.emailcommon.provider.Account;
     31 import com.android.emailcommon.provider.EmailContent;
     32 import com.android.emailcommon.provider.Mailbox;
     33 import com.android.exchange.Eas;
     34 import com.android.exchange.eas.EasPing;
     35 import com.android.mail.utils.LogUtils;
     36 
     37 import java.util.concurrent.locks.Condition;
     38 import java.util.concurrent.locks.Lock;
     39 import java.util.concurrent.locks.ReentrantLock;
     40 
     41 /**
     42  * Bookkeeping for handling synchronization between pings and other sync related operations.
     43  * "Ping" refers to a hanging POST or GET that is used to receive push notifications. Ping is
     44  * the term for the Exchange command, but this code should be generic enough to be extended to IMAP.
     45  *
     46  * Basic rules of how these interact (note that all rules are per account):
     47  * - Only one operation (ping or other active sync operation) may run at a time.
     48  * - For shorthand, this class uses "sync" to mean "non-ping operation"; most such operations are
     49  *   sync ops, but some may not be (e.g. EAS Settings).
     50  * - Syncs can come from many sources concurrently; this class must serialize them.
     51  *
     52  * WHEN A SYNC STARTS:
     53  * - If nothing is running, proceed.
     54  * - If something is already running: wait until it's done.
     55  * - If the running thing is a ping task: interrupt it.
     56  *
     57  * WHEN A SYNC ENDS:
     58  * - If there are waiting syncs: signal one to proceed.
     59  * - If there are no waiting syncs and this account is configured for push: start a ping.
     60  * - Otherwise: This account is now idle.
     61  *
     62  * WHEN A PING TASK ENDS:
     63  * - A ping task loops until either it's interrupted by a sync (in which case, there will be one or
     64  *   more waiting syncs when the ping terminates), or encounters an error.
     65  * - If there are waiting syncs, and we were interrupted: signal one to proceed.
     66  * - If there are waiting syncs, but the ping terminated with an error: TODO: How to handle?
     67  * - If there are no waiting syncs and this account is configured for push: This means the ping task
     68  *   was terminated due to an error. Handle this by sending a sync request through the SyncManager
     69  *   that doesn't actually do any syncing, and whose only effect is to restart the ping.
     70  * - Otherwise: This account is now idle.
     71  *
     72  * WHEN AN ACCOUNT WANTS TO START OR CHANGE ITS PUSH BEHAVIOR:
     73  * - If nothing is running, start a new ping task.
     74  * - If a ping task is currently running, restart it with the new settings.
     75  * - If a sync is currently running, do nothing.
     76  *
     77  * WHEN AN ACCOUNT WANTS TO STOP GETTING PUSH:
     78  * - If nothing is running, do nothing.
     79  * - If a ping task is currently running, interrupt it.
     80  */
     81 public class PingSyncSynchronizer {
     82 
     83     private static final String TAG = Eas.LOG_TAG;
     84 
     85     private static final long SYNC_ERROR_BACKOFF_MILLIS =  DateUtils.MINUTE_IN_MILLIS;
     86 
     87     // Enable this to make pings get automatically renewed every hour. This
     88     // should not be needed, but if there is a software error that results in
     89     // the ping being lost, this is a fallback to make sure that messages are
     90     // not delayed more than an hour.
     91     private static final boolean SCHEDULE_KICK = true;
     92     private static final long KICK_SYNC_INTERVAL_SECONDS =
     93             DateUtils.HOUR_IN_MILLIS / DateUtils.SECOND_IN_MILLIS;
     94 
     95     /**
     96      * This class handles bookkeeping for a single account.
     97      */
     98     private class AccountSyncState {
     99         /** The currently running {@link PingTask}, or null if we aren't in the middle of a Ping. */
    100         private PingTask mPingTask;
    101 
    102         // Values for mPushEnabled.
    103         public static final int PUSH_UNKNOWN = 0;
    104         public static final int PUSH_ENABLED = 1;
    105         public static final int PUSH_DISABLED = 2;
    106 
    107         /**
    108          * Tracks whether this account wants to get push notifications, based on calls to
    109          * {@link #pushModify} and {@link #pushStop} (i.e. it tracks the last requested push state).
    110          */
    111         private int mPushEnabled;
    112 
    113         /**
    114          * The number of syncs that are blocked waiting for the current operation to complete.
    115          * Unlike Pings, sync operations do not start their own tasks and are assumed to run in
    116          * whatever thread calls into this class.
    117          */
    118         private int mSyncCount;
    119 
    120         /** The condition on which to block syncs that need to wait. */
    121         private Condition mCondition;
    122 
    123         /** The accountId for this accountState, used for logging */
    124         private long mAccountId;
    125 
    126         public AccountSyncState(final Lock lock, final long accountId) {
    127             mPingTask = null;
    128             // We don't yet have enough information to know whether or not push should be enabled.
    129             // We need to look up the account and it's folders, which won't yet exist for a newly
    130             // created account.
    131             mPushEnabled = PUSH_UNKNOWN;
    132             mSyncCount = 0;
    133             mCondition = lock.newCondition();
    134             mAccountId = accountId;
    135         }
    136 
    137         /**
    138          * Update bookkeeping for a new sync:
    139          * - Stop the Ping if there is one.
    140          * - Wait until there's nothing running for this account before proceeding.
    141          */
    142         public void syncStart() {
    143             ++mSyncCount;
    144             if (mPingTask != null) {
    145                 // Syncs are higher priority than Ping -- terminate the Ping.
    146                 LogUtils.i(TAG, "PSS Sync is pre-empting a ping acct:%d", mAccountId);
    147                 mPingTask.stop();
    148             }
    149             if (mPingTask != null || mSyncCount > 1) {
    150                 // Theres something we need to wait for before we can proceed.
    151                 try {
    152                     LogUtils.i(TAG, "PSS Sync needs to wait: Ping: %s, Pending tasks: %d acct: %d",
    153                             mPingTask != null ? "yes" : "no", mSyncCount, mAccountId);
    154                     mCondition.await();
    155                 } catch (final InterruptedException e) {
    156                     // TODO: Handle this properly. Not catching it might be the right answer.
    157                     LogUtils.i(TAG, "PSS InterruptedException acct:%d", mAccountId);
    158                 }
    159             }
    160         }
    161 
    162         /**
    163          * Update bookkeeping when a sync completes. This includes signaling pending ops to
    164          * go ahead, or starting the ping if appropriate and there are no waiting ops.
    165          * @return Whether this account is now idle.
    166          */
    167         public boolean syncEnd(final boolean lastSyncHadError, final Account account,
    168                                final PingSyncSynchronizer synchronizer) {
    169             --mSyncCount;
    170             if (mSyncCount > 0) {
    171                 LogUtils.i(TAG, "PSS Signalling a pending sync to proceed acct:%d.",
    172                         account.getId());
    173                 mCondition.signal();
    174                 return false;
    175             } else {
    176                 if (mPushEnabled == PUSH_UNKNOWN) {
    177                     LogUtils.i(TAG, "PSS push enabled is unknown");
    178                     mPushEnabled = (EasService.pingNeededForAccount(mService, account) ?
    179                             PUSH_ENABLED : PUSH_DISABLED);
    180                 }
    181                 if (mPushEnabled == PUSH_ENABLED) {
    182                     if (lastSyncHadError) {
    183                         LogUtils.i(TAG, "PSS last sync had error, scheduling delayed ping acct:%d.",
    184                                 account.getId());
    185                         scheduleDelayedPing(synchronizer.getContext(), account);
    186                         return true;
    187                     } else {
    188                         LogUtils.i(TAG, "PSS last sync succeeded, starting new ping acct:%d.",
    189                                 account.getId());
    190                         final android.accounts.Account amAccount =
    191                                 new android.accounts.Account(account.mEmailAddress,
    192                                         Eas.EXCHANGE_ACCOUNT_MANAGER_TYPE);
    193                         mPingTask = new PingTask(synchronizer.getContext(), account, amAccount,
    194                                 synchronizer);
    195                         mPingTask.start();
    196                         return false;
    197                     }
    198                 }
    199             }
    200             LogUtils.i(TAG, "PSS no push enabled acct:%d.", account.getId());
    201             return true;
    202         }
    203 
    204         /**
    205          * Update bookkeeping when the ping task terminates, including signaling any waiting ops.
    206          * @return Whether this account is now idle.
    207          */
    208         private boolean pingEnd(final android.accounts.Account amAccount) {
    209             mPingTask = null;
    210             if (mSyncCount > 0) {
    211                 LogUtils.i(TAG, "PSS pingEnd, syncs still in progress acct:%d.", mAccountId);
    212                 mCondition.signal();
    213                 return false;
    214             } else {
    215                 if (mPushEnabled == PUSH_ENABLED || mPushEnabled == PUSH_UNKNOWN) {
    216                     if (mPushEnabled == PUSH_UNKNOWN) {
    217                         // This should not occur. If mPushEnabled is unknown, we should not
    218                         // have started a ping. Still, we'd rather err on the side of restarting
    219                         // the ping, so log an error and request a new ping. Eventually we should
    220                         // do a sync, and then we'll correctly initialize mPushEnabled in
    221                         // syncEnd().
    222                         LogUtils.e(TAG, "PSS pingEnd, with mPushEnabled UNKNOWN?");
    223                     }
    224                     LogUtils.i(TAG, "PSS pingEnd, starting new ping acct:%d.", mAccountId);
    225                     /**
    226                      * This situation only arises if we encountered some sort of error that
    227                      * stopped our ping but not due to a sync interruption. In this scenario
    228                      * we'll leverage the SyncManager to request a push only sync that will
    229                      * restart the ping when the time is right. */
    230                     EasPing.requestPing(amAccount);
    231                     return false;
    232                 }
    233             }
    234             LogUtils.i(TAG, "PSS pingEnd, no longer need ping acct:%d.", mAccountId);
    235             return true;
    236         }
    237 
    238         private void scheduleDelayedPing(final Context context,
    239                                          final Account account) {
    240             LogUtils.i(TAG, "PSS Scheduling a delayed ping acct:%d.", account.getId());
    241             final Intent intent = new Intent(context, EasService.class);
    242             intent.setAction(Eas.EXCHANGE_SERVICE_INTENT_ACTION);
    243             intent.putExtra(EasService.EXTRA_START_PING, true);
    244             intent.putExtra(EasService.EXTRA_PING_ACCOUNT, account);
    245 
    246             final PendingIntent pi = PendingIntent.getService(context, 0, intent,
    247                     PendingIntent.FLAG_ONE_SHOT);
    248             final AlarmManager am = (AlarmManager)context.getSystemService(
    249                     Context.ALARM_SERVICE);
    250             final long atTime = SystemClock.elapsedRealtime() + SYNC_ERROR_BACKOFF_MILLIS;
    251             am.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, atTime, pi);
    252         }
    253 
    254         /**
    255          * Modifies or starts a ping for this account if no syncs are running.
    256          */
    257         public void pushModify(final Account account, final PingSyncSynchronizer synchronizer) {
    258             LogUtils.i(LogUtils.TAG, "PSS pushModify acct:%d", account.getId());
    259             mPushEnabled = PUSH_ENABLED;
    260             final android.accounts.Account amAccount =
    261                     new android.accounts.Account(account.mEmailAddress,
    262                             Eas.EXCHANGE_ACCOUNT_MANAGER_TYPE);
    263             if (mSyncCount == 0) {
    264                 if (mPingTask == null) {
    265                     // No ping, no running syncs -- start a new ping.
    266                     LogUtils.i(LogUtils.TAG, "PSS starting ping task acct:%d", account.getId());
    267                     mPingTask = new PingTask(synchronizer.getContext(), account, amAccount,
    268                             synchronizer);
    269                     mPingTask.start();
    270                 } else {
    271                     // Ping is already running, so tell it to restart to pick up any new params.
    272                     LogUtils.i(LogUtils.TAG, "PSS restarting ping task acct:%d", account.getId());
    273                     mPingTask.restart();
    274                 }
    275             } else {
    276                 LogUtils.i(LogUtils.TAG, "PSS syncs still in progress acct:%d", account.getId());
    277             }
    278             if (SCHEDULE_KICK) {
    279                 final Bundle extras = new Bundle(1);
    280                 extras.putBoolean(Mailbox.SYNC_EXTRA_PUSH_ONLY, true);
    281                 ContentResolver.addPeriodicSync(amAccount, EmailContent.AUTHORITY, extras,
    282                         KICK_SYNC_INTERVAL_SECONDS);
    283             }
    284         }
    285 
    286         /**
    287          * Stop the currently running ping.
    288          */
    289         public void pushStop() {
    290             LogUtils.i(LogUtils.TAG, "PSS pushStop acct:%d", mAccountId);
    291             mPushEnabled = PUSH_DISABLED;
    292             if (mPingTask != null) {
    293                 mPingTask.stop();
    294             }
    295         }
    296     }
    297 
    298     /**
    299      * Lock for access to {@link #mAccountStateMap}, also used to create the {@link Condition}s for
    300      * each Account.
    301      */
    302     private final ReentrantLock mLock;
    303 
    304     /**
    305      * Map from account ID -> {@link AccountSyncState} for accounts with a running operation.
    306      * An account is in this map only when this account is active, i.e. has a ping or sync running
    307      * or pending. If an account is not in the middle of a sync and is not configured for push,
    308      * it will not be here. This allows to use emptiness of this map to know whether the service
    309      * needs to be running, and is also handy when debugging.
    310      */
    311     private final LongSparseArray<AccountSyncState> mAccountStateMap;
    312 
    313     /** The {@link Service} that this object is managing. */
    314     private final Service mService;
    315 
    316     public PingSyncSynchronizer(final Service service) {
    317         mLock = new ReentrantLock();
    318         mAccountStateMap = new LongSparseArray<AccountSyncState>();
    319         mService = service;
    320     }
    321 
    322     public Context getContext() {
    323         return mService;
    324     }
    325 
    326     /**
    327      * Gets the {@link AccountSyncState} for an account.
    328      * The caller must hold {@link #mLock}.
    329      * @param accountId The id for the account we're interested in.
    330      * @param createIfNeeded If true, create the account state if it's not already there.
    331      * @return The {@link AccountSyncState} for that account, or null if the account is idle and
    332      *         createIfNeeded is false.
    333      */
    334     private AccountSyncState getAccountState(final long accountId, final boolean createIfNeeded) {
    335         assert mLock.isHeldByCurrentThread();
    336         AccountSyncState state = mAccountStateMap.get(accountId);
    337         if (state == null && createIfNeeded) {
    338             LogUtils.i(TAG, "PSS adding account state for acct:%d", accountId);
    339             state = new AccountSyncState(mLock, accountId);
    340             mAccountStateMap.put(accountId, state);
    341             // TODO: Is this too late to startService?
    342             if (mAccountStateMap.size() == 1) {
    343                 LogUtils.i(TAG, "PSS added first account, starting service");
    344                 mService.startService(new Intent(mService, mService.getClass()));
    345             }
    346         }
    347         return state;
    348     }
    349 
    350     /**
    351      * Remove an account from the map. If this was the last account, then also stop this service.
    352      * The caller must hold {@link #mLock}.
    353      * @param accountId The id for the account we're removing.
    354      */
    355     private void removeAccount(final long accountId) {
    356         assert mLock.isHeldByCurrentThread();
    357         LogUtils.i(TAG, "PSS removing account state for acct:%d", accountId);
    358         mAccountStateMap.delete(accountId);
    359         if (mAccountStateMap.size() == 0) {
    360             LogUtils.i(TAG, "PSS removed last account; stopping service.");
    361             mService.stopSelf();
    362         }
    363     }
    364 
    365     public void syncStart(final long accountId) {
    366         mLock.lock();
    367         try {
    368             LogUtils.i(TAG, "PSS syncStart for account acct:%d", accountId);
    369             final AccountSyncState accountState = getAccountState(accountId, true);
    370             accountState.syncStart();
    371         } finally {
    372             mLock.unlock();
    373         }
    374     }
    375 
    376     public void syncEnd(final boolean lastSyncHadError, final Account account) {
    377         mLock.lock();
    378         try {
    379             final long accountId = account.getId();
    380             LogUtils.i(TAG, "PSS syncEnd for account acct:%d", account.getId());
    381             final AccountSyncState accountState = getAccountState(accountId, false);
    382             if (accountState == null) {
    383                 LogUtils.w(TAG, "PSS syncEnd for account %d but no state found", accountId);
    384                 return;
    385             }
    386             if (accountState.syncEnd(lastSyncHadError, account, this)) {
    387                 removeAccount(accountId);
    388             }
    389         } finally {
    390             mLock.unlock();
    391         }
    392     }
    393 
    394     public void pingEnd(final long accountId, final android.accounts.Account amAccount) {
    395         mLock.lock();
    396         try {
    397             LogUtils.i(TAG, "PSS pingEnd for account %d", accountId);
    398             final AccountSyncState accountState = getAccountState(accountId, false);
    399             if (accountState == null) {
    400                 LogUtils.w(TAG, "PSS pingEnd for account %d but no state found", accountId);
    401                 return;
    402             }
    403             if (accountState.pingEnd(amAccount)) {
    404                 removeAccount(accountId);
    405             }
    406         } finally {
    407             mLock.unlock();
    408         }
    409     }
    410 
    411     public void pushModify(final Account account) {
    412         mLock.lock();
    413         try {
    414             final long accountId = account.getId();
    415             LogUtils.i(TAG, "PSS pushModify acct:%d", accountId);
    416             final AccountSyncState accountState = getAccountState(accountId, true);
    417             accountState.pushModify(account, this);
    418         } finally {
    419             mLock.unlock();
    420         }
    421     }
    422 
    423     public void pushStop(final long accountId) {
    424         mLock.lock();
    425         try {
    426             LogUtils.i(TAG, "PSS pushStop acct:%d", accountId);
    427             final AccountSyncState accountState = getAccountState(accountId, false);
    428             if (accountState != null) {
    429                 accountState.pushStop();
    430             }
    431         } finally {
    432             mLock.unlock();
    433         }
    434     }
    435 
    436     /**
    437      * Stops our service if our map contains no active accounts.
    438      */
    439     public void stopServiceIfIdle() {
    440         mLock.lock();
    441         try {
    442             LogUtils.i(TAG, "PSS stopIfIdle");
    443             if (mAccountStateMap.size() == 0) {
    444                 LogUtils.i(TAG, "PSS has no active accounts; stopping service.");
    445                 mService.stopSelf();
    446             }
    447         } finally {
    448             mLock.unlock();
    449         }
    450     }
    451 
    452     /**
    453      * Tells all running ping tasks to stop.
    454      */
    455     public void stopAllPings() {
    456         mLock.lock();
    457         try {
    458             for (int i = 0; i < mAccountStateMap.size(); ++i) {
    459                 mAccountStateMap.valueAt(i).pushStop();
    460             }
    461         } finally {
    462             mLock.unlock();
    463         }
    464     }
    465 }
    466