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