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

Commit 64d95bae authored by Makoto Onuki's avatar Makoto Onuki Committed by Android (Google) Code Review
Browse files

Merge "Utility methods for new contact editor flow" into ics-factoryrom

parents 1599d252 558669da
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