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.ContentResolver; 21 import android.content.Intent; 22 import android.database.Cursor; 23 import android.os.AsyncTask; 24 import android.os.Bundle; 25 import android.os.IBinder; 26 import android.provider.CalendarContract; 27 import android.provider.ContactsContract; 28 import android.text.TextUtils; 29 30 import com.android.emailcommon.provider.Account; 31 import com.android.emailcommon.provider.EmailContent; 32 import com.android.emailcommon.provider.HostAuth; 33 import com.android.emailcommon.provider.Mailbox; 34 import com.android.emailcommon.service.IEmailService; 35 import com.android.emailcommon.service.IEmailServiceCallback; 36 import com.android.emailcommon.service.SearchParams; 37 import com.android.emailcommon.service.ServiceProxy; 38 import com.android.exchange.Eas; 39 import com.android.exchange.eas.EasFolderSync; 40 import com.android.exchange.eas.EasLoadAttachment; 41 import com.android.exchange.eas.EasOperation; 42 import com.android.exchange.eas.EasSearch; 43 import com.android.mail.utils.LogUtils; 44 45 import java.util.HashSet; 46 import java.util.Set; 47 48 /** 49 * Service to handle all communication with the EAS server. Note that this is completely decoupled 50 * from the sync adapters; sync adapters should make blocking calls on this service to actually 51 * perform any operations. 52 */ 53 public class EasService extends Service { 54 55 private static final String TAG = Eas.LOG_TAG; 56 57 /** 58 * The content authorities that can be synced for EAS accounts. Initialization must wait until 59 * after we have a chance to call {@link EmailContent#init} (and, for future content types, 60 * possibly other initializations) because that's how we can know what the email authority is. 61 */ 62 private static String[] AUTHORITIES_TO_SYNC; 63 64 /** Bookkeeping for ping tasks & sync threads management. */ 65 private final PingSyncSynchronizer mSynchronizer; 66 67 /** 68 * Implementation of the IEmailService interface. 69 * For the most part these calls should consist of creating the correct {@link EasOperation} 70 * class and calling {@link #doOperation} with it. 71 */ 72 private final IEmailService.Stub mBinder = new IEmailService.Stub() { 73 @Override 74 public void sendMail(final long accountId) { 75 LogUtils.d(TAG, "IEmailService.sendMail: %d", accountId); 76 } 77 78 @Override 79 public void loadAttachment(final IEmailServiceCallback callback, final long accountId, 80 final long attachmentId, final boolean background) { 81 LogUtils.d(TAG, "IEmailService.loadAttachment: %d", attachmentId); 82 final EasLoadAttachment operation = new EasLoadAttachment(EasService.this, accountId, 83 attachmentId, callback); 84 doOperation(operation, "IEmailService.loadAttachment"); 85 } 86 87 @Override 88 public void updateFolderList(final long accountId) { 89 final EasFolderSync operation = new EasFolderSync(EasService.this, accountId); 90 doOperation(operation, "IEmailService.updateFolderList"); 91 } 92 93 @Override 94 public void sync(final long accountId, final boolean updateFolderList, 95 final int mailboxType, final long[] folders) {} 96 97 @Override 98 public void pushModify(final long accountId) { 99 LogUtils.d(TAG, "IEmailService.pushModify: %d", accountId); 100 final Account account = Account.restoreAccountWithId(EasService.this, accountId); 101 if (pingNeededForAccount(account)) { 102 mSynchronizer.pushModify(account); 103 } else { 104 mSynchronizer.pushStop(accountId); 105 } 106 } 107 108 @Override 109 public Bundle validate(final HostAuth hostAuth) { 110 final EasFolderSync operation = new EasFolderSync(EasService.this, hostAuth); 111 doOperation(operation, "IEmailService.validate"); 112 return operation.getValidationResult(); 113 } 114 115 @Override 116 public int searchMessages(final long accountId, final SearchParams searchParams, 117 final long destMailboxId) { 118 final EasSearch operation = new EasSearch(EasService.this, accountId, searchParams, 119 destMailboxId); 120 doOperation(operation, "IEmailService.searchMessages"); 121 return operation.getTotalResults(); 122 } 123 124 @Override 125 public void sendMeetingResponse(final long messageId, final int response) { 126 LogUtils.d(TAG, "IEmailService.sendMeetingResponse: %d, %d", messageId, response); 127 } 128 129 @Override 130 public Bundle autoDiscover(final String username, final String password) { 131 LogUtils.d(TAG, "IEmailService.autoDiscover"); 132 return null; 133 } 134 135 @Override 136 public void setLogging(final int flags) { 137 LogUtils.d(TAG, "IEmailService.setLogging"); 138 } 139 140 @Override 141 public void deleteAccountPIMData(final String emailAddress) { 142 LogUtils.d(TAG, "IEmailService.deleteAccountPIMData"); 143 } 144 }; 145 146 /** 147 * Content selection string for getting all accounts that are configured for push. 148 * TODO: Add protocol check so that we don't get e.g. IMAP accounts here. 149 * (Not currently necessary but eventually will be.) 150 */ 151 private static final String PUSH_ACCOUNTS_SELECTION = 152 EmailContent.AccountColumns.SYNC_INTERVAL + 153 "=" + Integer.toString(Account.CHECK_INTERVAL_PUSH); 154 155 /** {@link AsyncTask} to restart pings for all accounts that need it. */ 156 private class RestartPingsTask extends AsyncTask<Void, Void, Void> { 157 private boolean mHasRestartedPing = false; 158 159 @Override 160 protected Void doInBackground(Void... params) { 161 final Cursor c = EasService.this.getContentResolver().query(Account.CONTENT_URI, 162 Account.CONTENT_PROJECTION, PUSH_ACCOUNTS_SELECTION, null, null); 163 if (c != null) { 164 try { 165 while (c.moveToNext()) { 166 final Account account = new Account(); 167 account.restore(c); 168 if (EasService.this.pingNeededForAccount(account)) { 169 mHasRestartedPing = true; 170 EasService.this.mSynchronizer.pushModify(account); 171 } 172 } 173 } finally { 174 c.close(); 175 } 176 } 177 return null; 178 } 179 180 @Override 181 protected void onPostExecute(Void result) { 182 if (!mHasRestartedPing) { 183 LogUtils.d(TAG, "RestartPingsTask did not start any pings."); 184 EasService.this.mSynchronizer.stopServiceIfIdle(); 185 } 186 } 187 } 188 189 public EasService() { 190 super(); 191 mSynchronizer = new PingSyncSynchronizer(this); 192 } 193 194 @Override 195 public void onCreate() { 196 super.onCreate(); 197 EmailContent.init(this); 198 AUTHORITIES_TO_SYNC = new String[] { 199 EmailContent.AUTHORITY, 200 CalendarContract.AUTHORITY, 201 ContactsContract.AUTHORITY 202 }; 203 204 // Restart push for all accounts that need it. Because this requires DB loads, we do it in 205 // an AsyncTask, and we startService to ensure that we stick around long enough for the 206 // task to complete. The task will stop the service if necessary after it's done. 207 startService(new Intent(this, EasService.class)); 208 new RestartPingsTask().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); 209 } 210 211 @Override 212 public void onDestroy() { 213 mSynchronizer.stopAllPings(); 214 } 215 216 @Override 217 public IBinder onBind(final Intent intent) { 218 return mBinder; 219 } 220 221 @Override 222 public int onStartCommand(final Intent intent, final int flags, final int startId) { 223 if (intent != null && 224 TextUtils.equals(Eas.EXCHANGE_SERVICE_INTENT_ACTION, intent.getAction())) { 225 if (intent.getBooleanExtra(ServiceProxy.EXTRA_FORCE_SHUTDOWN, false)) { 226 // We've been asked to forcibly shutdown. This happens if email accounts are 227 // deleted, otherwise we can get errors if services are still running for 228 // accounts that are now gone. 229 // TODO: This is kind of a hack, it would be nicer if we could handle it correctly 230 // if accounts disappear out from under us. 231 LogUtils.d(TAG, "Forced shutdown, killing process"); 232 System.exit(-1); 233 } 234 } 235 return START_STICKY; 236 } 237 238 public int doOperation(final EasOperation operation, final String loggingName) { 239 final long accountId = operation.getAccountId(); 240 final Account account = operation.getAccount(); 241 LogUtils.d(TAG, "%s: %d", loggingName, accountId); 242 mSynchronizer.syncStart(accountId); 243 // TODO: Do we need a wakelock here? For RPC coming from sync adapters, no -- the SA 244 // already has one. But for others, maybe? Not sure what's guaranteed for AIDL calls. 245 // If we add a wakelock (or anything else for that matter) here, must remember to undo 246 // it in the finally block below. 247 // On the other hand, even for SAs, it doesn't hurt to get a wakelock here. 248 try { 249 return operation.performOperation(); 250 } finally { 251 mSynchronizer.syncEnd(account); 252 } 253 } 254 255 /** 256 * Determine whether this account is configured with folders that are ready for push 257 * notifications. 258 * @param account The {@link Account} that we're interested in. 259 * @return Whether this account needs to ping. 260 */ 261 public boolean pingNeededForAccount(final Account account) { 262 // Check account existence. 263 if (account == null || account.mId == Account.NO_ACCOUNT) { 264 LogUtils.d(TAG, "Do not ping: Account not found or not valid"); 265 return false; 266 } 267 268 // Check if account is configured for a push sync interval. 269 if (account.mSyncInterval != Account.CHECK_INTERVAL_PUSH) { 270 LogUtils.d(TAG, "Do not ping: Account %d not configured for push", account.mId); 271 return false; 272 } 273 274 // Check security hold status of the account. 275 if ((account.mFlags & Account.FLAGS_SECURITY_HOLD) != 0) { 276 LogUtils.d(TAG, "Do not ping: Account %d is on security hold", account.mId); 277 return false; 278 } 279 280 // Check if the account has performed at least one sync so far (accounts must perform 281 // the initial sync before push is possible). 282 if (EmailContent.isInitialSyncKey(account.mSyncKey)) { 283 LogUtils.d(TAG, "Do not ping: Account %d has not done initial sync", account.mId); 284 return false; 285 } 286 287 // Check that there's at least one mailbox that is both configured for push notifications, 288 // and whose content type is enabled for sync in the account manager. 289 final android.accounts.Account amAccount = new android.accounts.Account( 290 account.mEmailAddress, Eas.EXCHANGE_ACCOUNT_MANAGER_TYPE); 291 292 final Set<String> authsToSync = getAuthoritiesToSync(amAccount, AUTHORITIES_TO_SYNC); 293 // If we have at least one sync-enabled content type, check for syncing mailboxes. 294 if (!authsToSync.isEmpty()) { 295 final Cursor c = Mailbox.getMailboxesForPush(getContentResolver(), account.mId); 296 if (c != null) { 297 try { 298 while (c.moveToNext()) { 299 final int mailboxType = c.getInt(Mailbox.CONTENT_TYPE_COLUMN); 300 if (authsToSync.contains(Mailbox.getAuthority(mailboxType))) { 301 return true; 302 } 303 } 304 } finally { 305 c.close(); 306 } 307 } 308 } 309 LogUtils.d(TAG, "Do not ping: Account %d has no folders configured for push", account.mId); 310 return false; 311 } 312 313 /** 314 * Determine which content types are set to sync for an account. 315 * @param account The account whose sync settings we're looking for. 316 * @param authorities All possible authorities we could care about. 317 * @return The authorities for the content types we want to sync for account. 318 */ 319 private static Set<String> getAuthoritiesToSync(final android.accounts.Account account, 320 final String[] authorities) { 321 final HashSet<String> authsToSync = new HashSet(); 322 for (final String authority : authorities) { 323 if (ContentResolver.getSyncAutomatically(account, authority)) { 324 authsToSync.add(authority); 325 } 326 } 327 return authsToSync; 328 } 329 } 330