• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 android.accounts.Account;
20 import android.accounts.AccountManager;
21 import android.app.Activity;
22 import android.content.Context;
23 import android.content.Intent;
24 import android.content.SharedPreferences;
25 import android.preference.PreferenceManager;
26 import android.text.TextUtils;
27 import android.util.Log;
28 
29 import com.android.contacts.common.test.NeededForTesting;
30 import com.android.contacts.common.model.AccountTypeManager;
31 import com.android.contacts.common.model.account.AccountType;
32 import com.android.contacts.common.model.account.AccountWithDataSet;
33 import com.google.common.annotations.VisibleForTesting;
34 import com.google.common.collect.ImmutableList;
35 import com.google.common.collect.Sets;
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 
ContactEditorUtils(Context context)60     private ContactEditorUtils(Context context) {
61         this(context, AccountTypeManager.getInstance(context));
62     }
63 
64     @VisibleForTesting
ContactEditorUtils(Context context, AccountTypeManager accountTypes)65     ContactEditorUtils(Context context, AccountTypeManager accountTypes) {
66         mContext = context.getApplicationContext();
67         mPrefs = PreferenceManager.getDefaultSharedPreferences(mContext);
68         mAccountTypes = accountTypes;
69     }
70 
getInstance(Context context)71     public static synchronized ContactEditorUtils getInstance(Context context) {
72         if (sInstance == null) {
73             sInstance = new ContactEditorUtils(context.getApplicationContext());
74         }
75         return sInstance;
76     }
77 
78     @NeededForTesting
cleanupForTest()79     void cleanupForTest() {
80         mPrefs.edit().remove(KEY_DEFAULT_ACCOUNT).remove(KEY_KNOWN_ACCOUNTS)
81                 .remove(KEY_ANYTHING_SAVED).apply();
82     }
83 
84     @NeededForTesting
removeDefaultAccountForTest()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      */
resetPreferenceValues()93     private void resetPreferenceValues() {
94         mPrefs.edit().putString(KEY_KNOWN_ACCOUNTS, "").putString(KEY_DEFAULT_ACCOUNT, "").apply();
95     }
96 
getWritableAccounts()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      */
isFirstLaunch()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      */
saveDefaultAndAllAccounts(AccountWithDataSet defaultAccount)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      */
getDefaultAccount()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
isValidAccount(AccountWithDataSet account)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
getSavedAccounts()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      */
shouldShowAccountChangedNotification()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
getWritableAccountTypeStrings()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      */
createAddWritableAccountIntent()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      */
getCreatedAccount(int resultCode, Intent resultData)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