| /* |
| * Copyright (C) 2011 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.contacts.editor; |
| |
| import com.android.contacts.model.AccountType; |
| import com.android.contacts.model.AccountTypeManager; |
| import com.android.contacts.model.AccountWithDataSet; |
| import com.google.common.annotations.VisibleForTesting; |
| import com.google.common.collect.ImmutableList; |
| import com.google.common.collect.Sets; |
| |
| import android.accounts.Account; |
| import android.accounts.AccountManager; |
| import android.app.Activity; |
| import android.content.Context; |
| import android.content.Intent; |
| import android.content.SharedPreferences; |
| import android.preference.PreferenceManager; |
| import android.text.TextUtils; |
| import android.util.Log; |
| |
| import java.util.ArrayList; |
| import java.util.List; |
| import java.util.Set; |
| |
| /** |
| * Utility methods for the "account changed" notification in the new contact creation flow. |
| */ |
| public class ContactEditorUtils { |
| private static final String TAG = "ContactEditorUtils"; |
| |
| private static final String KEY_DEFAULT_ACCOUNT = "ContactEditorUtils_default_account"; |
| private static final String KEY_KNOWN_ACCOUNTS = "ContactEditorUtils_known_accounts"; |
| // Key to tell the first time launch. |
| private static final String KEY_ANYTHING_SAVED = "ContactEditorUtils_anything_saved"; |
| |
| private static final List<AccountWithDataSet> EMPTY_ACCOUNTS = ImmutableList.of(); |
| |
| private static ContactEditorUtils sInstance; |
| |
| private final Context mContext; |
| private final SharedPreferences mPrefs; |
| private final AccountTypeManager mAccountTypes; |
| |
| private ContactEditorUtils(Context context) { |
| this(context, AccountTypeManager.getInstance(context)); |
| } |
| |
| @VisibleForTesting |
| ContactEditorUtils(Context context, AccountTypeManager accountTypes) { |
| mContext = context.getApplicationContext(); |
| mPrefs = PreferenceManager.getDefaultSharedPreferences(mContext); |
| mAccountTypes = accountTypes; |
| } |
| |
| public static synchronized ContactEditorUtils getInstance(Context context) { |
| if (sInstance == null) { |
| sInstance = new ContactEditorUtils(context); |
| } |
| return sInstance; |
| } |
| |
| void cleanupForTest() { |
| mPrefs.edit().remove(KEY_DEFAULT_ACCOUNT).remove(KEY_KNOWN_ACCOUNTS) |
| .remove(KEY_ANYTHING_SAVED).apply(); |
| } |
| |
| void removeDefaultAccountForTest() { |
| mPrefs.edit().remove(KEY_DEFAULT_ACCOUNT).apply(); |
| } |
| |
| /** |
| * Sets the {@link #KEY_KNOWN_ACCOUNTS} and {@link #KEY_DEFAULT_ACCOUNT} preference values to |
| * empty strings to reset the state of the preferences file. |
| */ |
| private void resetPreferenceValues() { |
| mPrefs.edit().putString(KEY_KNOWN_ACCOUNTS, "").putString(KEY_DEFAULT_ACCOUNT, "").apply(); |
| } |
| |
| private List<AccountWithDataSet> getWritableAccounts() { |
| return mAccountTypes.getAccounts(true); |
| } |
| |
| /** |
| * @return true if it's the first launch and {@link #saveDefaultAndAllAccounts} has never |
| * been called. |
| */ |
| private boolean isFirstLaunch() { |
| return !mPrefs.getBoolean(KEY_ANYTHING_SAVED, false); |
| } |
| |
| /** |
| * Saves all writable accounts and the default account, which can later be obtained |
| * with {@link #getDefaultAccount}. |
| * |
| * This should be called when saving a newly created contact. |
| * |
| * @param defaultAccount the account used to save a newly created contact. Or pass {@code null} |
| * If the user selected "local only". |
| */ |
| public void saveDefaultAndAllAccounts(AccountWithDataSet defaultAccount) { |
| final SharedPreferences.Editor editor = mPrefs.edit() |
| .putBoolean(KEY_ANYTHING_SAVED, true); |
| |
| if (defaultAccount == null) { |
| // If the default is "local only", there should be no writable accounts. |
| // This should always be the case with our spec, but because we load the account list |
| // asynchronously using a worker thread, it is possible that there are accounts at this |
| // point. So if the default is null always clear the account list. |
| editor.putString(KEY_KNOWN_ACCOUNTS, ""); |
| editor.putString(KEY_DEFAULT_ACCOUNT, ""); |
| } else { |
| editor.putString(KEY_KNOWN_ACCOUNTS, |
| AccountWithDataSet.stringifyList(getWritableAccounts())); |
| editor.putString(KEY_DEFAULT_ACCOUNT, defaultAccount.stringify()); |
| } |
| editor.apply(); |
| } |
| |
| /** |
| * @return the default account saved with {@link #saveDefaultAndAllAccounts}. |
| * |
| * Note the {@code null} return value can mean either {@link #saveDefaultAndAllAccounts} has |
| * never been called, or {@code null} was passed to {@link #saveDefaultAndAllAccounts} -- |
| * i.e. the user selected "local only". |
| * |
| * Also note that the returned account may have been removed already. |
| */ |
| public AccountWithDataSet getDefaultAccount() { |
| final String saved = mPrefs.getString(KEY_DEFAULT_ACCOUNT, null); |
| if (TextUtils.isEmpty(saved)) { |
| return null; |
| } |
| try { |
| return AccountWithDataSet.unstringify(saved); |
| } catch (IllegalArgumentException exception) { |
| Log.e(TAG, "Error with retrieving default account " + exception.toString()); |
| // unstringify()can throw an exception if the string is not in an expected format. |
| // Hence, if the preferences file is corrupt, just reset the preference values |
| resetPreferenceValues(); |
| return null; |
| } |
| } |
| |
| /** |
| * @return true if an account still exists. {@code null} is considered "local only" here, |
| * so it's valid too. |
| */ |
| @VisibleForTesting |
| boolean isValidAccount(AccountWithDataSet account) { |
| if (account == null) { |
| return true; // It's "local only" account, which is valid. |
| } |
| return getWritableAccounts().contains(account); |
| } |
| |
| /** |
| * @return saved known accounts, or an empty list if none has been saved yet. |
| */ |
| @VisibleForTesting |
| List<AccountWithDataSet> getSavedAccounts() { |
| final String saved = mPrefs.getString(KEY_KNOWN_ACCOUNTS, null); |
| if (TextUtils.isEmpty(saved)) { |
| return EMPTY_ACCOUNTS; |
| } |
| try { |
| return AccountWithDataSet.unstringifyList(saved); |
| } catch (IllegalArgumentException exception) { |
| Log.e(TAG, "Error with retrieving saved accounts " + exception.toString()); |
| // unstringifyList()can throw an exception if the string is not in an expected format. |
| // Hence, if the preferences file is corrupt, just reset the preference values |
| resetPreferenceValues(); |
| return EMPTY_ACCOUNTS; |
| } |
| } |
| |
| /** |
| * @return true if the contact editor should show the "accounts changed" notification, that is: |
| * - If it's the first launch. |
| * - Or, if an account has been added. |
| * - Or, if the default account has been removed. |
| * (And some extra sanity check) |
| * |
| * Note if this method returns {@code false}, the caller can safely assume that |
| * {@link #getDefaultAccount} will return a valid account. (Either an account which still |
| * exists, or {@code null} which should be interpreted as "local only".) |
| */ |
| public boolean shouldShowAccountChangedNotification() { |
| if (isFirstLaunch()) { |
| return true; |
| } |
| |
| // Account added? |
| final List<AccountWithDataSet> savedAccounts = getSavedAccounts(); |
| final List<AccountWithDataSet> currentWritableAccounts = getWritableAccounts(); |
| for (AccountWithDataSet account : currentWritableAccounts) { |
| if (!savedAccounts.contains(account)) { |
| return true; // New account found. |
| } |
| } |
| |
| final AccountWithDataSet defaultAccount = getDefaultAccount(); |
| |
| // Does default account still exist? |
| if (!isValidAccount(defaultAccount)) { |
| return true; |
| } |
| |
| // If there is an inconsistent state in the preferences file - default account is null |
| // ("local" account) while there are multiple accounts, then show the notification dialog. |
| // This shouldn't ever happen, but this should allow the user can get back into a normal |
| // state after they respond to the notification. |
| if (defaultAccount == null && currentWritableAccounts.size() > 0) { |
| Log.e(TAG, "Preferences file in an inconsistent state, request that the default account" |
| + " and current writable accounts be saved again"); |
| return true; |
| } |
| |
| // All good. |
| return false; |
| } |
| |
| @VisibleForTesting |
| String[] getWritableAccountTypeStrings() { |
| final Set<String> types = Sets.newHashSet(); |
| for (AccountType type : mAccountTypes.getAccountTypes(true)) { |
| types.add(type.accountType); |
| } |
| return types.toArray(new String[types.size()]); |
| } |
| |
| /** |
| * Create an {@link Intent} to start "add new account" setup wizard. Selectable account |
| * types will be limited to ones that supports editing contacts. |
| * |
| * Use {@link Activity#startActivityForResult} or |
| * {@link android.app.Fragment#startActivityForResult} to start the wizard, and |
| * {@link Activity#onActivityResult} or {@link android.app.Fragment#onActivityResult} to |
| * get the result. |
| */ |
| public Intent createAddWritableAccountIntent() { |
| return AccountManager.newChooseAccountIntent( |
| null, // selectedAccount |
| new ArrayList<Account>(), // allowableAccounts |
| getWritableAccountTypeStrings(), // allowableAccountTypes |
| false, // alwaysPromptForAccount |
| null, // descriptionOverrideText |
| null, // addAccountAuthTokenType |
| null, // addAccountRequiredFeatures |
| null // addAccountOptions |
| ); |
| } |
| |
| /** |
| * Parses a result from {@link #createAddWritableAccountIntent} and returns the created |
| * {@link Account}, or null if the user has canceled the wizard. Pass the {@code resultCode} |
| * and {@code data} parameters passed to {@link Activity#onActivityResult} or |
| * {@link android.app.Fragment#onActivityResult}. |
| * |
| * Note although the return type is {@link AccountWithDataSet}, return values from this method |
| * will never have {@link AccountWithDataSet#dataSet} set, as there's no way to create an |
| * extension package account from setup wizard. |
| */ |
| public AccountWithDataSet getCreatedAccount(int resultCode, Intent resultData) { |
| // Javadoc doesn't say anything about resultCode but that the data intent will be non null |
| // on success. |
| if (resultData == null) return null; |
| |
| final String accountType = resultData.getStringExtra(AccountManager.KEY_ACCOUNT_TYPE); |
| final String accountName = resultData.getStringExtra(AccountManager.KEY_ACCOUNT_NAME); |
| |
| // Just in case |
| if (TextUtils.isEmpty(accountType) || TextUtils.isEmpty(accountName)) return null; |
| |
| return new AccountWithDataSet(accountName, accountType, null); |
| } |
| } |