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 17package com.android.exchange.service; 18 19import android.app.AlarmManager; 20import android.app.PendingIntent; 21import android.app.Service; 22import android.content.ContentResolver; 23import android.content.Context; 24import android.content.Intent; 25import android.os.Bundle; 26import android.os.SystemClock; 27import android.support.v4.util.LongSparseArray; 28import android.text.format.DateUtils; 29 30import com.android.emailcommon.provider.Account; 31import com.android.emailcommon.provider.EmailContent; 32import com.android.emailcommon.provider.Mailbox; 33import com.android.exchange.Eas; 34import com.android.exchange.eas.EasPing; 35import com.android.mail.utils.LogUtils; 36 37import java.util.concurrent.locks.Condition; 38import java.util.concurrent.locks.Lock; 39import 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 */ 81public 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 // There’s 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