am 64d95bae: Merge "Utility methods for new contact editor flow" into ics-factoryrom

* commit '64d95bae817be522e576ece311fa17d88a4c627c':
  Utility methods for new contact editor flow
diff --git a/src/com/android/contacts/editor/ContactEditorUtils.java b/src/com/android/contacts/editor/ContactEditorUtils.java
new file mode 100644
index 0000000..900e68a
--- /dev/null
+++ b/src/com/android/contacts/editor/ContactEditorUtils.java
@@ -0,0 +1,249 @@
+/*
+ * 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.android.contacts.test.NeededForTesting;
+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 java.util.ArrayList;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * Utility methods for the "account changed" notification in the new contact creation flow.
+ *
+ * TODO Remove all the "@VisibleForTesting"s once they're actually used in the app.
+ *      (Until then we need them to avoid "no such method" in tests)
+ */
+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;
+        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();
+    }
+
+    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".
+     */
+    @NeededForTesting
+    public void saveDefaultAndAllAccounts(AccountWithDataSet defaultAccount) {
+        mPrefs.edit()
+                .putBoolean(KEY_ANYTHING_SAVED, true)
+                .putString(
+                        KEY_KNOWN_ACCOUNTS,AccountWithDataSet.stringifyList(getWritableAccounts()))
+                .putString(KEY_DEFAULT_ACCOUNT,
+                        (defaultAccount == null) ? "" : defaultAccount.stringify())
+                .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.
+     */
+    @NeededForTesting
+    public AccountWithDataSet getDefaultAccount() {
+        final String saved = mPrefs.getString(KEY_DEFAULT_ACCOUNT, null);
+        if (TextUtils.isEmpty(saved)) {
+            return null;
+        }
+        return AccountWithDataSet.unstringify(saved);
+    }
+
+    /**
+     * @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;
+        }
+        return AccountWithDataSet.unstringifyList(saved);
+    }
+
+    /**
+     * @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.
+     *
+     * 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".)
+     */
+    @NeededForTesting
+    public boolean shouldShowAccountChangedNotification() {
+        if (isFirstLaunch()) {
+            return true;
+        }
+
+        // Account added?
+        final List<AccountWithDataSet> savedAccounts = getSavedAccounts();
+        for (AccountWithDataSet account : getWritableAccounts()) {
+            if (!savedAccounts.contains(account)) {
+                return true; // New account found.
+            }
+        }
+
+        // Does default account still exist?
+        if (!isValidAccount(getDefaultAccount())) {
+            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.
+     */
+    @NeededForTesting
+    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.
+     */
+    @NeededForTesting
+    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);
+    }
+}
+
diff --git a/src/com/android/contacts/model/AccountTypeManager.java b/src/com/android/contacts/model/AccountTypeManager.java
index bdd8a50..dc2fb0d 100644
--- a/src/com/android/contacts/model/AccountTypeManager.java
+++ b/src/com/android/contacts/model/AccountTypeManager.java
@@ -112,6 +112,13 @@
         final AccountType type = getAccountType(accountType, dataSet);
         return type == null ? null : type.getKindForMimetype(mimeType);
     }
+
+    /*
+     * Returns all registered {@link AccountType}s, including extension ones.
+     *
+     * @param contactWritableOnly if true, it only returns ones that support writing contacts.
+     */
+    public abstract List<AccountType> getAccountTypes(boolean contactWritableOnly);
 }
 
 class AccountTypeManagerImpl extends AccountTypeManager
@@ -539,4 +546,17 @@
         }
         return Collections.unmodifiableMap(result);
     }
+
+    @Override
+    public List<AccountType> getAccountTypes(boolean contactWritableOnly) {
+        final List<AccountType> accountTypes = Lists.newArrayList();
+        synchronized (this) {
+            for (AccountType type : mAccountTypesWithDataSets.values()) {
+                if (!contactWritableOnly || type.areContactsWritable()) {
+                    accountTypes.add(type);
+                }
+            }
+        }
+        return accountTypes;
+    }
 }
