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 AccountSyncState(final Lock lock, final long accountId)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 */ syncStart()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 // There’s 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 */ syncEnd(final boolean lastSyncHadError, final Account account, final PingSyncSynchronizer synchronizer)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 */ pingEnd(final android.accounts.Account amAccount)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 scheduleDelayedPing(final Context context, final Account account)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 */ pushModify(final Account account, final PingSyncSynchronizer synchronizer)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 */ pushStop()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 PingSyncSynchronizer(final Service service)316 public PingSyncSynchronizer(final Service service) { 317 mLock = new ReentrantLock(); 318 mAccountStateMap = new LongSparseArray<AccountSyncState>(); 319 mService = service; 320 } 321 getContext()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 */ getAccountState(final long accountId, final boolean createIfNeeded)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 */ removeAccount(final long accountId)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 syncStart(final long accountId)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 syncEnd(final boolean lastSyncHadError, final Account account)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 pingEnd(final long accountId, final android.accounts.Account amAccount)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 pushModify(final Account account)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 pushStop(final long accountId)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 */ stopServiceIfIdle()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 */ stopAllPings()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