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