Home | History | Annotate | Download | only in editor
      1 /*
      2  * Copyright (C) 2011 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.contacts.editor;
     18 
     19 import com.android.contacts.model.AccountType;
     20 import com.android.contacts.model.AccountTypeManager;
     21 import com.android.contacts.model.AccountWithDataSet;
     22 import com.android.contacts.test.NeededForTesting;
     23 import com.google.common.annotations.VisibleForTesting;
     24 import com.google.common.collect.ImmutableList;
     25 import com.google.common.collect.Sets;
     26 
     27 import android.accounts.Account;
     28 import android.accounts.AccountManager;
     29 import android.app.Activity;
     30 import android.content.Context;
     31 import android.content.Intent;
     32 import android.content.SharedPreferences;
     33 import android.preference.PreferenceManager;
     34 import android.text.TextUtils;
     35 import android.util.Log;
     36 
     37 import java.util.ArrayList;
     38 import java.util.List;
     39 import java.util.Set;
     40 
     41 /**
     42  * Utility methods for the "account changed" notification in the new contact creation flow.
     43  */
     44 public class ContactEditorUtils {
     45     private static final String TAG = "ContactEditorUtils";
     46 
     47     private static final String KEY_DEFAULT_ACCOUNT = "ContactEditorUtils_default_account";
     48     private static final String KEY_KNOWN_ACCOUNTS = "ContactEditorUtils_known_accounts";
     49     // Key to tell the first time launch.
     50     private static final String KEY_ANYTHING_SAVED = "ContactEditorUtils_anything_saved";
     51 
     52     private static final List<AccountWithDataSet> EMPTY_ACCOUNTS = ImmutableList.of();
     53 
     54     private static ContactEditorUtils sInstance;
     55 
     56     private final Context mContext;
     57     private final SharedPreferences mPrefs;
     58     private final AccountTypeManager mAccountTypes;
     59 
     60     private ContactEditorUtils(Context context) {
     61         this(context, AccountTypeManager.getInstance(context));
     62     }
     63 
     64     @VisibleForTesting
     65     ContactEditorUtils(Context context, AccountTypeManager accountTypes) {
     66         mContext = context.getApplicationContext();
     67         mPrefs = PreferenceManager.getDefaultSharedPreferences(mContext);
     68         mAccountTypes = accountTypes;
     69     }
     70 
     71     public static synchronized ContactEditorUtils getInstance(Context context) {
     72         if (sInstance == null) {
     73             sInstance = new ContactEditorUtils(context);
     74         }
     75         return sInstance;
     76     }
     77 
     78     @NeededForTesting
     79     void cleanupForTest() {
     80         mPrefs.edit().remove(KEY_DEFAULT_ACCOUNT).remove(KEY_KNOWN_ACCOUNTS)
     81                 .remove(KEY_ANYTHING_SAVED).apply();
     82     }
     83 
     84     @NeededForTesting
     85     void removeDefaultAccountForTest() {
     86         mPrefs.edit().remove(KEY_DEFAULT_ACCOUNT).apply();
     87     }
     88 
     89     /**
     90      * Sets the {@link #KEY_KNOWN_ACCOUNTS} and {@link #KEY_DEFAULT_ACCOUNT} preference values to
     91      * empty strings to reset the state of the preferences file.
     92      */
     93     private void resetPreferenceValues() {
     94         mPrefs.edit().putString(KEY_KNOWN_ACCOUNTS, "").putString(KEY_DEFAULT_ACCOUNT, "").apply();
     95     }
     96 
     97     private List<AccountWithDataSet> getWritableAccounts() {
     98         return mAccountTypes.getAccounts(true);
     99     }
    100 
    101     /**
    102      * @return true if it's the first launch and {@link #saveDefaultAndAllAccounts} has never
    103      *     been called.
    104      */
    105     private boolean isFirstLaunch() {
    106         return !mPrefs.getBoolean(KEY_ANYTHING_SAVED, false);
    107     }
    108 
    109     /**
    110      * Saves all writable accounts and the default account, which can later be obtained
    111      * with {@link #getDefaultAccount}.
    112      *
    113      * This should be called when saving a newly created contact.
    114      *
    115      * @param defaultAccount the account used to save a newly created contact.  Or pass {@code null}
    116      *     If the user selected "local only".
    117      */
    118     public void saveDefaultAndAllAccounts(AccountWithDataSet defaultAccount) {
    119         final SharedPreferences.Editor editor = mPrefs.edit()
    120                 .putBoolean(KEY_ANYTHING_SAVED, true);
    121 
    122         if (defaultAccount == null) {
    123             // If the default is "local only", there should be no writable accounts.
    124             // This should always be the case with our spec, but because we load the account list
    125             // asynchronously using a worker thread, it is possible that there are accounts at this
    126             // point. So if the default is null always clear the account list.
    127             editor.putString(KEY_KNOWN_ACCOUNTS, "");
    128             editor.putString(KEY_DEFAULT_ACCOUNT, "");
    129         } else {
    130             editor.putString(KEY_KNOWN_ACCOUNTS,
    131                     AccountWithDataSet.stringifyList(getWritableAccounts()));
    132             editor.putString(KEY_DEFAULT_ACCOUNT, defaultAccount.stringify());
    133         }
    134         editor.apply();
    135     }
    136 
    137     /**
    138      * @return the default account saved with {@link #saveDefaultAndAllAccounts}.
    139      *
    140      * Note the {@code null} return value can mean either {@link #saveDefaultAndAllAccounts} has
    141      * never been called, or {@code null} was passed to {@link #saveDefaultAndAllAccounts} --
    142      * i.e. the user selected "local only".
    143      *
    144      * Also note that the returned account may have been removed already.
    145      */
    146     public AccountWithDataSet getDefaultAccount() {
    147         final String saved = mPrefs.getString(KEY_DEFAULT_ACCOUNT, null);
    148         if (TextUtils.isEmpty(saved)) {
    149             return null;
    150         }
    151         try {
    152             return AccountWithDataSet.unstringify(saved);
    153         } catch (IllegalArgumentException exception) {
    154             Log.e(TAG, "Error with retrieving default account " + exception.toString());
    155             // unstringify()can throw an exception if the string is not in an expected format.
    156             // Hence, if the preferences file is corrupt, just reset the preference values
    157             resetPreferenceValues();
    158             return null;
    159         }
    160     }
    161 
    162     /**
    163      * @return true if an account still exists.  {@code null} is considered "local only" here,
    164      *    so it's valid too.
    165      */
    166     @VisibleForTesting
    167     boolean isValidAccount(AccountWithDataSet account) {
    168         if (account == null) {
    169             return true; // It's "local only" account, which is valid.
    170         }
    171         return getWritableAccounts().contains(account);
    172     }
    173 
    174     /**
    175      * @return saved known accounts, or an empty list if none has been saved yet.
    176      */
    177     @VisibleForTesting
    178     List<AccountWithDataSet> getSavedAccounts() {
    179         final String saved = mPrefs.getString(KEY_KNOWN_ACCOUNTS, null);
    180         if (TextUtils.isEmpty(saved)) {
    181             return EMPTY_ACCOUNTS;
    182         }
    183         try {
    184             return AccountWithDataSet.unstringifyList(saved);
    185         } catch (IllegalArgumentException exception) {
    186             Log.e(TAG, "Error with retrieving saved accounts " + exception.toString());
    187             // unstringifyList()can throw an exception if the string is not in an expected format.
    188             // Hence, if the preferences file is corrupt, just reset the preference values
    189             resetPreferenceValues();
    190             return EMPTY_ACCOUNTS;
    191         }
    192     }
    193 
    194     /**
    195      * @return true if the contact editor should show the "accounts changed" notification, that is:
    196      * - If it's the first launch.
    197      * - Or, if an account has been added.
    198      * - Or, if the default account has been removed.
    199      * (And some extra sanity check)
    200      *
    201      * Note if this method returns {@code false}, the caller can safely assume that
    202      * {@link #getDefaultAccount} will return a valid account.  (Either an account which still
    203      * exists, or {@code null} which should be interpreted as "local only".)
    204      */
    205     public boolean shouldShowAccountChangedNotification() {
    206         if (isFirstLaunch()) {
    207             return true;
    208         }
    209 
    210         // Account added?
    211         final List<AccountWithDataSet> savedAccounts = getSavedAccounts();
    212         final List<AccountWithDataSet> currentWritableAccounts = getWritableAccounts();
    213         for (AccountWithDataSet account : currentWritableAccounts) {
    214             if (!savedAccounts.contains(account)) {
    215                 return true; // New account found.
    216             }
    217         }
    218 
    219         final AccountWithDataSet defaultAccount = getDefaultAccount();
    220 
    221         // Does default account still exist?
    222         if (!isValidAccount(defaultAccount)) {
    223             return true;
    224         }
    225 
    226         // If there is an inconsistent state in the preferences file - default account is null
    227         // ("local" account) while there are multiple accounts, then show the notification dialog.
    228         // This shouldn't ever happen, but this should allow the user can get back into a normal
    229         // state after they respond to the notification.
    230         if (defaultAccount == null && currentWritableAccounts.size() > 0) {
    231             Log.e(TAG, "Preferences file in an inconsistent state, request that the default account"
    232                     + " and current writable accounts be saved again");
    233             return true;
    234         }
    235 
    236         // All good.
    237         return false;
    238     }
    239 
    240     @VisibleForTesting
    241     String[] getWritableAccountTypeStrings() {
    242         final Set<String> types = Sets.newHashSet();
    243         for (AccountType type : mAccountTypes.getAccountTypes(true)) {
    244             types.add(type.accountType);
    245         }
    246         return types.toArray(new String[types.size()]);
    247     }
    248 
    249     /**
    250      * Create an {@link Intent} to start "add new account" setup wizard.  Selectable account
    251      * types will be limited to ones that supports editing contacts.
    252      *
    253      * Use {@link Activity#startActivityForResult} or
    254      * {@link android.app.Fragment#startActivityForResult} to start the wizard, and
    255      * {@link Activity#onActivityResult} or {@link android.app.Fragment#onActivityResult} to
    256      * get the result.
    257      */
    258     public Intent createAddWritableAccountIntent() {
    259         return AccountManager.newChooseAccountIntent(
    260                 null, // selectedAccount
    261                 new ArrayList<Account>(), // allowableAccounts
    262                 getWritableAccountTypeStrings(), // allowableAccountTypes
    263                 false, // alwaysPromptForAccount
    264                 null, // descriptionOverrideText
    265                 null, // addAccountAuthTokenType
    266                 null, // addAccountRequiredFeatures
    267                 null // addAccountOptions
    268                 );
    269     }
    270 
    271     /**
    272      * Parses a result from {@link #createAddWritableAccountIntent} and returns the created
    273      * {@link Account}, or null if the user has canceled the wizard.  Pass the {@code resultCode}
    274      * and {@code data} parameters passed to {@link Activity#onActivityResult} or
    275      * {@link android.app.Fragment#onActivityResult}.
    276      *
    277      * Note although the return type is {@link AccountWithDataSet}, return values from this method
    278      * will never have {@link AccountWithDataSet#dataSet} set, as there's no way to create an
    279      * extension package account from setup wizard.
    280      */
    281     public AccountWithDataSet getCreatedAccount(int resultCode, Intent resultData) {
    282         // Javadoc doesn't say anything about resultCode but that the data intent will be non null
    283         // on success.
    284         if (resultData == null) return null;
    285 
    286         final String accountType = resultData.getStringExtra(AccountManager.KEY_ACCOUNT_TYPE);
    287         final String accountName = resultData.getStringExtra(AccountManager.KEY_ACCOUNT_NAME);
    288 
    289         // Just in case
    290         if (TextUtils.isEmpty(accountType) || TextUtils.isEmpty(accountName)) return null;
    291 
    292         return new AccountWithDataSet(accountName, accountType, null);
    293     }
    294 }
    295