diff --git a/src/com/android/contacts/model/AccountWithDataSet.java b/src/com/android/contacts/model/AccountWithDataSet.java
index 55af795..e379346 100644
--- a/src/com/android/contacts/model/AccountWithDataSet.java
+++ b/src/com/android/contacts/model/AccountWithDataSet.java
@@ -17,21 +17,34 @@
 package com.android.contacts.model;
 
 import com.android.internal.util.Objects;
+import com.google.common.collect.Lists;
 
 import android.accounts.Account;
 import android.content.Context;
 import android.database.Cursor;
 import android.net.Uri;
 import android.os.Parcel;
+import android.os.Parcelable.Creator;
 import android.provider.BaseColumns;
 import android.provider.ContactsContract;
 import android.provider.ContactsContract.RawContacts;
 import android.text.TextUtils;
 
+import java.util.ArrayList;
+import java.util.List;
+import java.util.regex.Pattern;
+
 /**
  * Wrapper for an account that includes a data set (which may be null).
  */
 public class AccountWithDataSet extends Account {
+    private static final String STRINGIFY_SEPARATOR = "\u0001";
+    private static final String ARRAY_STRINGIFY_SEPARATOR = "\u0002";
+
+    private static final Pattern STRINGIFY_SEPARATOR_PAT =
+            Pattern.compile(Pattern.quote(STRINGIFY_SEPARATOR));
+    private static final Pattern ARRAY_STRINGIFY_SEPARATOR_PAT =
+            Pattern.compile(Pattern.quote(ARRAY_STRINGIFY_SEPARATOR));
 
     public final String dataSet;
     private final AccountTypeWithDataSet mAccountTypeWithDataSet;
@@ -47,12 +60,29 @@
         mAccountTypeWithDataSet = AccountTypeWithDataSet.get(type, dataSet);
     }
 
-    public AccountWithDataSet(Parcel in, String dataSet) {
+    public AccountWithDataSet(Parcel in) {
         super(in);
-        this.dataSet = dataSet;
+        this.dataSet = in.readString();
         mAccountTypeWithDataSet = AccountTypeWithDataSet.get(type, dataSet);
     }
 
+    @Override
+    public void writeToParcel(Parcel dest, int flags) {
+        super.writeToParcel(dest, flags);
+        dest.writeString(dataSet);
+    }
+
+    // For Parcelable
+    public static final Creator<AccountWithDataSet> CREATOR = new Creator<AccountWithDataSet>() {
+        public AccountWithDataSet createFromParcel(Parcel source) {
+            return new AccountWithDataSet(source);
+        }
+
+        public AccountWithDataSet[] newArray(int size) {
+            return new AccountWithDataSet[size];
+        }
+    };
+
     public AccountTypeWithDataSet getAccountTypeWithDataSet() {
         return mAccountTypeWithDataSet;
     }
@@ -100,4 +130,67 @@
     public String toString() {
         return "AccountWithDataSet {name=" + name + ", type=" + type + ", dataSet=" + dataSet + "}";
     }
+
+    private static StringBuilder addStringified(StringBuilder sb, AccountWithDataSet account) {
+        sb.append(account.name);
+        sb.append(STRINGIFY_SEPARATOR);
+        sb.append(account.type);
+        sb.append(STRINGIFY_SEPARATOR);
+        if (!TextUtils.isEmpty(account.dataSet)) sb.append(account.dataSet);
+
+        return sb;
+    }
+
+    /**
+     * Pack the instance into a string.
+     */
+    public String stringify() {
+        return addStringified(new StringBuilder(), this).toString();
+    }
+
+    /**
+     * Unpack a string created by {@link #stringify}.
+     */
+    public static AccountWithDataSet unstringify(String s) {
+        final String[] array = STRINGIFY_SEPARATOR_PAT.split(s, 3);
+        if (array.length < 3) {
+            throw new IllegalArgumentException("Invalid string");
+        }
+        return new AccountWithDataSet(array[0], array[1],
+                TextUtils.isEmpty(array[2]) ? null : array[2]);
+    }
+
+    /**
+     * Pack a list of {@link AccountWithDataSet} into a string.
+     */
+    public static String stringifyList(List<AccountWithDataSet> accounts) {
+        final StringBuilder sb = new StringBuilder();
+
+        for (AccountWithDataSet account : accounts) {
+            if (sb.length() > 0) {
+                sb.append(ARRAY_STRINGIFY_SEPARATOR);
+            }
+            addStringified(sb, account);
+        }
+
+        return sb.toString();
+    }
+
+    /**
+     * Unpack a list of {@link AccountWithDataSet} into a string.
+     */
+    public static List<AccountWithDataSet> unstringifyList(String s) {
+        final ArrayList<AccountWithDataSet> ret = Lists.newArrayList();
+        if (TextUtils.isEmpty(s)) {
+            return ret;
+        }
+
+        final String[] array = ARRAY_STRINGIFY_SEPARATOR_PAT.split(s);
+
+        for (int i = 0; i < array.length; i++) {
+            ret.add(unstringify(array[i]));
+        }
+
+        return ret;
+    }
 }
