ContactEditorUtils.java revision 558669dab4109afebd19eade1f95a396215fb44d
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
17package com.android.contacts.editor;
18
19import com.android.contacts.model.AccountType;
20import com.android.contacts.model.AccountTypeManager;
21import com.android.contacts.model.AccountWithDataSet;
22import com.android.contacts.test.NeededForTesting;
23import com.google.common.annotations.VisibleForTesting;
24import com.google.common.collect.ImmutableList;
25import com.google.common.collect.Sets;
26
27import android.accounts.Account;
28import android.accounts.AccountManager;
29import android.app.Activity;
30import android.content.Context;
31import android.content.Intent;
32import android.content.SharedPreferences;
33import android.preference.PreferenceManager;
34import android.text.TextUtils;
35
36import java.util.ArrayList;
37import java.util.List;
38import java.util.Set;
39
40/**
41 * Utility methods for the "account changed" notification in the new contact creation flow.
42 *
43 * TODO Remove all the "@VisibleForTesting"s once they're actually used in the app.
44 *      (Until then we need them to avoid "no such method" in tests)
45 */
46public class ContactEditorUtils {
47    private static final String TAG = "ContactEditorUtils";
48
49    private static final String KEY_DEFAULT_ACCOUNT = "ContactEditorUtils_default_account";
50    private static final String KEY_KNOWN_ACCOUNTS = "ContactEditorUtils_known_accounts";
51    // Key to tell the first time launch.
52    private static final String KEY_ANYTHING_SAVED = "ContactEditorUtils_anything_saved";
53
54    private static final List<AccountWithDataSet> EMPTY_ACCOUNTS = ImmutableList.of();
55
56    private static ContactEditorUtils sInstance;
57
58    private final Context mContext;
59    private final SharedPreferences mPrefs;
60    private final AccountTypeManager mAccountTypes;
61
62    private ContactEditorUtils(Context context) {
63        this(context, AccountTypeManager.getInstance(context));
64    }
65
66    @VisibleForTesting
67    ContactEditorUtils(Context context, AccountTypeManager accountTypes) {
68        mContext = context;
69        mPrefs = PreferenceManager.getDefaultSharedPreferences(mContext);
70        mAccountTypes = accountTypes;
71    }
72
73    public static synchronized ContactEditorUtils getInstance(Context context) {
74        if (sInstance == null) {
75            sInstance = new ContactEditorUtils(context);
76        }
77        return sInstance;
78    }
79
80    void cleanupForTest() {
81        mPrefs.edit().remove(KEY_DEFAULT_ACCOUNT).remove(KEY_KNOWN_ACCOUNTS)
82                .remove(KEY_ANYTHING_SAVED).apply();
83    }
84
85    private List<AccountWithDataSet> getWritableAccounts() {
86        return mAccountTypes.getAccounts(true);
87    }
88
89    /**
90     * @return true if it's the first launch and {@link #saveDefaultAndAllAccounts} has never
91     *     been called.
92     */
93    private boolean isFirstLaunch() {
94        return !mPrefs.getBoolean(KEY_ANYTHING_SAVED, false);
95    }
96
97    /**
98     * Saves all writable accounts and the default account, which can later be obtained
99     * with {@link #getDefaultAccount}.
100     *
101     * This should be called when saving a newly created contact.
102     *
103     * @param defaultAccount the account used to save a newly created contact.  Or pass {@code null}
104     *     If the user selected "local only".
105     */
106    @NeededForTesting
107    public void saveDefaultAndAllAccounts(AccountWithDataSet defaultAccount) {
108        mPrefs.edit()
109                .putBoolean(KEY_ANYTHING_SAVED, true)
110                .putString(
111                        KEY_KNOWN_ACCOUNTS,AccountWithDataSet.stringifyList(getWritableAccounts()))
112                .putString(KEY_DEFAULT_ACCOUNT,
113                        (defaultAccount == null) ? "" : defaultAccount.stringify())
114                .apply();
115    }
116
117    /**
118     * @return the default account saved with {@link #saveDefaultAndAllAccounts}.
119     *
120     * Note the {@code null} return value can mean either {@link #saveDefaultAndAllAccounts} has
121     * never been called, or {@code null} was passed to {@link #saveDefaultAndAllAccounts} --
122     * i.e. the user selected "local only".
123     *
124     * Also note that the returned account may have been removed already.
125     */
126    @NeededForTesting
127    public AccountWithDataSet getDefaultAccount() {
128        final String saved = mPrefs.getString(KEY_DEFAULT_ACCOUNT, null);
129        if (TextUtils.isEmpty(saved)) {
130            return null;
131        }
132        return AccountWithDataSet.unstringify(saved);
133    }
134
135    /**
136     * @return true if an account still exists.  {@code null} is considered "local only" here,
137     *    so it's valid too.
138     */
139    @VisibleForTesting
140    boolean isValidAccount(AccountWithDataSet account) {
141        if (account == null) {
142            return true; // It's "local only" account, which is valid.
143        }
144        return getWritableAccounts().contains(account);
145    }
146
147    /**
148     * @return saved known accounts, or an empty list if none has been saved yet.
149     */
150    @VisibleForTesting
151    List<AccountWithDataSet> getSavedAccounts() {
152        final String saved = mPrefs.getString(KEY_KNOWN_ACCOUNTS, null);
153        if (TextUtils.isEmpty(saved)) {
154            return EMPTY_ACCOUNTS;
155        }
156        return AccountWithDataSet.unstringifyList(saved);
157    }
158
159    /**
160     * @return true if the contact editor should show the "accounts changed" notification, that is:
161     * - If it's the first launch.
162     * - Or, if an account has been added.
163     * - Or, if the default account has been removed.
164     *
165     * Note if this method returns {@code false}, the caller can safely assume that
166     * {@link #getDefaultAccount} will return a valid account.  (Either an account which still
167     * exists, or {@code null} which should be interpreted as "local only".)
168     */
169    @NeededForTesting
170    public boolean shouldShowAccountChangedNotification() {
171        if (isFirstLaunch()) {
172            return true;
173        }
174
175        // Account added?
176        final List<AccountWithDataSet> savedAccounts = getSavedAccounts();
177        for (AccountWithDataSet account : getWritableAccounts()) {
178            if (!savedAccounts.contains(account)) {
179                return true; // New account found.
180            }
181        }
182
183        // Does default account still exist?
184        if (!isValidAccount(getDefaultAccount())) {
185            return true;
186        }
187
188        // All good.
189        return false;
190    }
191
192    @VisibleForTesting
193    String[] getWritableAccountTypeStrings() {
194        final Set<String> types = Sets.newHashSet();
195        for (AccountType type : mAccountTypes.getAccountTypes(true)) {
196            types.add(type.accountType);
197        }
198        return types.toArray(new String[types.size()]);
199    }
200
201    /**
202     * Create an {@link Intent} to start "add new account" setup wizard.  Selectable account
203     * types will be limited to ones that supports editing contacts.
204     *
205     * Use {@link Activity#startActivityForResult} or
206     * {@link android.app.Fragment#startActivityForResult} to start the wizard, and
207     * {@link Activity#onActivityResult} or {@link android.app.Fragment#onActivityResult} to
208     * get the result.
209     */
210    @NeededForTesting
211    public Intent createAddWritableAccountIntent() {
212        return AccountManager.newChooseAccountIntent(
213                null, // selectedAccount
214                new ArrayList<Account>(), // allowableAccounts
215                getWritableAccountTypeStrings(), // allowableAccountTypes
216                false, // alwaysPromptForAccount
217                null, // descriptionOverrideText
218                null, // addAccountAuthTokenType
219                null, // addAccountRequiredFeatures
220                null // addAccountOptions
221                );
222    }
223
224    /**
225     * Parses a result from {@link #createAddWritableAccountIntent} and returns the created
226     * {@link Account}, or null if the user has canceled the wizard.  Pass the {@code resultCode}
227     * and {@code data} parameters passed to {@link Activity#onActivityResult} or
228     * {@link android.app.Fragment#onActivityResult}.
229     *
230     * Note although the return type is {@link AccountWithDataSet}, return values from this method
231     * will never have {@link AccountWithDataSet#dataSet} set, as there's no way to create an
232     * extension package account from setup wizard.
233     */
234    @NeededForTesting
235    public AccountWithDataSet getCreatedAccount(int resultCode, Intent resultData) {
236        // Javadoc doesn't say anything about resultCode but that the data intent will be non null
237        // on success.
238        if (resultData == null) return null;
239
240        final String accountType = resultData.getStringExtra(AccountManager.KEY_ACCOUNT_TYPE);
241        final String accountName = resultData.getStringExtra(AccountManager.KEY_ACCOUNT_NAME);
242
243        // Just in case
244        if (TextUtils.isEmpty(accountType) || TextUtils.isEmpty(accountName)) return null;
245
246        return new AccountWithDataSet(accountName, accountType, null);
247    }
248}
249
250