blob: 05a041dece352f3e017beb1f58b10f399fbb696c [file] [log] [blame]
/*
* 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);
}
}