diff --git a/tests/src/com/android/contacts/editor/ContactEditorUtilsTest.java b/tests/src/com/android/contacts/editor/ContactEditorUtilsTest.java
new file mode 100644
index 0000000..9f4e487
--- /dev/null
+++ b/tests/src/com/android/contacts/editor/ContactEditorUtilsTest.java
@@ -0,0 +1,310 @@
+/*
+ * 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.editor.ContactEditorUtils;
+import com.android.contacts.model.AccountType;
+import com.android.contacts.model.AccountWithDataSet;
+import com.android.contacts.tests.mocks.MockAccountTypeManager;
+import com.google.android.collect.Sets;
+
+import android.content.Context;
+import android.test.AndroidTestCase;
+import android.test.MoreAsserts;
+import android.test.suitebuilder.annotation.SmallTest;
+
+import java.util.Collection;
+import java.util.Set;
+
+/**
+ * Test case for {@link ContactEditorUtils}.
+ *
+ * adb shell am instrument -w -e class com.android.contacts.editor.ContactEditorUtilsTest \
+       com.android.contacts.tests/android.test.InstrumentationTestRunner
+ */
+@SmallTest
+public class ContactEditorUtilsTest extends AndroidTestCase {
+    private MockAccountTypeManager mAccountTypes;
+    private ContactEditorUtils mTarget;
+
+    private static final MockAccountType TYPE1 = new MockAccountType("type1", null, true);
+    private static final MockAccountType TYPE2 = new MockAccountType("type2", null, true);
+    private static final MockAccountType TYPE2EX = new MockAccountType("type2", "ext", true);
+
+    // Only type 3 is "readonly".
+    private static final MockAccountType TYPE3 = new MockAccountType("type3", null, false);
+
+    private static final AccountWithDataSet ACCOUNT_1_A = new AccountWithDataSet(
+            "a", TYPE1.accountType, TYPE1.dataSet);
+    private static final AccountWithDataSet ACCOUNT_1_B = new AccountWithDataSet(
+            "b", TYPE1.accountType, TYPE1.dataSet);
+
+    private static final AccountWithDataSet ACCOUNT_2_A = new AccountWithDataSet(
+            "a", TYPE2.accountType, TYPE2.dataSet);
+    private static final AccountWithDataSet ACCOUNT_2EX_A = new AccountWithDataSet(
+            "a", TYPE2EX.accountType, TYPE2EX.dataSet);
+
+    private static final AccountWithDataSet ACCOUNT_3_C = new AccountWithDataSet(
+            "c", TYPE3.accountType, TYPE3.dataSet);
+
+    @Override
+    protected void setUp() throws Exception {
+        // Initialize with 0 types, 0 accounts.
+        mAccountTypes = new MockAccountTypeManager(new AccountType[] {},
+                new AccountWithDataSet[] {});
+        mTarget = new ContactEditorUtils(getContext(), mAccountTypes);
+
+        // Clear the preferences.
+        mTarget.cleanupForTest();
+    }
+
+    private void setAccountTypes(AccountType... types) {
+        mAccountTypes.mTypes = types;
+    }
+
+    private void setAccounts(AccountWithDataSet... accounts) {
+        mAccountTypes.mAccounts = accounts;
+    }
+
+    public void testGetWritableAccountTypeStrings() {
+        String[] types;
+
+        // 0 writable types
+        setAccountTypes();
+
+        types = mTarget.getWritableAccountTypeStrings();
+        MoreAsserts.assertEquals(types, new String[0]);
+
+        // 1 writable type
+        setAccountTypes(TYPE1);
+
+        types = mTarget.getWritableAccountTypeStrings();
+        MoreAsserts.assertEquals(Sets.newHashSet(TYPE1.accountType), Sets.newHashSet(types));
+
+        // 2 writable types
+        setAccountTypes(TYPE1, TYPE2EX);
+
+        types = mTarget.getWritableAccountTypeStrings();
+        MoreAsserts.assertEquals(Sets.newHashSet(TYPE1.accountType, TYPE2EX.accountType),
+                Sets.newHashSet(types));
+
+        // 3 writable types + 1 readonly type
+        setAccountTypes(TYPE1, TYPE2, TYPE2EX, TYPE3);
+
+        types = mTarget.getWritableAccountTypeStrings();
+        MoreAsserts.assertEquals(
+                Sets.newHashSet(TYPE1.accountType, TYPE2.accountType, TYPE2EX.accountType),
+                Sets.newHashSet(types));
+    }
+
+    /**
+     * Test for
+     * - {@link ContactEditorUtils#saveDefaultAndAllAccounts}
+     * - {@link ContactEditorUtils#getDefaultAccount}
+     * - {@link ContactEditorUtils#getSavedAccounts()}
+     */
+    public void testSaveDefaultAndAllAccounts() {
+        // Use these account types here.
+        setAccountTypes(TYPE1, TYPE2);
+
+        // If none has been saved, it should return an empty list.
+        assertEquals(0, mTarget.getSavedAccounts().size());
+
+        // Save 0 accounts.
+        mAccountTypes.mAccounts = new AccountWithDataSet[]{};
+        mTarget.saveDefaultAndAllAccounts(null);
+        assertNull(mTarget.getDefaultAccount());
+        MoreAsserts.assertEquals(
+                Sets.newHashSet(mAccountTypes.mAccounts),
+                toSet(mTarget.getSavedAccounts()));
+
+
+        // 1 account
+        mAccountTypes.mAccounts = new AccountWithDataSet[]{ACCOUNT_1_A};
+        mTarget.saveDefaultAndAllAccounts(ACCOUNT_1_A);
+        assertEquals(ACCOUNT_1_A, mTarget.getDefaultAccount());
+        MoreAsserts.assertEquals(
+                Sets.newHashSet(mAccountTypes.mAccounts),
+                toSet(mTarget.getSavedAccounts()));
+
+        // 2 account
+        mAccountTypes.mAccounts = new AccountWithDataSet[]{ACCOUNT_1_A, ACCOUNT_1_B};
+        mTarget.saveDefaultAndAllAccounts(ACCOUNT_1_B);
+        assertEquals(ACCOUNT_1_B, mTarget.getDefaultAccount());
+        MoreAsserts.assertEquals(
+                Sets.newHashSet(mAccountTypes.mAccounts),
+                toSet(mTarget.getSavedAccounts()));
+    }
+
+    public void testIsAccountValid() {
+        // Use these account types here.
+        setAccountTypes(TYPE1, TYPE2);
+
+        // 0 accounts
+        mAccountTypes.mAccounts = new AccountWithDataSet[]{};
+        assertFalse(mTarget.isValidAccount(ACCOUNT_1_A));
+        assertTrue(mTarget.isValidAccount(null)); // null is always valid
+
+        // 2 accounts
+        mAccountTypes.mAccounts = new AccountWithDataSet[]{ACCOUNT_1_A, ACCOUNT_2_A};
+        assertTrue(mTarget.isValidAccount(ACCOUNT_1_A));
+        assertTrue(mTarget.isValidAccount(ACCOUNT_2_A));
+        assertFalse(mTarget.isValidAccount(ACCOUNT_2EX_A));
+        assertTrue(mTarget.isValidAccount(null)); // null is always valid
+    }
+
+    /**
+     * Tests for {@link ContactEditorUtils#shouldShowAccountChangedNotification()}, starting with
+     * 0 accounts.
+     */
+    public void testShouldShowAccountChangedNotification_0Accounts() {
+        // There's always at least one writable type...
+        setAccountTypes(TYPE1);
+
+        // First launch -- always true.
+        assertTrue(mTarget.shouldShowAccountChangedNotification());
+
+        // We show the notification here, and user clicked "add account"
+        setAccounts(ACCOUNT_1_A);
+
+        // Now we open the contact editor with the new account.
+
+        // When closing the editor, we save the default account.
+        mTarget.saveDefaultAndAllAccounts(ACCOUNT_1_A);
+
+        // Next time the user creates a contact, we don't show the notification.
+        assertFalse(mTarget.shouldShowAccountChangedNotification());
+
+        // User added a new writable account, ACCOUNT_1_B.
+        setAccounts(ACCOUNT_1_A, ACCOUNT_1_B);
+
+        // Now we show the notification again.
+        assertTrue(mTarget.shouldShowAccountChangedNotification());
+
+        // User saved a new contact.  We update the account list and the default account.
+        mTarget.saveDefaultAndAllAccounts(ACCOUNT_1_B);
+
+        // User created another contact.  Now we don't show the notification.
+        assertFalse(mTarget.shouldShowAccountChangedNotification());
+
+        // User installed a new contact sync adapter...
+
+        // Added a new account type: TYPE2, and the TYPE2EX extension.
+        setAccountTypes(TYPE1, TYPE2, TYPE2EX);
+        // Add new accounts: ACCOUNT_2_A, ACCOUNT_2EX_A.
+        setAccounts(ACCOUNT_1_A, ACCOUNT_1_B, ACCOUNT_2_A, ACCOUNT_2EX_A);
+
+        // New account means another notification.
+        assertTrue(mTarget.shouldShowAccountChangedNotification());
+
+        // User saves a new contact, with a different default account.
+        mTarget.saveDefaultAndAllAccounts(ACCOUNT_2_A);
+
+        // Next time user creates a contact, no notification.
+        assertFalse(mTarget.shouldShowAccountChangedNotification());
+
+        // Remove ACCOUNT_2EX_A.
+        setAccountTypes(TYPE1, TYPE2, TYPE2EX);
+        setAccounts(ACCOUNT_1_A, ACCOUNT_1_B, ACCOUNT_2_A);
+
+        // ACCOUNT_2EX_A was not default, so no notification either.
+        assertFalse(mTarget.shouldShowAccountChangedNotification());
+
+        // Remove ACCOUNT_1_B, which is default.
+        setAccountTypes(TYPE1, TYPE2, TYPE2EX);
+        setAccounts(ACCOUNT_1_A, ACCOUNT_1_B);
+
+        // Now we show the notification.
+        assertTrue(mTarget.shouldShowAccountChangedNotification());
+    }
+
+    /**
+     * Tests for {@link ContactEditorUtils#shouldShowAccountChangedNotification()}, starting with
+     * 1 accounts.
+     */
+    public void testShouldShowAccountChangedNotification_1Account() {
+        setAccountTypes(TYPE1, TYPE2);
+        setAccounts(ACCOUNT_1_A);
+
+        // First launch -- always true.
+        assertTrue(mTarget.shouldShowAccountChangedNotification());
+
+        // User saves a new contact.
+        mTarget.saveDefaultAndAllAccounts(ACCOUNT_1_A);
+
+        // Next time, no notification.
+        assertFalse(mTarget.shouldShowAccountChangedNotification());
+
+        // The rest is the same...
+    }
+
+    /**
+     * Tests for {@link ContactEditorUtils#shouldShowAccountChangedNotification()}, starting with
+     * 0 accounts, and the user selected "local only".
+     */
+    public void testShouldShowAccountChangedNotification_0Account_localOnly() {
+        // There's always at least one writable type...
+        setAccountTypes(TYPE1);
+
+        // First launch -- always true.
+        assertTrue(mTarget.shouldShowAccountChangedNotification());
+
+        // We show the notification here, and user clicked "keep local" and saved an contact.
+        mTarget.saveDefaultAndAllAccounts(null);
+
+        // Now there are no accounts, and default account is null.
+
+        // The user created another contact, but this we shouldn't show the notification.
+        assertFalse(mTarget.shouldShowAccountChangedNotification());
+    }
+
+    private static <T> Set<T> toSet(Collection<T> collection) {
+        Set<T> ret = Sets.newHashSet();
+        ret.addAll(collection);
+        return ret;
+    }
+
+    private static class MockAccountType extends AccountType {
+        private boolean mAreContactsWritable;
+
+        public MockAccountType(String accountType, String dataSet, boolean areContactsWritable) {
+            this.accountType = accountType;
+            this.dataSet = dataSet;
+            mAreContactsWritable = areContactsWritable;
+        }
+
+        @Override
+        public boolean areContactsWritable() {
+            return mAreContactsWritable;
+        }
+
+        @Override
+        public int getHeaderColor(Context context) {
+            return 0;
+        }
+
+        @Override
+        public int getSideBarColor(Context context) {
+            return 0;
+        }
+
+        @Override
+        public boolean isGroupMembershipEditable() {
+            return true;
+        }
+    }
+}
diff --git a/tests/src/com/android/contacts/model/AccountWithDataSetTest.java b/tests/src/com/android/contacts/model/AccountWithDataSetTest.java
new file mode 100644
index 0000000..27c106e
--- /dev/null
+++ b/tests/src/com/android/contacts/model/AccountWithDataSetTest.java
@@ -0,0 +1,122 @@
+/*
+ * 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.model;
+
+import com.google.common.collect.Lists;
+
+import android.os.Bundle;
+import android.test.AndroidTestCase;
+import android.test.MoreAsserts;
+import android.test.suitebuilder.annotation.SmallTest;
+
+import java.util.List;
+
+/**
+ * Test case for {@link AccountWithDataSet}.
+ *
+ * adb shell am instrument -w -e class com.android.contacts.model.AccountWithDataSetTest \
+       com.android.contacts.tests/android.test.InstrumentationTestRunner
+ */
+@SmallTest
+public class AccountWithDataSetTest extends AndroidTestCase {
+    public void testStringifyAndUnstringify() {
+        AccountWithDataSet a1 = new AccountWithDataSet("name1", "typeA", null);
+        AccountWithDataSet a2 = new AccountWithDataSet("name2", "typeB", null);
+        AccountWithDataSet a3 = new AccountWithDataSet("name3", "typeB", "dataset");
+
+        // stringify() & unstringify
+        AccountWithDataSet a1r = AccountWithDataSet.unstringify(a1.stringify());
+        AccountWithDataSet a2r = AccountWithDataSet.unstringify(a2.stringify());
+        AccountWithDataSet a3r = AccountWithDataSet.unstringify(a3.stringify());
+
+        assertEquals(a1, a1r);
+        assertEquals(a2, a2r);
+        assertEquals(a3, a3r);
+
+        MoreAsserts.assertNotEqual(a1, a2r);
+        MoreAsserts.assertNotEqual(a1, a3r);
+
+        MoreAsserts.assertNotEqual(a2, a1r);
+        MoreAsserts.assertNotEqual(a2, a3r);
+
+        MoreAsserts.assertNotEqual(a3, a1r);
+        MoreAsserts.assertNotEqual(a3, a2r);
+    }
+
+    public void testStringifyListAndUnstringify() {
+        AccountWithDataSet a1 = new AccountWithDataSet("name1", "typeA", null);
+        AccountWithDataSet a2 = new AccountWithDataSet("name2", "typeB", null);
+        AccountWithDataSet a3 = new AccountWithDataSet("name3", "typeB", "dataset");
+
+        // Empty list
+        assertEquals(0, stringifyListAndUnstringify().size());
+
+        // 1 element
+        final List<AccountWithDataSet> listA = stringifyListAndUnstringify(a1);
+        assertEquals(1, listA.size());
+        assertEquals(a1, listA.get(0));
+
+        // 2 elements
+        final List<AccountWithDataSet> listB = stringifyListAndUnstringify(a2, a1);
+        assertEquals(2, listB.size());
+        assertEquals(a2, listB.get(0));
+        assertEquals(a1, listB.get(1));
+
+        // 3 elements
+        final List<AccountWithDataSet> listC = stringifyListAndUnstringify(a3, a2, a1);
+        assertEquals(3, listC.size());
+        assertEquals(a3, listC.get(0));
+        assertEquals(a2, listC.get(1));
+        assertEquals(a1, listC.get(2));
+    }
+
+    private static List<AccountWithDataSet> stringifyListAndUnstringify(
+            AccountWithDataSet... accounts) {
+
+        List<AccountWithDataSet> list = Lists.newArrayList(accounts);
+        return AccountWithDataSet.unstringifyList(AccountWithDataSet.stringifyList(list));
+    }
+
+    public void testParcelable() {
+        AccountWithDataSet a1 = new AccountWithDataSet("name1", "typeA", null);
+        AccountWithDataSet a2 = new AccountWithDataSet("name2", "typeB", null);
+        AccountWithDataSet a3 = new AccountWithDataSet("name3", "typeB", "dataset");
+
+        // Parcel them & unpercel.
+        final Bundle b = new Bundle();
+        b.putParcelable("a1", a1);
+        b.putParcelable("a2", a2);
+        b.putParcelable("a3", a3);
+
+        AccountWithDataSet a1r = b.getParcelable("a1");
+        AccountWithDataSet a2r = b.getParcelable("a2");
+        AccountWithDataSet a3r = b.getParcelable("a3");
+
+        assertEquals(a1, a1r);
+        assertEquals(a2, a2r);
+        assertEquals(a3, a3r);
+
+        MoreAsserts.assertNotEqual(a1, a2r);
+        MoreAsserts.assertNotEqual(a1, a3r);
+
+        MoreAsserts.assertNotEqual(a2, a1r);
+        MoreAsserts.assertNotEqual(a2, a3r);
+
+        MoreAsserts.assertNotEqual(a3, a1r);
+        MoreAsserts.assertNotEqual(a3, a2r);
+    }
+}
diff --git a/tests/src/com/android/contacts/tests/mocks/MockAccountTypeManager.java b/tests/src/com/android/contacts/tests/mocks/MockAccountTypeManager.java
index 7a04ae3..3b712c7 100644
--- a/tests/src/com/android/contacts/tests/mocks/MockAccountTypeManager.java
+++ b/tests/src/com/android/contacts/tests/mocks/MockAccountTypeManager.java
@@ -19,9 +19,11 @@
 import com.android.contacts.model.AccountTypeManager;
 import com.android.contacts.model.AccountTypeWithDataSet;
 import com.android.contacts.model.AccountWithDataSet;
+import com.google.android.collect.Lists;
 import com.google.android.collect.Maps;
 
 import java.util.Arrays;
+import java.util.Collection;
 import java.util.List;
 import java.util.Map;
 
@@ -32,8 +34,8 @@
  */
 public class MockAccountTypeManager extends AccountTypeManager {
 
-    private final AccountType[] mTypes;
-    private AccountWithDataSet[] mAccounts;
+    public AccountType[] mTypes;
+    public AccountWithDataSet[] mAccounts;
 
     public MockAccountTypeManager(AccountType[] types, AccountWithDataSet[] accounts) {
         this.mTypes = types;
@@ -60,4 +62,17 @@
     public Map<AccountTypeWithDataSet, AccountType> getInvitableAccountTypes() {
         return Maps.newHashMap(); // Always returns empty
     }
+
+    @Override
+    public List<AccountType> getAccountTypes(boolean writableOnly) {
+        final List<AccountType> ret = Lists.newArrayList();
+        synchronized (this) {
+            for (AccountType type : mTypes) {
+                if (!writableOnly || type.areContactsWritable()) {
+                    ret.add(type);
+                }
+            }
+        }
+        return ret;
+    }
 }