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.Context; 22 import android.content.Intent; 23 import android.content.SharedPreferences; 24 import android.database.Cursor; 25 import android.os.AsyncTask; 26 import android.os.Bundle; 27 import android.os.IBinder; 28 import android.provider.CalendarContract; 29 import android.provider.ContactsContract; 30 import android.text.TextUtils; 31 32 import com.android.emailcommon.TempDirectory; 33 import com.android.emailcommon.provider.Account; 34 import com.android.emailcommon.provider.EmailContent; 35 import com.android.emailcommon.provider.HostAuth; 36 import com.android.emailcommon.provider.Mailbox; 37 import com.android.emailcommon.service.EmailServiceProxy; 38 import com.android.emailcommon.service.EmailServiceStatus; 39 import com.android.emailcommon.service.EmailServiceVersion; 40 import com.android.emailcommon.service.HostAuthCompat; 41 import com.android.emailcommon.service.IEmailService; 42 import com.android.emailcommon.service.IEmailServiceCallback; 43 import com.android.emailcommon.service.SearchParams; 44 import com.android.emailcommon.service.ServiceProxy; 45 import com.android.exchange.Eas; 46 import com.android.exchange.eas.EasAutoDiscover; 47 import com.android.exchange.eas.EasFolderSync; 48 import com.android.exchange.eas.EasFullSyncOperation; 49 import com.android.exchange.eas.EasLoadAttachment; 50 import com.android.exchange.eas.EasOperation; 51 import com.android.exchange.eas.EasPing; 52 import com.android.exchange.eas.EasSearch; 53 import com.android.exchange.eas.EasSearchGal; 54 import com.android.exchange.eas.EasSendMeetingResponse; 55 import com.android.exchange.eas.EasSyncCalendar; 56 import com.android.exchange.eas.EasSyncContacts; 57 import com.android.exchange.provider.GalResult; 58 import com.android.mail.utils.LogUtils; 59 import com.google.common.annotations.VisibleForTesting; 60 61 import java.util.HashSet; 62 import java.util.Set; 63 64 /** 65 * Service to handle all communication with the EAS server. Note that this is completely decoupled 66 * from the sync adapters; sync adapters should make blocking calls on this service to actually 67 * perform any operations. 68 */ 69 public class EasService extends Service { 70 71 private static final String TAG = Eas.LOG_TAG; 72 73 private static final String PREFERENCES_FILE = "ExchangePrefs"; 74 private static final String PROTOCOL_LOGGING_PREF = "ProtocolLogging"; 75 private static final String FILE_LOGGING_PREF = "FileLogging"; 76 77 public static final String EXTRA_START_PING = "START_PING"; 78 public static final String EXTRA_PING_ACCOUNT = "PING_ACCOUNT"; 79 80 /** 81 * The content authorities that can be synced for EAS accounts. Initialization must wait until 82 * after we have a chance to call {@link EmailContent#init} (and, for future content types, 83 * possibly other initializations) because that's how we can know what the email authority is. 84 */ 85 private static String[] AUTHORITIES_TO_SYNC; 86 87 /** Bookkeeping for ping tasks & sync threads management. */ 88 private final PingSyncSynchronizer mSynchronizer; 89 90 private static boolean sProtocolLogging; 91 private static boolean sFileLogging; 92 93 /** 94 * Implementation of the IEmailService interface. 95 * For the most part these calls should consist of creating the correct {@link EasOperation} 96 * class and calling {@link #doOperation} with it. 97 */ 98 private final IEmailService.Stub mBinder = new IEmailService.Stub() { 99 @Override 100 public void loadAttachment(final IEmailServiceCallback callback, final long accountId, 101 final long attachmentId, final boolean background) { 102 LogUtils.d(TAG, "IEmailService.loadAttachment: %d", attachmentId); 103 final Account account = loadAccount(EasService.this, accountId); 104 if (account != null) { 105 final EasLoadAttachment operation = new EasLoadAttachment(EasService.this, account, 106 attachmentId, callback); 107 doOperation(operation, "IEmailService.loadAttachment"); 108 } 109 } 110 111 @Override 112 public void updateFolderList(final long accountId) { 113 LogUtils.d(TAG, "IEmailService.updateFolderList: %d", accountId); 114 final Account account = loadAccount(EasService.this, accountId); 115 if (account != null) { 116 final EasFolderSync operation = new EasFolderSync(EasService.this, account); 117 doOperation(operation, "IEmailService.updateFolderList"); 118 } 119 } 120 121 public void sendMail(final long accountId) { 122 // TODO: We should get rid of sendMail, and this is done in sync. 123 LogUtils.wtf(TAG, "unexpected call to EasService.sendMail"); 124 } 125 126 public int sync(final long accountId, Bundle syncExtras) { 127 LogUtils.d(TAG, "IEmailService.updateFolderList: %d", accountId); 128 final Account account = loadAccount(EasService.this, accountId); 129 if (account != null) { 130 EasFullSyncOperation op = new EasFullSyncOperation(EasService.this, account, 131 syncExtras); 132 final int result = doOperation(op, "IEmailService.sync"); 133 if (result == EasFullSyncOperation.RESULT_SECURITY_HOLD) { 134 LogUtils.i(LogUtils.TAG, "Security Hold trying to sync"); 135 return EmailServiceStatus.INTERNAL_ERROR; 136 } 137 return convertToEmailServiceStatus(result); 138 } else { 139 return EmailServiceStatus.INTERNAL_ERROR; 140 } 141 } 142 143 @Override 144 public void pushModify(final long accountId) { 145 LogUtils.d(TAG, "IEmailService.pushModify: %d", accountId); 146 final Account account = Account.restoreAccountWithId(EasService.this, accountId); 147 if (pingNeededForAccount(EasService.this, account)) { 148 mSynchronizer.pushModify(account); 149 } else { 150 mSynchronizer.pushStop(accountId); 151 } 152 } 153 154 @Override 155 public Bundle validate(final HostAuthCompat hostAuthCom) { 156 LogUtils.d(TAG, "IEmailService.validate"); 157 final HostAuth hostAuth = hostAuthCom.toHostAuth(); 158 final EasFolderSync operation = new EasFolderSync(EasService.this, hostAuth); 159 doOperation(operation, "IEmailService.validate"); 160 return operation.getValidationResult(); 161 } 162 163 @Override 164 public int searchMessages(final long accountId, final SearchParams searchParams, 165 final long destMailboxId) { 166 LogUtils.d(TAG, "IEmailService.searchMessages"); 167 final Account account = loadAccount(EasService.this, accountId); 168 if (account != null) { 169 final EasSearch operation = new EasSearch(EasService.this, account, searchParams, 170 destMailboxId); 171 doOperation(operation, "IEmailService.searchMessages"); 172 return operation.getTotalResults(); 173 } else { 174 return 0; 175 } 176 } 177 178 @Override 179 public void sendMeetingResponse(final long messageId, final int response) { 180 EmailContent.Message msg = EmailContent.Message.restoreMessageWithId(EasService.this, 181 messageId); 182 LogUtils.d(TAG, "IEmailService.sendMeetingResponse"); 183 if (msg == null) { 184 LogUtils.e(TAG, "Could not load message %d in sendMeetingResponse", messageId); 185 return; 186 } 187 final Account account = loadAccount(EasService.this, msg.mAccountKey); 188 if (account != null) { 189 final EasSendMeetingResponse operation = new EasSendMeetingResponse(EasService.this, 190 account, msg, response); 191 doOperation(operation, "IEmailService.sendMeetingResponse"); 192 } 193 } 194 195 @Override 196 public Bundle autoDiscover(final String username, final String password) { 197 final String domain = EasAutoDiscover.getDomain(username); 198 for (int attempt = 0; attempt <= EasAutoDiscover.ATTEMPT_MAX; attempt++) { 199 LogUtils.d(TAG, "autodiscover attempt %d", attempt); 200 final String uri = EasAutoDiscover.genUri(domain, attempt); 201 Bundle result = autoDiscoverInternal(uri, attempt, username, password, true); 202 int resultCode = result.getInt(EmailServiceProxy.AUTO_DISCOVER_BUNDLE_ERROR_CODE); 203 if (resultCode != EasAutoDiscover.RESULT_BAD_RESPONSE) { 204 return result; 205 } else { 206 LogUtils.d(TAG, "got BAD_RESPONSE"); 207 } 208 } 209 return null; 210 } 211 212 private Bundle autoDiscoverInternal(final String uri, final int attempt, 213 final String username, final String password, 214 final boolean canRetry) { 215 final EasAutoDiscover op = new EasAutoDiscover(EasService.this, uri, attempt, 216 username, password); 217 final int result = op.performOperation(); 218 if (result == EasAutoDiscover.RESULT_REDIRECT) { 219 // Try again recursively with the new uri. TODO we should limit the number of redirects. 220 final String redirectUri = op.getRedirectUri(); 221 return autoDiscoverInternal(redirectUri, attempt, username, password, canRetry); 222 } else if (result == EasAutoDiscover.RESULT_SC_UNAUTHORIZED) { 223 if (canRetry && username.contains("@")) { 224 // Try again using the bare user name 225 final int atSignIndex = username.indexOf('@'); 226 final String bareUsername = username.substring(0, atSignIndex); 227 LogUtils.d(TAG, "%d received; trying username: %s", result, atSignIndex); 228 // Try again recursively, but this time don't allow retries for username. 229 return autoDiscoverInternal(uri, attempt, bareUsername, password, false); 230 } else { 231 // Either we're already on our second try or the username didn't have an "@" 232 // to begin with. Either way, failure. 233 final Bundle bundle = new Bundle(1); 234 bundle.putInt(EmailServiceProxy.AUTO_DISCOVER_BUNDLE_ERROR_CODE, 235 EasAutoDiscover.RESULT_OTHER_FAILURE); 236 return bundle; 237 } 238 } else if (result != EasAutoDiscover.RESULT_OK) { 239 // Return failure, we'll try again with an alternate address 240 final Bundle bundle = new Bundle(1); 241 bundle.putInt(EmailServiceProxy.AUTO_DISCOVER_BUNDLE_ERROR_CODE, 242 EasAutoDiscover.RESULT_BAD_RESPONSE); 243 return bundle; 244 } 245 // Success. 246 return op.getResultBundle(); 247 } 248 249 @Override 250 public void setLogging(final int flags) { 251 sProtocolLogging = ((flags & EmailServiceProxy.DEBUG_EXCHANGE_BIT) != 0); 252 sFileLogging = ((flags & EmailServiceProxy.DEBUG_FILE_BIT) != 0); 253 SharedPreferences sharedPrefs = EasService.this.getSharedPreferences(PREFERENCES_FILE, 254 Context.MODE_PRIVATE); 255 sharedPrefs.edit().putBoolean(PROTOCOL_LOGGING_PREF, sProtocolLogging).apply(); 256 sharedPrefs.edit().putBoolean(FILE_LOGGING_PREF, sFileLogging).apply(); 257 LogUtils.d(TAG, "IEmailService.setLogging %d, storing to shared pref", flags); 258 } 259 260 @Override 261 public void deleteExternalAccountPIMData(final String emailAddress) { 262 LogUtils.d(TAG, "IEmailService.deleteAccountPIMData"); 263 if (emailAddress != null) { 264 // TODO: stop pings 265 final Context context = EasService.this; 266 EasSyncContacts.wipeAccountFromContentProvider(context, emailAddress); 267 EasSyncCalendar.wipeAccountFromContentProvider(context, emailAddress); 268 } 269 } 270 271 public int getApiVersion() { 272 return EmailServiceVersion.CURRENT; 273 } 274 }; 275 loadAccount(final Context context, final long accountId)276 private static Account loadAccount(final Context context, final long accountId) { 277 Account account = Account.restoreAccountWithId(context, accountId); 278 if (account == null) { 279 LogUtils.e(TAG, "Could not load account %d", accountId); 280 } 281 return account; 282 } 283 284 /** 285 * Content selection string for getting all accounts that are configured for push. 286 * TODO: Add protocol check so that we don't get e.g. IMAP accounts here. 287 * (Not currently necessary but eventually will be.) 288 */ 289 private static final String PUSH_ACCOUNTS_SELECTION = 290 EmailContent.AccountColumns.SYNC_INTERVAL + 291 "=" + Integer.toString(Account.CHECK_INTERVAL_PUSH); 292 293 /** {@link AsyncTask} to restart pings for all accounts that need it. */ 294 private class RestartPingsTask extends AsyncTask<Void, Void, Void> { 295 private boolean mHasRestartedPing = false; 296 297 @Override doInBackground(Void... params)298 protected Void doInBackground(Void... params) { 299 LogUtils.i(TAG, "RestartPingTask"); 300 final Cursor c = EasService.this.getContentResolver().query(Account.CONTENT_URI, 301 Account.CONTENT_PROJECTION, PUSH_ACCOUNTS_SELECTION, null, null); 302 if (c != null) { 303 try { 304 while (c.moveToNext()) { 305 final Account account = new Account(); 306 account.restore(c); 307 LogUtils.i(TAG, "RestartPingsTask starting ping for %d", account.getId()); 308 if (pingNeededForAccount(EasService.this, account)) { 309 mHasRestartedPing = true; 310 EasService.this.mSynchronizer.pushModify(account); 311 } 312 } 313 } finally { 314 c.close(); 315 } 316 } 317 return null; 318 } 319 320 @Override onPostExecute(Void result)321 protected void onPostExecute(Void result) { 322 if (!mHasRestartedPing) { 323 LogUtils.i(TAG, "RestartPingsTask did not start any pings."); 324 EasService.this.mSynchronizer.stopServiceIfIdle(); 325 } 326 } 327 } 328 EasService()329 public EasService() { 330 super(); 331 mSynchronizer = new PingSyncSynchronizer(this); 332 } 333 334 @Override onCreate()335 public void onCreate() { 336 LogUtils.i(TAG, "EasService.onCreate"); 337 super.onCreate(); 338 TempDirectory.setTempDirectory(this); 339 EmailContent.init(this); 340 AUTHORITIES_TO_SYNC = new String[] { 341 EmailContent.AUTHORITY, 342 CalendarContract.AUTHORITY, 343 ContactsContract.AUTHORITY 344 }; 345 SharedPreferences sharedPrefs = EasService.this.getSharedPreferences(PREFERENCES_FILE, 346 Context.MODE_PRIVATE); 347 sProtocolLogging = sharedPrefs.getBoolean(PROTOCOL_LOGGING_PREF, false); 348 sFileLogging = sharedPrefs.getBoolean(FILE_LOGGING_PREF, false); 349 // Restart push for all accounts that need it. Because this requires DB loads, we do it in 350 // an AsyncTask, and we startService to ensure that we stick around long enough for the 351 // task to complete. The task will stop the service if necessary after it's done. 352 startService(new Intent(this, EasService.class)); 353 new RestartPingsTask().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); 354 } 355 356 @Override onDestroy()357 public void onDestroy() { 358 LogUtils.i(TAG, "onDestroy"); 359 mSynchronizer.stopAllPings(); 360 } 361 362 @Override onBind(final Intent intent)363 public IBinder onBind(final Intent intent) { 364 return mBinder; 365 } 366 367 @Override onStartCommand(final Intent intent, final int flags, final int startId)368 public int onStartCommand(final Intent intent, final int flags, final int startId) { 369 if (intent != null && 370 TextUtils.equals(Eas.EXCHANGE_SERVICE_INTENT_ACTION, intent.getAction())) { 371 if (intent.getBooleanExtra(ServiceProxy.EXTRA_FORCE_SHUTDOWN, false)) { 372 // We've been asked to forcibly shutdown. This happens if email accounts are 373 // deleted, otherwise we can get errors if services are still running for 374 // accounts that are now gone. 375 // TODO: This is kind of a hack, it would be nicer if we could handle it correctly 376 // if accounts disappear out from under us. 377 LogUtils.d(TAG, "Forced shutdown, killing process"); 378 System.exit(-1); 379 } else if (intent.getBooleanExtra(EXTRA_START_PING, false)) { 380 LogUtils.d(LogUtils.TAG, "Restarting ping"); 381 final Account account = intent.getParcelableExtra(EXTRA_PING_ACCOUNT); 382 final android.accounts.Account amAccount = 383 new android.accounts.Account(account.mEmailAddress, 384 Eas.EXCHANGE_ACCOUNT_MANAGER_TYPE); 385 EasPing.requestPing(amAccount); 386 } 387 } 388 return START_STICKY; 389 } 390 doOperation(final EasOperation operation, final String loggingName)391 public int doOperation(final EasOperation operation, final String loggingName) { 392 LogUtils.d(TAG, "%s: %d", loggingName, operation.getAccountId()); 393 mSynchronizer.syncStart(operation.getAccountId()); 394 int result = EasOperation.RESULT_MIN_OK_RESULT; 395 // TODO: Do we need a wakelock here? For RPC coming from sync adapters, no -- the SA 396 // already has one. But for others, maybe? Not sure what's guaranteed for AIDL calls. 397 // If we add a wakelock (or anything else for that matter) here, must remember to undo 398 // it in the finally block below. 399 // On the other hand, even for SAs, it doesn't hurt to get a wakelock here. 400 try { 401 result = operation.performOperation(); 402 LogUtils.d(TAG, "Operation result %d", result); 403 return result; 404 } finally { 405 mSynchronizer.syncEnd(result < EasOperation.RESULT_MIN_OK_RESULT, 406 operation.getAccount()); 407 } 408 } 409 410 /** 411 * Determine whether this account is configured with folders that are ready for push 412 * notifications. 413 * @param account The {@link Account} that we're interested in. 414 * @param context The context 415 * @return Whether this account needs to ping. 416 */ 417 public static boolean pingNeededForAccount(final Context context, final Account account) { 418 // Check account existence. 419 if (account == null || account.mId == Account.NO_ACCOUNT) { 420 LogUtils.d(TAG, "Do not ping: Account not found or not valid"); 421 return false; 422 } 423 424 // Check if account is configured for a push sync interval. 425 if (account.mSyncInterval != Account.CHECK_INTERVAL_PUSH) { 426 LogUtils.d(TAG, "Do not ping: Account %d not configured for push", account.mId); 427 return false; 428 } 429 430 // Check security hold status of the account. 431 if ((account.mFlags & Account.FLAGS_SECURITY_HOLD) != 0) { 432 LogUtils.d(TAG, "Do not ping: Account %d is on security hold", account.mId); 433 return false; 434 } 435 436 // Check if the account has performed at least one sync so far (accounts must perform 437 // the initial sync before push is possible). 438 if (EmailContent.isInitialSyncKey(account.mSyncKey)) { 439 LogUtils.d(TAG, "Do not ping: Account %d has not done initial sync", account.mId); 440 return false; 441 } 442 443 // Check that there's at least one mailbox that is both configured for push notifications, 444 // and whose content type is enabled for sync in the account manager. 445 final android.accounts.Account amAccount = new android.accounts.Account( 446 account.mEmailAddress, Eas.EXCHANGE_ACCOUNT_MANAGER_TYPE); 447 448 final Set<String> authsToSync = getAuthoritiesToSync(amAccount, AUTHORITIES_TO_SYNC); 449 // If we have at least one sync-enabled content type, check for syncing mailboxes. 450 if (!authsToSync.isEmpty()) { 451 final Cursor c = Mailbox.getMailboxesForPush(context.getContentResolver(), account.mId); 452 if (c != null) { 453 try { 454 while (c.moveToNext()) { 455 final int mailboxType = c.getInt(Mailbox.CONTENT_TYPE_COLUMN); 456 if (authsToSync.contains(Mailbox.getAuthority(mailboxType))) { 457 LogUtils.d(TAG, "should ping for account %d", account.mId); 458 return true; 459 } 460 } 461 } finally { 462 c.close(); 463 } 464 } 465 } 466 LogUtils.d(TAG, "Do not ping: Account %d has no folders configured for push", account.mId); 467 return false; 468 } 469 470 static public GalResult searchGal(final Context context, final long accountId, 471 final String filter, final int limit) { 472 GalResult galResult = null; 473 final Account account = loadAccount(context, accountId); 474 if (account != null) { 475 final EasSearchGal operation = new EasSearchGal(context, account, filter, limit); 476 // We don't use doOperation() here for two reasons: 477 // 1. This is a static function, doOperation is not, and we don't have an instance of 478 // EasService. 479 // 2. All doOperation() does besides this is stop the ping and then restart it. This is 480 // required during syncs, but not for GalSearches. 481 final int result = operation.performOperation(); 482 if (result == EasSearchGal.RESULT_OK) { 483 galResult = operation.getResult(); 484 } 485 } 486 return galResult; 487 } 488 489 /** 490 * Converts from an EasOperation status to a status code defined in EmailServiceStatus. 491 * This is used to communicate the status of a sync operation to the caller. 492 * @param easStatus result returned from an EasOperation 493 * @return EmailServiceStatus 494 */ 495 private int convertToEmailServiceStatus(int easStatus) { 496 if (easStatus >= EasOperation.RESULT_MIN_OK_RESULT) { 497 return EmailServiceStatus.SUCCESS; 498 } 499 switch (easStatus) { 500 case EasOperation.RESULT_ABORT: 501 case EasOperation.RESULT_RESTART: 502 // This should only happen if a ping is interruped for some reason. We would not 503 // expect see that here, since this should only be called for a sync. 504 LogUtils.e(TAG, "Abort or Restart easStatus"); 505 return EmailServiceStatus.SUCCESS; 506 507 case EasOperation.RESULT_TOO_MANY_REDIRECTS: 508 return EmailServiceStatus.INTERNAL_ERROR; 509 510 case EasOperation.RESULT_NETWORK_PROBLEM: 511 // This is due to an IO error, we need the caller to know about this so that it 512 // can let the syncManager know. 513 return EmailServiceStatus.IO_ERROR; 514 515 case EasOperation.RESULT_FORBIDDEN: 516 case EasOperation.RESULT_AUTHENTICATION_ERROR: 517 return EmailServiceStatus.LOGIN_FAILED; 518 519 case EasOperation.RESULT_PROVISIONING_ERROR: 520 return EmailServiceStatus.PROVISIONING_ERROR; 521 522 case EasOperation.RESULT_CLIENT_CERTIFICATE_REQUIRED: 523 return EmailServiceStatus.CLIENT_CERTIFICATE_ERROR; 524 525 case EasOperation.RESULT_PROTOCOL_VERSION_UNSUPPORTED: 526 return EmailServiceStatus.PROTOCOL_ERROR; 527 528 case EasOperation.RESULT_INITIALIZATION_FAILURE: 529 case EasOperation.RESULT_HARD_DATA_FAILURE: 530 case EasOperation.RESULT_OTHER_FAILURE: 531 return EmailServiceStatus.INTERNAL_ERROR; 532 533 case EasOperation.RESULT_NON_FATAL_ERROR: 534 // We do not expect to see this error here: This should be consumed in 535 // EasFullSyncOperation. The only case this occurs in is when we try to send 536 // a message in the outbox, and there's some problem with the message locally 537 // that prevents it from being sent. We return a 538 LogUtils.e(TAG, "Other non-fatal error easStatus %d", easStatus); 539 return EmailServiceStatus.SUCCESS; 540 } 541 LogUtils.e(TAG, "Unexpected easStatus %d", easStatus); 542 return EmailServiceStatus.INTERNAL_ERROR; 543 } 544 545 546 /** 547 * Determine which content types are set to sync for an account. 548 * @param account The account whose sync settings we're looking for. 549 * @param authorities All possible authorities we could care about. 550 * @return The authorities for the content types we want to sync for account. 551 */ getAuthoritiesToSync(final android.accounts.Account account, final String[] authorities)552 public static Set<String> getAuthoritiesToSync(final android.accounts.Account account, 553 final String[] authorities) { 554 final HashSet<String> authsToSync = new HashSet(); 555 for (final String authority : authorities) { 556 if (ContentResolver.getSyncAutomatically(account, authority)) { 557 authsToSync.add(authority); 558 } 559 } 560 return authsToSync; 561 } 562 563 @VisibleForTesting setProtocolLogging(final boolean val)564 public static void setProtocolLogging(final boolean val) { 565 sProtocolLogging = val; 566 } 567 568 @VisibleForTesting setFileLogging(final boolean val)569 public static void setFileLogging(final boolean val) { 570 sFileLogging = val; 571 } 572 getProtocolLogging()573 public static boolean getProtocolLogging() { 574 return sProtocolLogging; 575 } 576 getFileLogging()577 public static boolean getFileLogging() { 578 return sFileLogging; 579 } 580 581 } 582