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     private static final String EXTRA_START_PING = "START_PING";
     87     private static final String EXTRA_PING_ACCOUNT = "PING_ACCOUNT";
     88 
     89     // Enable this to make pings get automatically renewed every hour. This
     90     // should not be needed, but if there is a software error that results in
     91     // the ping being lost, this is a fallback to make sure that messages are
     92     // not delayed more than an hour.
     93     private static final boolean SCHEDULE_KICK = false;
     94     private static final long KICK_SYNC_INTERVAL_SECONDS =
     95             DateUtils.HOUR_IN_MILLIS / DateUtils.SECOND_IN_MILLIS;
     96 
     97     /**
     98      * This class handles bookkeeping for a single account.
     99      */
    100     private static class AccountSyncState {
    101         /** The currently running {@link PingTask}, or null if we aren't in the middle of a Ping. */
    102         private PingTask mPingTask;
    103 
    104         /**
    105          * Tracks whether this account wants to get push notifications, based on calls to
    106          * {@link #pushModify} and {@link #pushStop} (i.e. it tracks the last requested push state).
    107          */
    108         private boolean mPushEnabled;
    109 
    110         /**
    111          * The number of syncs that are blocked waiting for the current operation to complete.
    112          * Unlike Pings, sync operations do not start their own tasks and are assumed to run in
    113          * whatever thread calls into this class.
    114          */
    115         private int mSyncCount;
    116 
    117         /** The condition on which to block syncs that need to wait. */
    118         private Condition mCondition;
    119 
    120         public AccountSyncState(final Lock lock ) {
    121             mPingTask = null;
    122             mPushEnabled = false;
    123             mSyncCount = 0;
    124             mCondition = lock.newCondition();
    125         }
    126 
    127         /**
    128          * Update bookkeeping for a new sync:
    129          * - Stop the Ping if there is one.
    130          * - Wait until there's nothing running for this account before proceeding.
    131          */
    132         public void syncStart() {
    133             ++mSyncCount;
    134             if (mPingTask != null) {
    135                 // Syncs are higher priority than Ping -- terminate the Ping.
    136                 LogUtils.d(TAG, "Sync is pre-empting a ping");
    137                 mPingTask.stop();
    138             }
    139             if (mPingTask != null || mSyncCount > 1) {
    140                 // Theres something we need to wait for before we can proceed.
    141                 try {
    142                     LogUtils.d(TAG, "Sync needs to wait: Ping: %s, Pending tasks: %d",
    143                             mPingTask != null ? "yes" : "no", mSyncCount);
    144                     mCondition.await();
    145                 } catch (final InterruptedException e) {
    146                     // TODO: Handle this properly. Not catching it might be the right answer.
    147                 }
    148             }
    149         }
    150 
    151         /**
    152          * Update bookkeeping when a sync completes. This includes signaling pending ops to
    153          * go ahead, or starting the ping if appropriate and there are no waiting ops.
    154          * @return Whether this account is now idle.
    155          */
    156         public boolean syncEnd(final boolean lastSyncHadError, final Account account,
    157                                final PingSyncSynchronizer synchronizer) {
    158             --mSyncCount;
    159             if (mSyncCount > 0) {
    160                 LogUtils.d(TAG, "Signalling a pending sync to proceed.");
    161                 mCondition.signal();
    162                 return false;
    163             } else {
    164                 if (mPushEnabled) {
    165                     if (lastSyncHadError) {
    166                         final android.accounts.Account amAccount =
    167                                 new android.accounts.Account(account.mEmailAddress,
    168                                     Eas.EXCHANGE_ACCOUNT_MANAGER_TYPE);
    169                         scheduleDelayedPing(synchronizer.getContext(), amAccount);
    170                         return true;
    171                     } else {
    172                         final android.accounts.Account amAccount =
    173                                 new android.accounts.Account(account.mEmailAddress,
    174                                         Eas.EXCHANGE_ACCOUNT_MANAGER_TYPE);
    175                         mPingTask = new PingTask(synchronizer.getContext(), account, amAccount,
    176                                 synchronizer);
    177                         mPingTask.start();
    178                         return false;
    179                     }
    180                 }
    181             }
    182             return true;
    183         }
    184 
    185         /**
    186          * Update bookkeeping when the ping task terminates, including signaling any waiting ops.
    187          * @return Whether this account is now idle.
    188          */
    189         private boolean pingEnd(final android.accounts.Account amAccount) {
    190             mPingTask = null;
    191             if (mSyncCount > 0) {
    192                 mCondition.signal();
    193                 return false;
    194             } else {
    195                 if (mPushEnabled) {
    196                     /**
    197                      * This situation only arises if we encountered some sort of error that
    198                      * stopped our ping but not due to a sync interruption. In this scenario
    199                      * we'll leverage the SyncManager to request a push only sync that will
    200                      * restart the ping when the time is right. */
    201                     EasPing.requestPing(amAccount);
    202                     return false;
    203                 }
    204             }
    205             return true;
    206         }
    207 
    208         private void scheduleDelayedPing(final Context context,
    209                                          final android.accounts.Account amAccount) {
    210             LogUtils.d(TAG, "Scheduling a delayed ping.");
    211             final Intent intent = new Intent(context, EmailSyncAdapterService.class);
    212             intent.setAction(Eas.EXCHANGE_SERVICE_INTENT_ACTION);
    213             intent.putExtra(EXTRA_START_PING, true);
    214             intent.putExtra(EXTRA_PING_ACCOUNT, amAccount);
    215             final PendingIntent pi = PendingIntent.getService(context, 0, intent,
    216                     PendingIntent.FLAG_ONE_SHOT);
    217             final AlarmManager am = (AlarmManager)context.getSystemService(
    218                     Context.ALARM_SERVICE);
    219             final long atTime = SystemClock.elapsedRealtime() + SYNC_ERROR_BACKOFF_MILLIS;
    220             am.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, atTime, pi);
    221         }
    222 
    223         /**
    224          * Modifies or starts a ping for this account if no syncs are running.
    225          */
    226         public void pushModify(final Account account, final PingSyncSynchronizer synchronizer) {
    227             mPushEnabled = true;
    228             final android.accounts.Account amAccount =
    229                     new android.accounts.Account(account.mEmailAddress,
    230                             Eas.EXCHANGE_ACCOUNT_MANAGER_TYPE);
    231             if (mSyncCount == 0) {
    232                 if (mPingTask == null) {
    233                     // No ping, no running syncs -- start a new ping.
    234                     mPingTask = new PingTask(synchronizer.getContext(), account, amAccount,
    235                             synchronizer);
    236                     mPingTask.start();
    237                 } else {
    238                     // Ping is already running, so tell it to restart to pick up any new params.
    239                     mPingTask.restart();
    240                 }
    241             }
    242             if (SCHEDULE_KICK) {
    243                 final Bundle extras = new Bundle(1);
    244                 extras.putBoolean(Mailbox.SYNC_EXTRA_PUSH_ONLY, true);
    245                 ContentResolver.addPeriodicSync(amAccount, EmailContent.AUTHORITY, extras,
    246                         KICK_SYNC_INTERVAL_SECONDS);
    247             }
    248         }
    249 
    250         /**
    251          * Stop the currently running ping.
    252          */
    253         public void pushStop() {
    254             mPushEnabled = false;
    255             if (mPingTask != null) {
    256                 mPingTask.stop();
    257             }
    258         }
    259     }
    260 
    261     /**
    262      * Lock for access to {@link #mAccountStateMap}, also used to create the {@link Condition}s for
    263      * each Account.
    264      */
    265     private final ReentrantLock mLock;
    266 
    267     /**
    268      * Map from account ID -> {@link AccountSyncState} for accounts with a running operation.
    269      * An account is in this map only when this account is active, i.e. has a ping or sync running
    270      * or pending. If an account is not in the middle of a sync and is not configured for push,
    271      * it will not be here. This allows to use emptiness of this map to know whether the service
    272      * needs to be running, and is also handy when debugging.
    273      */
    274     private final LongSparseArray<AccountSyncState> mAccountStateMap;
    275 
    276     /** The {@link Service} that this object is managing. */
    277     private final Service mService;
    278 
    279     public PingSyncSynchronizer(final Service service) {
    280         mLock = new ReentrantLock();
    281         mAccountStateMap = new LongSparseArray<AccountSyncState>();
    282         mService = service;
    283     }
    284 
    285     public Context getContext() {
    286         return mService;
    287     }
    288 
    289     /**
    290      * Gets the {@link AccountSyncState} for an account.
    291      * The caller must hold {@link #mLock}.
    292      * @param accountId The id for the account we're interested in.
    293      * @param createIfNeeded If true, create the account state if it's not already there.
    294      * @return The {@link AccountSyncState} for that account, or null if the account is idle and
    295      *         createIfNeeded is false.
    296      */
    297     private AccountSyncState getAccountState(final long accountId, final boolean createIfNeeded) {
    298         assert mLock.isHeldByCurrentThread();
    299         AccountSyncState state = mAccountStateMap.get(accountId);
    300         if (state == null && createIfNeeded) {
    301             LogUtils.d(TAG, "PSS adding account state for %d", accountId);
    302             state = new AccountSyncState(mLock);
    303             mAccountStateMap.put(accountId, state);
    304             // TODO: Is this too late to startService?
    305             if (mAccountStateMap.size() == 1) {
    306                 LogUtils.i(TAG, "PSS added first account, starting service");
    307                 mService.startService(new Intent(mService, mService.getClass()));
    308             }
    309         }
    310         return state;
    311     }
    312 
    313     /**
    314      * Remove an account from the map. If this was the last account, then also stop this service.
    315      * The caller must hold {@link #mLock}.
    316      * @param accountId The id for the account we're removing.
    317      */
    318     private void removeAccount(final long accountId) {
    319         assert mLock.isHeldByCurrentThread();
    320         LogUtils.d(TAG, "PSS removing account state for %d", accountId);
    321         mAccountStateMap.delete(accountId);
    322         if (mAccountStateMap.size() == 0) {
    323             LogUtils.i(TAG, "PSS removed last account; stopping service.");
    324             mService.stopSelf();
    325         }
    326     }
    327 
    328     public void syncStart(final long accountId) {
    329         mLock.lock();
    330         try {
    331             LogUtils.d(TAG, "PSS syncStart for account %d", accountId);
    332             final AccountSyncState accountState = getAccountState(accountId, true);
    333             accountState.syncStart();
    334         } finally {
    335             mLock.unlock();
    336         }
    337     }
    338 
    339     public void syncEnd(final boolean lastSyncHadError, final Account account) {
    340         mLock.lock();
    341         try {
    342             final long accountId = account.getId();
    343             LogUtils.d(TAG, "PSS syncEnd for account %d", account.getId());
    344             final AccountSyncState accountState = getAccountState(accountId, false);
    345             if (accountState == null) {
    346                 LogUtils.w(TAG, "PSS syncEnd for account %d but no state found", accountId);
    347                 return;
    348             }
    349             if (accountState.syncEnd(lastSyncHadError, account, this)) {
    350                 removeAccount(accountId);
    351             }
    352         } finally {
    353             mLock.unlock();
    354         }
    355     }
    356 
    357     public void pingEnd(final long accountId, final android.accounts.Account amAccount) {
    358         mLock.lock();
    359         try {
    360             LogUtils.d(TAG, "PSS pingEnd for account %d", accountId);
    361             final AccountSyncState accountState = getAccountState(accountId, false);
    362             if (accountState == null) {
    363                 LogUtils.w(TAG, "PSS pingEnd for account %d but no state found", accountId);
    364                 return;
    365             }
    366             if (accountState.pingEnd(amAccount)) {
    367                 removeAccount(accountId);
    368             }
    369         } finally {
    370             mLock.unlock();
    371         }
    372     }
    373 
    374     public void pushModify(final Account account) {
    375         mLock.lock();
    376         try {
    377             final long accountId = account.getId();
    378             LogUtils.d(TAG, "PSS pushModify for account %d", accountId);
    379             final AccountSyncState accountState = getAccountState(accountId, true);
    380             accountState.pushModify(account, this);
    381         } finally {
    382             mLock.unlock();
    383         }
    384     }
    385 
    386     public void pushStop(final long accountId) {
    387         mLock.lock();
    388         try {
    389             LogUtils.d(TAG, "PSS pushStop for account %d", accountId);
    390             final AccountSyncState accountState = getAccountState(accountId, false);
    391             if (accountState != null) {
    392                 accountState.pushStop();
    393             }
    394         } finally {
    395             mLock.unlock();
    396         }
    397     }
    398 
    399     /**
    400      * Stops our service if our map contains no active accounts.
    401      */
    402     public void stopServiceIfIdle() {
    403         mLock.lock();
    404         try {
    405             LogUtils.d(TAG, "PSS stopIfIdle");
    406             if (mAccountStateMap.size() == 0) {
    407                 LogUtils.i(TAG, "PSS has no active accounts; stopping service.");
    408                 mService.stopSelf();
    409             }
    410         } finally {
    411             mLock.unlock();
    412         }
    413     }
    414 
    415     /**
    416      * Tells all running ping tasks to stop.
    417      */
    418     public void stopAllPings() {
    419         mLock.lock();
    420         try {
    421             for (int i = 0; i < mAccountStateMap.size(); ++i) {
    422                 mAccountStateMap.valueAt(i).pushStop();
    423             }
    424         } finally {
    425             mLock.unlock();
    426         }
    427     }
    428 }
    429