MailAppProvider.java revision 828f46144e79c7bb0856f43dc237e4faef3623de
1/** 2 * Copyright (c) 2011, Google Inc. 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.mail.providers; 18 19import android.app.Activity; 20import android.content.ContentProvider; 21import android.content.ContentResolver; 22import android.content.ContentValues; 23import android.content.Context; 24import android.content.CursorLoader; 25import android.content.Intent; 26import android.content.Loader; 27import android.content.Loader.OnLoadCompleteListener; 28import android.content.SharedPreferences; 29import android.database.Cursor; 30import android.database.MatrixCursor; 31import android.net.Uri; 32import android.os.Bundle; 33import android.provider.BaseColumns; 34import android.text.TextUtils; 35 36import com.android.mail.providers.UIProvider.AccountCursorExtraKeys; 37import com.android.mail.providers.protos.boot.AccountReceiver; 38import com.android.mail.utils.LogTag; 39import com.android.mail.utils.LogUtils; 40import com.android.mail.utils.MatrixCursorWithExtra; 41import com.google.common.collect.ImmutableSet; 42import com.google.common.collect.Maps; 43import com.google.common.collect.Sets; 44 45import java.util.Collections; 46import java.util.Map; 47import java.util.Set; 48import java.util.regex.Pattern; 49 50 51 52/** 53 * The Account Cache provider allows email providers to register "accounts" and the UI has a single 54 * place to query for the list of accounts. 55 * 56 * During development this will allow new account types to be added, and allow them to be shown in 57 * the application. For example, the mock accounts can be enabled/disabled. 58 * In the future, once other processes can add new accounts, this could allow other "mail" 59 * applications have their content appear within the application 60 */ 61public abstract class MailAppProvider extends ContentProvider 62 implements OnLoadCompleteListener<Cursor>{ 63 64 private static final String SHARED_PREFERENCES_NAME = "MailAppProvider"; 65 private static final String ACCOUNT_LIST_KEY = "accountList"; 66 private static final String LAST_VIEWED_ACCOUNT_KEY = "lastViewedAccount"; 67 68 /** 69 * Extra used in the result from the activity launched by the intent specified 70 * by {@link #getNoAccountsIntent} to return the list of accounts. The data 71 * specified by this extra key should be a ParcelableArray. 72 */ 73 public static final String ADD_ACCOUNT_RESULT_ACCOUNTS_EXTRA = "addAccountResultAccounts"; 74 75 private final static String LOG_TAG = LogTag.getLogTag(); 76 77 private final Map<Uri, AccountCacheEntry> mAccountCache = Maps.newHashMap(); 78 79 private final Map<Uri, CursorLoader> mCursorLoaderMap = Maps.newHashMap(); 80 81 private ContentResolver mResolver; 82 private static String sAuthority; 83 private static MailAppProvider sInstance; 84 85 private volatile boolean mAccountsFullyLoaded = false; 86 87 private SharedPreferences mSharedPrefs; 88 89 /** 90 * Allows the implementing provider to specify the authority for this provider. Email and Gmail 91 * must specify different authorities. 92 */ 93 protected abstract String getAuthority(); 94 95 /** 96 * Authority for the suggestions provider. Email and Gmail must specify different authorities, 97 * much like the implementation of {@link #getAuthority()}. 98 * @return the suggestion authority associated with this provider. 99 */ 100 public abstract String getSuggestionAuthority(); 101 102 /** 103 * Allows the implementing provider to specify an intent that should be used in a call to 104 * {@link Context#startActivityForResult(android.content.Intent)} when the account provider 105 * doesn't return any accounts. 106 * 107 * The result from the {@link Activity} activity should include the list of accounts in 108 * the returned intent, in the 109 110 * @return Intent or null, if the provider doesn't specify a behavior when no accounts are 111 * specified. 112 */ 113 protected abstract Intent getNoAccountsIntent(Context context); 114 115 /** 116 * The cursor returned from a call to {@link android.content.ContentResolver#query()} with this 117 * uri will return a cursor that with columns that are a subset of the columns specified 118 * in {@link UIProvider.ConversationColumns} 119 * The cursor returned by this query can return a {@link android.os.Bundle} 120 * from a call to {@link android.database.Cursor#getExtras()}. This Bundle may have 121 * values with keys listed in {@link AccountCursorExtraKeys} 122 */ 123 public static Uri getAccountsUri() { 124 return Uri.parse("content://" + sAuthority + "/"); 125 } 126 127 public static MailAppProvider getInstance() { 128 return sInstance; 129 } 130 131 /** Default constructor */ 132 public MailAppProvider() { 133 sInstance = this; 134 } 135 136 @Override 137 public boolean onCreate() { 138 sAuthority = getAuthority(); 139 mResolver = getContext().getContentResolver(); 140 141 final Intent intent = new Intent(AccountReceiver.ACTION_PROVIDER_CREATED); 142 getContext().sendBroadcast(intent); 143 144 // Load the previously saved account list 145 loadCachedAccountList(); 146 147 return true; 148 } 149 150 @Override 151 public void shutdown() { 152 sInstance = null; 153 154 for (CursorLoader loader : mCursorLoaderMap.values()) { 155 loader.stopLoading(); 156 } 157 mCursorLoaderMap.clear(); 158 } 159 160 @Override 161 public Cursor query(Uri url, String[] projection, String selection, String[] selectionArgs, 162 String sortOrder) { 163 // This content provider currently only supports one query (to return the list of accounts). 164 // No reason to check the uri. Currently only checking the projections 165 166 // Validates and returns the projection that should be used. 167 final String[] resultProjection = UIProviderValidator.validateAccountProjection(projection); 168 final Bundle extras = new Bundle(); 169 extras.putInt(AccountCursorExtraKeys.ACCOUNTS_LOADED, mAccountsFullyLoaded ? 1 : 0); 170 171 // Make a copy of the account cache 172 173 final Set<AccountCacheEntry> accountList; 174 synchronized (mAccountCache) { 175 accountList = ImmutableSet.copyOf(mAccountCache.values()); 176 } 177 178 final MatrixCursor cursor = 179 new MatrixCursorWithExtra(resultProjection, accountList.size(), extras); 180 181 for (AccountCacheEntry accountEntry : accountList) { 182 final Account account = accountEntry.mAccount; 183 final MatrixCursor.RowBuilder builder = cursor.newRow(); 184 185 for (String column : resultProjection) { 186 if (TextUtils.equals(column, BaseColumns._ID)) { 187 // TODO(pwestbro): remove this as it isn't used. 188 builder.add(Integer.valueOf(0)); 189 } else if (TextUtils.equals(column, UIProvider.AccountColumns.NAME)) { 190 builder.add(account.name); 191 } else if (TextUtils.equals(column, UIProvider.AccountColumns.PROVIDER_VERSION)) { 192 // TODO fix this 193 builder.add(Integer.valueOf(account.providerVersion)); 194 } else if (TextUtils.equals(column, UIProvider.AccountColumns.URI)) { 195 builder.add(account.uri); 196 } else if (TextUtils.equals(column, UIProvider.AccountColumns.CAPABILITIES)) { 197 builder.add(Integer.valueOf(account.capabilities)); 198 } else if (TextUtils.equals(column, UIProvider.AccountColumns.FOLDER_LIST_URI)) { 199 builder.add(account.folderListUri); 200 } else if (TextUtils 201 .equals(column, UIProvider.AccountColumns.FULL_FOLDER_LIST_URI)) { 202 builder.add(account.fullFolderListUri); 203 } else if (TextUtils.equals(column, UIProvider.AccountColumns.SEARCH_URI)) { 204 builder.add(account.searchUri); 205 } else if (TextUtils.equals(column, 206 UIProvider.AccountColumns.ACCOUNT_FROM_ADDRESSES)) { 207 builder.add(account.accountFromAddresses); 208 } else if (TextUtils.equals(column, UIProvider.AccountColumns.SAVE_DRAFT_URI)) { 209 builder.add(account.saveDraftUri); 210 } else if (TextUtils.equals(column, UIProvider.AccountColumns.SEND_MAIL_URI)) { 211 builder.add(account.sendMessageUri); 212 } else if (TextUtils.equals(column, 213 UIProvider.AccountColumns.EXPUNGE_MESSAGE_URI)) { 214 builder.add(account.expungeMessageUri); 215 } else if (TextUtils.equals(column, UIProvider.AccountColumns.UNDO_URI)) { 216 builder.add(account.undoUri); 217 } else if (TextUtils.equals(column, 218 UIProvider.AccountColumns.SETTINGS_INTENT_URI)) { 219 builder.add(account.settingsIntentUri); 220 } else if (TextUtils.equals(column, 221 UIProvider.AccountColumns.HELP_INTENT_URI)) { 222 builder.add(account.helpIntentUri); 223 } else if (TextUtils.equals(column, 224 UIProvider.AccountColumns.SEND_FEEDBACK_INTENT_URI)) { 225 builder.add(account.sendFeedbackIntentUri); 226 } else if (TextUtils.equals(column, UIProvider.AccountColumns.SYNC_STATUS)) { 227 builder.add(Integer.valueOf(account.syncStatus)); 228 } else if (TextUtils.equals(column, UIProvider.AccountColumns.COMPOSE_URI)) { 229 builder.add(account.composeIntentUri); 230 } else if (TextUtils.equals(column, UIProvider.AccountColumns.MIME_TYPE)) { 231 builder.add(account.mimeType); 232 } else if (TextUtils.equals(column, 233 UIProvider.AccountColumns.RECENT_FOLDER_LIST_URI)) { 234 builder.add(account.recentFolderListUri); 235 } else if (TextUtils.equals(column, 236 UIProvider.AccountColumns.DEFAULT_RECENT_FOLDER_LIST_URI)) { 237 builder.add(account.defaultRecentFolderListUri); 238 } else if (TextUtils.equals(column, 239 UIProvider.AccountColumns.MANUAL_SYNC_URI)) { 240 builder.add(account.manualSyncUri); 241 } else if (TextUtils.equals(column, UIProvider.AccountColumns.COLOR)) { 242 builder.add(account.color); 243 } else if (TextUtils.equals(column, 244 UIProvider.AccountColumns.SettingsColumns.SIGNATURE)) { 245 builder.add(account.settings.signature); 246 } else if (TextUtils.equals(column, 247 UIProvider.AccountColumns.SettingsColumns.AUTO_ADVANCE)) { 248 builder.add(Integer.valueOf(account.settings.autoAdvance)); 249 } else if (TextUtils.equals(column, 250 UIProvider.AccountColumns.SettingsColumns.MESSAGE_TEXT_SIZE)) { 251 builder.add(Integer.valueOf(account.settings.messageTextSize)); 252 } else if (TextUtils.equals(column, 253 UIProvider.AccountColumns.SettingsColumns.REPLY_BEHAVIOR)) { 254 builder.add(Integer.valueOf(account.settings.replyBehavior)); 255 } else if (TextUtils.equals(column, 256 UIProvider.AccountColumns.SettingsColumns.HIDE_CHECKBOXES)) { 257 builder.add(Integer.valueOf(account.settings.hideCheckboxes ? 1 : 0)); 258 } else if (TextUtils.equals(column, 259 UIProvider.AccountColumns.SettingsColumns.CONFIRM_DELETE)) { 260 builder.add(Integer.valueOf(account.settings.confirmDelete ? 1 : 0)); 261 } else if (TextUtils.equals(column, 262 UIProvider.AccountColumns.SettingsColumns.CONFIRM_ARCHIVE)) { 263 builder.add(Integer.valueOf(account.settings.confirmArchive ? 1 : 0)); 264 } else if (TextUtils.equals(column, 265 UIProvider.AccountColumns.SettingsColumns.CONFIRM_SEND)) { 266 builder.add(Integer.valueOf(account.settings.confirmSend ? 1 : 0)); 267 } else if (TextUtils.equals(column, 268 UIProvider.AccountColumns.SettingsColumns.DEFAULT_INBOX)) { 269 builder.add(account.settings.defaultInbox); 270 } else if (TextUtils.equals(column, 271 UIProvider.AccountColumns.SettingsColumns.DEFAULT_INBOX_NAME)) { 272 builder.add(account.settings.defaultInboxName); 273 } else if (TextUtils.equals(column, 274 UIProvider.AccountColumns.SettingsColumns.SNAP_HEADERS)) { 275 builder.add(Integer.valueOf(account.settings.snapHeaders)); 276 } else if (TextUtils.equals(column, 277 UIProvider.AccountColumns.SettingsColumns.FORCE_REPLY_FROM_DEFAULT)) { 278 builder.add(Integer.valueOf(account.settings.forceReplyFromDefault ? 1 : 0)); 279 } else if (TextUtils.equals(column, 280 UIProvider.AccountColumns.SettingsColumns.MAX_ATTACHMENT_SIZE)) { 281 builder.add(account.settings.maxAttachmentSize); 282 } else { 283 throw new IllegalStateException("Column not found: " + column); 284 } 285 } 286 } 287 288 cursor.setNotificationUri(mResolver, getAccountsUri()); 289 return cursor; 290 } 291 292 @Override 293 public Uri insert(Uri url, ContentValues values) { 294 return url; 295 } 296 297 @Override 298 public int update(Uri url, ContentValues values, String selection, 299 String[] selectionArgs) { 300 return 0; 301 } 302 303 @Override 304 public int delete(Uri url, String selection, String[] selectionArgs) { 305 return 0; 306 } 307 308 @Override 309 public String getType(Uri uri) { 310 return null; 311 } 312 313 /** 314 * Asynchronously ads all of the accounts that are specified by the result set returned by 315 * {@link ContentProvider#query()} for the specified uri. The content provider handling the 316 * query needs to handle the {@link UIProvider.ACCOUNTS_PROJECTION} 317 * Any changes to the underlying provider will automatically be reflected. 318 * @param resolver 319 * @param accountsQueryUri 320 */ 321 public static void addAccountsForUriAsync(Uri accountsQueryUri) { 322 getInstance().startAccountsLoader(accountsQueryUri); 323 } 324 325 /** 326 * Returns the intent that should be used in a call to 327 * {@link Context#startActivity(android.content.Intent)} when the account provider doesn't 328 * return any accounts 329 * @return Intent or null, if the provider doesn't specify a behavior when no acccounts are 330 * specified. 331 */ 332 public static Intent getNoAccountIntent(Context context) { 333 return getInstance().getNoAccountsIntent(context); 334 } 335 336 private synchronized void startAccountsLoader(Uri accountsQueryUri) { 337 final CursorLoader accountsCursorLoader = new CursorLoader(getContext(), accountsQueryUri, 338 UIProvider.ACCOUNTS_PROJECTION, null, null, null); 339 340 // Listen for the results 341 accountsCursorLoader.registerListener(accountsQueryUri.hashCode(), this); 342 accountsCursorLoader.startLoading(); 343 344 // If there is a previous loader for the given uri, stop it 345 final CursorLoader oldLoader = mCursorLoaderMap.get(accountsQueryUri); 346 if (oldLoader != null) { 347 oldLoader.stopLoading(); 348 } 349 mCursorLoaderMap.put(accountsQueryUri, accountsCursorLoader); 350 } 351 352 public static void addAccount(Account account, Uri accountsQueryUri) { 353 final MailAppProvider provider = getInstance(); 354 if (provider == null) { 355 throw new IllegalStateException("MailAppProvider not intialized"); 356 } 357 provider.addAccountImpl(account, accountsQueryUri, true /* notify */); 358 } 359 360 private void addAccountImpl(Account account, Uri accountsQueryUri, boolean notify) { 361 synchronized (mAccountCache) { 362 if (account != null) { 363 LogUtils.v(LOG_TAG, "adding account %s", account); 364 mAccountCache.put(account.uri, new AccountCacheEntry(account, accountsQueryUri)); 365 } 366 } 367 // Explicitly calling this out of the synchronized block in case any of the observers get 368 // called synchronously. 369 if (notify) { 370 broadcastAccountChange(); 371 } 372 373 // Cache the updated account list 374 cacheAccountList(); 375 } 376 377 public static void removeAccount(Uri accountUri) { 378 final MailAppProvider provider = getInstance(); 379 if (provider == null) { 380 throw new IllegalStateException("MailAppProvider not intialized"); 381 } 382 provider.removeAccounts(Collections.singleton(accountUri), true /* notify */); 383 } 384 385 private void removeAccounts(Set<Uri> uris, boolean notify) { 386 synchronized (mAccountCache) { 387 for (Uri accountUri : uris) { 388 mAccountCache.remove(accountUri); 389 } 390 } 391 392 // Explicitly calling this out of the synchronized block in case any of the observers get 393 // called synchronously. 394 if (notify) { 395 broadcastAccountChange(); 396 } 397 398 // Cache the updated account list 399 cacheAccountList(); 400 } 401 402 private static void broadcastAccountChange() { 403 final MailAppProvider provider = sInstance; 404 405 if (provider != null) { 406 provider.mResolver.notifyChange(getAccountsUri(), null); 407 } 408 } 409 410 /** 411 * Returns the {@link Account#uri} (in String form) of the last viewed account. 412 */ 413 public String getLastViewedAccount() { 414 return getPreferences().getString(LAST_VIEWED_ACCOUNT_KEY, null); 415 } 416 417 /** 418 * Persists the {@link Account#uri} (in String form) of the last viewed account. 419 */ 420 public void setLastViewedAccount(String accountUriStr) { 421 final SharedPreferences.Editor editor = getPreferences().edit(); 422 editor.putString(LAST_VIEWED_ACCOUNT_KEY, accountUriStr); 423 editor.apply(); 424 } 425 426 private void loadCachedAccountList() { 427 final SharedPreferences preference = getPreferences(); 428 429 final Set<String> accountsStringSet = preference.getStringSet(ACCOUNT_LIST_KEY, null); 430 431 if (accountsStringSet != null) { 432 for (String serializedAccount : accountsStringSet) { 433 try { 434 final AccountCacheEntry accountEntry = 435 new AccountCacheEntry(serializedAccount); 436 if (accountEntry.mAccount.settings != null) { 437 addAccountImpl(accountEntry.mAccount, accountEntry.mAccountsQueryUri, 438 false /* don't notify */); 439 } else { 440 LogUtils.e(LOG_TAG, "Dropping account that doesn't specify settings"); 441 } 442 } catch (Exception e) { 443 // Unable to create account object, skip to next 444 LogUtils.e(LOG_TAG, e, 445 "Unable to create account object from serialized string '%s'", 446 serializedAccount); 447 } 448 } 449 broadcastAccountChange(); 450 } 451 } 452 453 private void cacheAccountList() { 454 final SharedPreferences preference = getPreferences(); 455 456 final Set<AccountCacheEntry> accountList; 457 synchronized (mAccountCache) { 458 accountList = ImmutableSet.copyOf(mAccountCache.values()); 459 } 460 461 final Set<String> serializedAccounts = Sets.newHashSet(); 462 for (AccountCacheEntry accountEntry : accountList) { 463 serializedAccounts.add(accountEntry.serialize()); 464 } 465 466 final SharedPreferences.Editor editor = getPreferences().edit(); 467 editor.putStringSet(ACCOUNT_LIST_KEY, serializedAccounts); 468 editor.apply(); 469 } 470 471 private SharedPreferences getPreferences() { 472 if (mSharedPrefs == null) { 473 mSharedPrefs = getContext().getSharedPreferences( 474 SHARED_PREFERENCES_NAME, Context.MODE_PRIVATE); 475 } 476 return mSharedPrefs; 477 } 478 479 static public Account getAccountFromAccountUri(Uri accountUri) { 480 MailAppProvider provider = getInstance(); 481 if (provider != null && provider.mAccountsFullyLoaded) { 482 synchronized(provider.mAccountCache) { 483 AccountCacheEntry entry = provider.mAccountCache.get(accountUri); 484 if (entry != null) { 485 return entry.mAccount; 486 } 487 } 488 } 489 return null; 490 } 491 492 @Override 493 public void onLoadComplete(Loader<Cursor> loader, Cursor data) { 494 if (data == null) { 495 LogUtils.d(LOG_TAG, "null account cursor returned"); 496 return; 497 } 498 499 LogUtils.d(LOG_TAG, "Cursor with %d accounts returned", data.getCount()); 500 final CursorLoader cursorLoader = (CursorLoader)loader; 501 final Uri accountsQueryUri = cursorLoader.getUri(); 502 503 final Set<AccountCacheEntry> accountList; 504 synchronized (mAccountCache) { 505 accountList = ImmutableSet.copyOf(mAccountCache.values()); 506 } 507 508 // Build a set of the account uris that had been associated with that query 509 final Set<Uri> previousQueryUriMap = Sets.newHashSet(); 510 for (AccountCacheEntry entry : accountList) { 511 if (accountsQueryUri.equals(entry.mAccountsQueryUri)) { 512 previousQueryUriMap.add(entry.mAccount.uri); 513 } 514 } 515 516 // Update the internal state of this provider if the returned result set 517 // represents all accounts 518 // TODO: determine what should happen with a heterogeneous set of accounts 519 final Bundle extra = data.getExtras(); 520 mAccountsFullyLoaded = extra.getInt(AccountCursorExtraKeys.ACCOUNTS_LOADED) != 0; 521 522 final Set<Uri> newQueryUriMap = Sets.newHashSet(); 523 while (data.moveToNext()) { 524 final Account account = new Account(data); 525 final Uri accountUri = account.uri; 526 newQueryUriMap.add(accountUri); 527 addAccountImpl(account, accountsQueryUri, false /* don't notify */); 528 } 529 530 if (previousQueryUriMap != null) { 531 // Remove all of the accounts that are in the new result set 532 previousQueryUriMap.removeAll(newQueryUriMap); 533 534 // For all of the entries that had been in the previous result set, and are not 535 // in the new result set, remove them from the cache 536 if (previousQueryUriMap.size() > 0 && mAccountsFullyLoaded) { 537 removeAccounts(previousQueryUriMap, false /* don't notify */); 538 } 539 } 540 broadcastAccountChange(); 541 } 542 543 /** 544 * Object that allows the Account Cache provider to associate the account with the content 545 * provider uri that originated that account. 546 */ 547 private static class AccountCacheEntry { 548 final Account mAccount; 549 final Uri mAccountsQueryUri; 550 551 private static final String ACCOUNT_ENTRY_COMPONENT_SEPARATOR = "^**^"; 552 private static final Pattern ACCOUNT_ENTRY_COMPONENT_SEPARATOR_PATTERN = 553 Pattern.compile("\\^\\*\\*\\^"); 554 555 private static final int NUMBER_MEMBERS = 2; 556 557 public AccountCacheEntry(Account account, Uri accountQueryUri) { 558 mAccount = account; 559 mAccountsQueryUri = accountQueryUri; 560 } 561 562 /** 563 * Return a serialized String for this AccountCacheEntry. 564 */ 565 public synchronized String serialize() { 566 StringBuilder out = new StringBuilder(); 567 out.append(mAccount.serialize()).append(ACCOUNT_ENTRY_COMPONENT_SEPARATOR); 568 final String accountQueryUri = 569 mAccountsQueryUri != null ? mAccountsQueryUri.toString() : ""; 570 out.append(accountQueryUri); 571 return out.toString(); 572 } 573 574 /** 575 * Create an account cache object from a serialized string previously stored away. 576 * If the serializedString does not parse as a valid account, we throw an 577 * {@link IllegalArgumentException}. The caller is responsible for checking this and 578 * ignoring the newly created object if the exception is thrown. 579 * @param serializedString 580 */ 581 public AccountCacheEntry(String serializedString) throws IllegalArgumentException { 582 String[] cacheEntryMembers = TextUtils.split(serializedString, 583 ACCOUNT_ENTRY_COMPONENT_SEPARATOR_PATTERN); 584 if (cacheEntryMembers.length != NUMBER_MEMBERS) { 585 throw new IllegalArgumentException("AccountCacheEntry de-serializing failed. " 586 + "Wrong number of members detected. " 587 + cacheEntryMembers.length + " detected"); 588 } 589 mAccount = Account.newinstance(cacheEntryMembers[0]); 590 if (mAccount == null) { 591 throw new IllegalArgumentException("AccountCacheEntry de-serializing failed. " 592 + "Account object could not be created from the serialized string: " 593 + serializedString); 594 } 595 if (mAccount.settings == Settings.EMPTY_SETTINGS) { 596 throw new IllegalArgumentException("AccountCacheEntry de-serializing failed. " 597 + "Settings could not be created from the string: " + serializedString); 598 } 599 mAccountsQueryUri = !TextUtils.isEmpty(cacheEntryMembers[1]) ? 600 Uri.parse(cacheEntryMembers[1]) : null; 601 } 602 } 603} 604