Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit a2aaa653 authored by Makoto Onuki's avatar Makoto Onuki Committed by Android Git Automerger
Browse files

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

* commit '64d95bae':
  Utility methods for new contact editor flow
parents 349d2199 64d95bae
Loading
Loading
Loading
Loading
+249 −0
Original line number Diff line number Diff line
/*
 * 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);
    }
}
+20 −0
Original line number Diff line number Diff line
@@ -112,6 +112,13 @@ public abstract class AccountTypeManager {
        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 @@ class AccountTypeManagerImpl extends AccountTypeManager
        }
        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;
    }
}
+95 −2
Original line number Diff line number Diff line
@@ -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 @@ public class AccountWithDataSet extends Account {
        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 class AccountWithDataSet extends Account {
    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;
    }
}
+310 −0

File added.

Preview size limit exceeded, changes collapsed.

+122 −0
Original line number Diff line number Diff line
/*
 * 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);
    }
}
Loading