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