summaryrefslogtreecommitdiff
path: root/java/com/android/contacts/common/model
diff options
context:
space:
mode:
Diffstat (limited to 'java/com/android/contacts/common/model')
-rw-r--r--java/com/android/contacts/common/model/AccountTypeManager.java813
-rw-r--r--java/com/android/contacts/common/model/BuilderWrapper.java53
-rw-r--r--java/com/android/contacts/common/model/CPOWrapper.java50
-rw-r--r--java/com/android/contacts/common/model/Contact.java384
-rw-r--r--java/com/android/contacts/common/model/ContactLoader.java998
-rw-r--r--java/com/android/contacts/common/model/RawContact.java351
-rw-r--r--java/com/android/contacts/common/model/account/AccountType.java501
-rw-r--r--java/com/android/contacts/common/model/account/AccountTypeWithDataSet.java103
-rw-r--r--java/com/android/contacts/common/model/account/AccountWithDataSet.java229
-rw-r--r--java/com/android/contacts/common/model/account/BaseAccountType.java1890
-rw-r--r--java/com/android/contacts/common/model/account/ExchangeAccountType.java365
-rw-r--r--java/com/android/contacts/common/model/account/ExternalAccountType.java443
-rw-r--r--java/com/android/contacts/common/model/account/FallbackAccountType.java77
-rw-r--r--java/com/android/contacts/common/model/account/GoogleAccountType.java206
-rw-r--r--java/com/android/contacts/common/model/account/SamsungAccountType.java235
-rw-r--r--java/com/android/contacts/common/model/dataitem/DataItem.java258
-rw-r--r--java/com/android/contacts/common/model/dataitem/DataKind.java132
-rw-r--r--java/com/android/contacts/common/model/dataitem/EmailDataItem.java47
-rw-r--r--java/com/android/contacts/common/model/dataitem/EventDataItem.java62
-rw-r--r--java/com/android/contacts/common/model/dataitem/GroupMembershipDataItem.java40
-rw-r--r--java/com/android/contacts/common/model/dataitem/IdentityDataItem.java39
-rw-r--r--java/com/android/contacts/common/model/dataitem/ImDataItem.java109
-rw-r--r--java/com/android/contacts/common/model/dataitem/NicknameDataItem.java39
-rw-r--r--java/com/android/contacts/common/model/dataitem/NoteDataItem.java35
-rw-r--r--java/com/android/contacts/common/model/dataitem/OrganizationDataItem.java64
-rw-r--r--java/com/android/contacts/common/model/dataitem/PhoneDataItem.java76
-rw-r--r--java/com/android/contacts/common/model/dataitem/PhotoDataItem.java39
-rw-r--r--java/com/android/contacts/common/model/dataitem/RelationDataItem.java62
-rw-r--r--java/com/android/contacts/common/model/dataitem/SipAddressDataItem.java40
-rw-r--r--java/com/android/contacts/common/model/dataitem/StructuredNameDataItem.java100
-rw-r--r--java/com/android/contacts/common/model/dataitem/StructuredPostalDataItem.java68
-rw-r--r--java/com/android/contacts/common/model/dataitem/WebsiteDataItem.java39
32 files changed, 7947 insertions, 0 deletions
diff --git a/java/com/android/contacts/common/model/AccountTypeManager.java b/java/com/android/contacts/common/model/AccountTypeManager.java
new file mode 100644
index 000000000..f225ff6ac
--- /dev/null
+++ b/java/com/android/contacts/common/model/AccountTypeManager.java
@@ -0,0 +1,813 @@
+/*
+ * Copyright (C) 2009 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.common.model;
+
+import android.accounts.Account;
+import android.accounts.AccountManager;
+import android.accounts.AuthenticatorDescription;
+import android.accounts.OnAccountsUpdateListener;
+import android.content.BroadcastReceiver;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.SyncAdapterType;
+import android.content.SyncStatusObserver;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.net.Uri;
+import android.os.AsyncTask;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Looper;
+import android.os.Message;
+import android.os.SystemClock;
+import android.provider.ContactsContract;
+import android.support.annotation.VisibleForTesting;
+import android.text.TextUtils;
+import android.util.ArrayMap;
+import android.util.Log;
+import android.util.TimingLogger;
+import com.android.contacts.common.MoreContactUtils;
+import com.android.contacts.common.list.ContactListFilterController;
+import com.android.contacts.common.model.account.AccountType;
+import com.android.contacts.common.model.account.AccountTypeWithDataSet;
+import com.android.contacts.common.model.account.AccountWithDataSet;
+import com.android.contacts.common.model.account.ExchangeAccountType;
+import com.android.contacts.common.model.account.ExternalAccountType;
+import com.android.contacts.common.model.account.FallbackAccountType;
+import com.android.contacts.common.model.account.GoogleAccountType;
+import com.android.contacts.common.model.account.SamsungAccountType;
+import com.android.contacts.common.model.dataitem.DataKind;
+import com.android.contacts.common.util.Constants;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+/**
+ * Singleton holder for all parsed {@link AccountType} available on the system, typically filled
+ * through {@link PackageManager} queries.
+ */
+public abstract class AccountTypeManager {
+
+ static final String TAG = "AccountTypeManager";
+
+ private static final Object mInitializationLock = new Object();
+ private static AccountTypeManager mAccountTypeManager;
+
+ /**
+ * Requests the singleton instance of {@link AccountTypeManager} with data bound from the
+ * available authenticators. This method can safely be called from the UI thread.
+ */
+ public static AccountTypeManager getInstance(Context context) {
+ synchronized (mInitializationLock) {
+ if (mAccountTypeManager == null) {
+ context = context.getApplicationContext();
+ mAccountTypeManager = new AccountTypeManagerImpl(context);
+ }
+ }
+ return mAccountTypeManager;
+ }
+
+ /**
+ * Set the instance of account type manager. This is only for and should only be used by unit
+ * tests. While having this method is not ideal, it's simpler than the alternative of holding this
+ * as a service in the ContactsApplication context class.
+ *
+ * @param mockManager The mock AccountTypeManager.
+ */
+ public static void setInstanceForTest(AccountTypeManager mockManager) {
+ synchronized (mInitializationLock) {
+ mAccountTypeManager = mockManager;
+ }
+ }
+
+ /**
+ * Returns the list of all accounts (if contactWritableOnly is false) or just the list of contact
+ * writable accounts (if contactWritableOnly is true).
+ */
+ // TODO: Consider splitting this into getContactWritableAccounts() and getAllAccounts()
+ public abstract List<AccountWithDataSet> getAccounts(boolean contactWritableOnly);
+
+ /** Returns the list of accounts that are group writable. */
+ public abstract List<AccountWithDataSet> getGroupWritableAccounts();
+
+ public abstract AccountType getAccountType(AccountTypeWithDataSet accountTypeWithDataSet);
+
+ public final AccountType getAccountType(String accountType, String dataSet) {
+ return getAccountType(AccountTypeWithDataSet.get(accountType, dataSet));
+ }
+
+ public final AccountType getAccountTypeForAccount(AccountWithDataSet account) {
+ if (account != null) {
+ return getAccountType(account.getAccountTypeWithDataSet());
+ }
+ return getAccountType(null, null);
+ }
+
+ /**
+ * @return Unmodifiable map from {@link AccountTypeWithDataSet}s to {@link AccountType}s which
+ * support the "invite" feature and have one or more account.
+ * <p>This is a filtered down and more "usable" list compared to {@link
+ * #getAllInvitableAccountTypes}, where usable is defined as: (1) making sure that the app
+ * that contributed the account type is not disabled (in order to avoid presenting the user
+ * with an option that does nothing), and (2) that there is at least one raw contact with that
+ * account type in the database (assuming that the user probably doesn't use that account
+ * type).
+ * <p>Warning: Don't use on the UI thread because this can scan the database.
+ */
+ public abstract Map<AccountTypeWithDataSet, AccountType> getUsableInvitableAccountTypes();
+
+ /**
+ * Find the best {@link DataKind} matching the requested {@link AccountType#accountType}, {@link
+ * AccountType#dataSet}, and {@link DataKind#mimeType}. If no direct match found, we try searching
+ * {@link FallbackAccountType}.
+ */
+ public DataKind getKindOrFallback(AccountType type, String mimeType) {
+ 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);
+
+ /**
+ * @param contactWritableOnly if true, it only returns ones that support writing contacts.
+ * @return true when this instance contains the given account.
+ */
+ public boolean contains(AccountWithDataSet account, boolean contactWritableOnly) {
+ for (AccountWithDataSet account_2 : getAccounts(false)) {
+ if (account.equals(account_2)) {
+ return true;
+ }
+ }
+ return false;
+ }
+}
+
+class AccountTypeManagerImpl extends AccountTypeManager
+ implements OnAccountsUpdateListener, SyncStatusObserver {
+
+ private static final Map<AccountTypeWithDataSet, AccountType>
+ EMPTY_UNMODIFIABLE_ACCOUNT_TYPE_MAP =
+ Collections.unmodifiableMap(new HashMap<AccountTypeWithDataSet, AccountType>());
+
+ /**
+ * A sample contact URI used to test whether any activities will respond to an invitable intent
+ * with the given URI as the intent data. This doesn't need to be specific to a real contact
+ * because an app that intercepts the intent should probably do so for all types of contact URIs.
+ */
+ private static final Uri SAMPLE_CONTACT_URI = ContactsContract.Contacts.getLookupUri(1, "xxx");
+
+ private static final int MESSAGE_LOAD_DATA = 0;
+ private static final int MESSAGE_PROCESS_BROADCAST_INTENT = 1;
+ private static final Comparator<AccountWithDataSet> ACCOUNT_COMPARATOR =
+ new Comparator<AccountWithDataSet>() {
+ @Override
+ public int compare(AccountWithDataSet a, AccountWithDataSet b) {
+ if (Objects.equals(a.name, b.name)
+ && Objects.equals(a.type, b.type)
+ && Objects.equals(a.dataSet, b.dataSet)) {
+ return 0;
+ } else if (b.name == null || b.type == null) {
+ return -1;
+ } else if (a.name == null || a.type == null) {
+ return 1;
+ } else {
+ int diff = a.name.compareTo(b.name);
+ if (diff != 0) {
+ return diff;
+ }
+ diff = a.type.compareTo(b.type);
+ if (diff != 0) {
+ return diff;
+ }
+
+ // Accounts without data sets get sorted before those that have them.
+ if (a.dataSet != null) {
+ return b.dataSet == null ? 1 : a.dataSet.compareTo(b.dataSet);
+ } else {
+ return -1;
+ }
+ }
+ }
+ };
+ private final InvitableAccountTypeCache mInvitableAccountTypeCache;
+ /**
+ * The boolean value is equal to true if the {@link InvitableAccountTypeCache} has been
+ * initialized. False otherwise.
+ */
+ private final AtomicBoolean mInvitablesCacheIsInitialized = new AtomicBoolean(false);
+ /**
+ * The boolean value is equal to true if the {@link FindInvitablesTask} is still executing. False
+ * otherwise.
+ */
+ private final AtomicBoolean mInvitablesTaskIsRunning = new AtomicBoolean(false);
+
+ private final Handler mMainThreadHandler = new Handler(Looper.getMainLooper());
+ private Context mContext;
+ private final Runnable mCheckFilterValidityRunnable =
+ new Runnable() {
+ @Override
+ public void run() {
+ ContactListFilterController.getInstance(mContext).checkFilterValidity(true);
+ }
+ };
+ private AccountManager mAccountManager;
+ private AccountType mFallbackAccountType;
+ private List<AccountWithDataSet> mAccounts = new ArrayList<>();
+ private List<AccountWithDataSet> mContactWritableAccounts = new ArrayList<>();
+ private List<AccountWithDataSet> mGroupWritableAccounts = new ArrayList<>();
+ private Map<AccountTypeWithDataSet, AccountType> mAccountTypesWithDataSets = new ArrayMap<>();
+ private Map<AccountTypeWithDataSet, AccountType> mInvitableAccountTypes =
+ EMPTY_UNMODIFIABLE_ACCOUNT_TYPE_MAP;
+ private HandlerThread mListenerThread;
+ private Handler mListenerHandler;
+ private BroadcastReceiver mBroadcastReceiver =
+ new BroadcastReceiver() {
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ Message msg = mListenerHandler.obtainMessage(MESSAGE_PROCESS_BROADCAST_INTENT, intent);
+ mListenerHandler.sendMessage(msg);
+ }
+ };
+ /* A latch that ensures that asynchronous initialization completes before data is used */
+ private volatile CountDownLatch mInitializationLatch = new CountDownLatch(1);
+
+ /** Internal constructor that only performs initial parsing. */
+ public AccountTypeManagerImpl(Context context) {
+ mContext = context;
+ mFallbackAccountType = new FallbackAccountType(context);
+
+ mAccountManager = AccountManager.get(mContext);
+
+ mListenerThread = new HandlerThread("AccountChangeListener");
+ mListenerThread.start();
+ mListenerHandler =
+ new Handler(mListenerThread.getLooper()) {
+ @Override
+ public void handleMessage(Message msg) {
+ switch (msg.what) {
+ case MESSAGE_LOAD_DATA:
+ loadAccountsInBackground();
+ break;
+ case MESSAGE_PROCESS_BROADCAST_INTENT:
+ processBroadcastIntent((Intent) msg.obj);
+ break;
+ }
+ }
+ };
+
+ mInvitableAccountTypeCache = new InvitableAccountTypeCache();
+
+ // Request updates when packages or accounts change
+ IntentFilter filter = new IntentFilter(Intent.ACTION_PACKAGE_ADDED);
+ filter.addAction(Intent.ACTION_PACKAGE_REMOVED);
+ filter.addAction(Intent.ACTION_PACKAGE_CHANGED);
+ filter.addDataScheme("package");
+ mContext.registerReceiver(mBroadcastReceiver, filter);
+ IntentFilter sdFilter = new IntentFilter();
+ sdFilter.addAction(Intent.ACTION_EXTERNAL_APPLICATIONS_AVAILABLE);
+ sdFilter.addAction(Intent.ACTION_EXTERNAL_APPLICATIONS_UNAVAILABLE);
+ mContext.registerReceiver(mBroadcastReceiver, sdFilter);
+
+ // Request updates when locale is changed so that the order of each field will
+ // be able to be changed on the locale change.
+ filter = new IntentFilter(Intent.ACTION_LOCALE_CHANGED);
+ mContext.registerReceiver(mBroadcastReceiver, filter);
+
+ mAccountManager.addOnAccountsUpdatedListener(this, mListenerHandler, false);
+
+ ContentResolver.addStatusChangeListener(ContentResolver.SYNC_OBSERVER_TYPE_SETTINGS, this);
+
+ mListenerHandler.sendEmptyMessage(MESSAGE_LOAD_DATA);
+ }
+
+ /**
+ * Find a specific {@link AuthenticatorDescription} in the provided list that matches the given
+ * account type.
+ */
+ protected static AuthenticatorDescription findAuthenticator(
+ AuthenticatorDescription[] auths, String accountType) {
+ for (AuthenticatorDescription auth : auths) {
+ if (accountType.equals(auth.type)) {
+ return auth;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Return all {@link AccountType}s with at least one account which supports "invite", i.e. its
+ * {@link AccountType#getInviteContactActivityClassName()} is not empty.
+ */
+ @VisibleForTesting
+ static Map<AccountTypeWithDataSet, AccountType> findAllInvitableAccountTypes(
+ Context context,
+ Collection<AccountWithDataSet> accounts,
+ Map<AccountTypeWithDataSet, AccountType> accountTypesByTypeAndDataSet) {
+ Map<AccountTypeWithDataSet, AccountType> result = new ArrayMap<>();
+ for (AccountWithDataSet account : accounts) {
+ AccountTypeWithDataSet accountTypeWithDataSet = account.getAccountTypeWithDataSet();
+ AccountType type = accountTypesByTypeAndDataSet.get(accountTypeWithDataSet);
+ if (type == null) {
+ continue; // just in case
+ }
+ if (result.containsKey(accountTypeWithDataSet)) {
+ continue;
+ }
+
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(
+ TAG,
+ "Type "
+ + accountTypeWithDataSet
+ + " inviteClass="
+ + type.getInviteContactActivityClassName());
+ }
+ if (!TextUtils.isEmpty(type.getInviteContactActivityClassName())) {
+ result.put(accountTypeWithDataSet, type);
+ }
+ }
+ return Collections.unmodifiableMap(result);
+ }
+
+ @Override
+ public void onStatusChanged(int which) {
+ mListenerHandler.sendEmptyMessage(MESSAGE_LOAD_DATA);
+ }
+
+ public void processBroadcastIntent(Intent intent) {
+ mListenerHandler.sendEmptyMessage(MESSAGE_LOAD_DATA);
+ }
+
+ /* This notification will arrive on the background thread */
+ public void onAccountsUpdated(Account[] accounts) {
+ // Refresh to catch any changed accounts
+ loadAccountsInBackground();
+ }
+
+ /**
+ * Returns instantly if accounts and account types have already been loaded. Otherwise waits for
+ * the background thread to complete the loading.
+ */
+ void ensureAccountsLoaded() {
+ CountDownLatch latch = mInitializationLatch;
+ if (latch == null) {
+ return;
+ }
+ while (true) {
+ try {
+ latch.await();
+ return;
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ }
+ }
+ }
+
+ /**
+ * Loads account list and corresponding account types (potentially with data sets). Always called
+ * on a background thread.
+ */
+ protected void loadAccountsInBackground() {
+ if (Log.isLoggable(Constants.PERFORMANCE_TAG, Log.DEBUG)) {
+ Log.d(Constants.PERFORMANCE_TAG, "AccountTypeManager.loadAccountsInBackground start");
+ }
+ TimingLogger timings = new TimingLogger(TAG, "loadAccountsInBackground");
+ final long startTime = SystemClock.currentThreadTimeMillis();
+ final long startTimeWall = SystemClock.elapsedRealtime();
+
+ // Account types, keyed off the account type and data set concatenation.
+ final Map<AccountTypeWithDataSet, AccountType> accountTypesByTypeAndDataSet = new ArrayMap<>();
+
+ // The same AccountTypes, but keyed off {@link RawContacts#ACCOUNT_TYPE}. Since there can
+ // be multiple account types (with different data sets) for the same type of account, each
+ // type string may have multiple AccountType entries.
+ final Map<String, List<AccountType>> accountTypesByType = new ArrayMap<>();
+
+ final List<AccountWithDataSet> allAccounts = new ArrayList<>();
+ final List<AccountWithDataSet> contactWritableAccounts = new ArrayList<>();
+ final List<AccountWithDataSet> groupWritableAccounts = new ArrayList<>();
+ final Set<String> extensionPackages = new HashSet<>();
+
+ final AccountManager am = mAccountManager;
+
+ final SyncAdapterType[] syncs = ContentResolver.getSyncAdapterTypes();
+ final AuthenticatorDescription[] auths = am.getAuthenticatorTypes();
+
+ // First process sync adapters to find any that provide contact data.
+ for (SyncAdapterType sync : syncs) {
+ if (!ContactsContract.AUTHORITY.equals(sync.authority)) {
+ // Skip sync adapters that don't provide contact data.
+ continue;
+ }
+
+ // Look for the formatting details provided by each sync
+ // adapter, using the authenticator to find general resources.
+ final String type = sync.accountType;
+ final AuthenticatorDescription auth = findAuthenticator(auths, type);
+ if (auth == null) {
+ Log.w(TAG, "No authenticator found for type=" + type + ", ignoring it.");
+ continue;
+ }
+
+ AccountType accountType;
+ if (GoogleAccountType.ACCOUNT_TYPE.equals(type)) {
+ accountType = new GoogleAccountType(mContext, auth.packageName);
+ } else if (ExchangeAccountType.isExchangeType(type)) {
+ accountType = new ExchangeAccountType(mContext, auth.packageName, type);
+ } else if (SamsungAccountType.isSamsungAccountType(mContext, type, auth.packageName)) {
+ accountType = new SamsungAccountType(mContext, auth.packageName, type);
+ } else {
+ Log.d(
+ TAG, "Registering external account type=" + type + ", packageName=" + auth.packageName);
+ accountType = new ExternalAccountType(mContext, auth.packageName, false);
+ }
+ if (!accountType.isInitialized()) {
+ if (accountType.isEmbedded()) {
+ throw new IllegalStateException(
+ "Problem initializing embedded type " + accountType.getClass().getCanonicalName());
+ } else {
+ // Skip external account types that couldn't be initialized.
+ continue;
+ }
+ }
+
+ accountType.accountType = auth.type;
+ accountType.titleRes = auth.labelId;
+ accountType.iconRes = auth.iconId;
+
+ addAccountType(accountType, accountTypesByTypeAndDataSet, accountTypesByType);
+
+ // Check to see if the account type knows of any other non-sync-adapter packages
+ // that may provide other data sets of contact data.
+ extensionPackages.addAll(accountType.getExtensionPackageNames());
+ }
+
+ // If any extension packages were specified, process them as well.
+ if (!extensionPackages.isEmpty()) {
+ Log.d(TAG, "Registering " + extensionPackages.size() + " extension packages");
+ for (String extensionPackage : extensionPackages) {
+ ExternalAccountType accountType = new ExternalAccountType(mContext, extensionPackage, true);
+ if (!accountType.isInitialized()) {
+ // Skip external account types that couldn't be initialized.
+ continue;
+ }
+ if (!accountType.hasContactsMetadata()) {
+ Log.w(
+ TAG,
+ "Skipping extension package "
+ + extensionPackage
+ + " because"
+ + " it doesn't have the CONTACTS_STRUCTURE metadata");
+ continue;
+ }
+ if (TextUtils.isEmpty(accountType.accountType)) {
+ Log.w(
+ TAG,
+ "Skipping extension package "
+ + extensionPackage
+ + " because"
+ + " the CONTACTS_STRUCTURE metadata doesn't have the accountType"
+ + " attribute");
+ continue;
+ }
+ Log.d(
+ TAG,
+ "Registering extension package account type="
+ + accountType.accountType
+ + ", dataSet="
+ + accountType.dataSet
+ + ", packageName="
+ + extensionPackage);
+
+ addAccountType(accountType, accountTypesByTypeAndDataSet, accountTypesByType);
+ }
+ }
+ timings.addSplit("Loaded account types");
+
+ // Map in accounts to associate the account names with each account type entry.
+ Account[] accounts = mAccountManager.getAccounts();
+ for (Account account : accounts) {
+ boolean syncable = ContentResolver.getIsSyncable(account, ContactsContract.AUTHORITY) > 0;
+
+ if (syncable) {
+ List<AccountType> accountTypes = accountTypesByType.get(account.type);
+ if (accountTypes != null) {
+ // Add an account-with-data-set entry for each account type that is
+ // authenticated by this account.
+ for (AccountType accountType : accountTypes) {
+ AccountWithDataSet accountWithDataSet =
+ new AccountWithDataSet(account.name, account.type, accountType.dataSet);
+ allAccounts.add(accountWithDataSet);
+ if (accountType.areContactsWritable()) {
+ contactWritableAccounts.add(accountWithDataSet);
+ }
+ if (accountType.isGroupMembershipEditable()) {
+ groupWritableAccounts.add(accountWithDataSet);
+ }
+ }
+ }
+ }
+ }
+
+ Collections.sort(allAccounts, ACCOUNT_COMPARATOR);
+ Collections.sort(contactWritableAccounts, ACCOUNT_COMPARATOR);
+ Collections.sort(groupWritableAccounts, ACCOUNT_COMPARATOR);
+
+ timings.addSplit("Loaded accounts");
+
+ synchronized (this) {
+ mAccountTypesWithDataSets = accountTypesByTypeAndDataSet;
+ mAccounts = allAccounts;
+ mContactWritableAccounts = contactWritableAccounts;
+ mGroupWritableAccounts = groupWritableAccounts;
+ mInvitableAccountTypes =
+ findAllInvitableAccountTypes(mContext, allAccounts, accountTypesByTypeAndDataSet);
+ }
+
+ timings.dumpToLog();
+ final long endTimeWall = SystemClock.elapsedRealtime();
+ final long endTime = SystemClock.currentThreadTimeMillis();
+
+ Log.i(
+ TAG,
+ "Loaded meta-data for "
+ + mAccountTypesWithDataSets.size()
+ + " account types, "
+ + mAccounts.size()
+ + " accounts in "
+ + (endTimeWall - startTimeWall)
+ + "ms(wall) "
+ + (endTime - startTime)
+ + "ms(cpu)");
+
+ if (mInitializationLatch != null) {
+ mInitializationLatch.countDown();
+ mInitializationLatch = null;
+ }
+ if (Log.isLoggable(Constants.PERFORMANCE_TAG, Log.DEBUG)) {
+ Log.d(Constants.PERFORMANCE_TAG, "AccountTypeManager.loadAccountsInBackground finish");
+ }
+
+ // Check filter validity since filter may become obsolete after account update. It must be
+ // done from UI thread.
+ mMainThreadHandler.post(mCheckFilterValidityRunnable);
+ }
+
+ // Bookkeeping method for tracking the known account types in the given maps.
+ private void addAccountType(
+ AccountType accountType,
+ Map<AccountTypeWithDataSet, AccountType> accountTypesByTypeAndDataSet,
+ Map<String, List<AccountType>> accountTypesByType) {
+ accountTypesByTypeAndDataSet.put(accountType.getAccountTypeAndDataSet(), accountType);
+ List<AccountType> accountsForType = accountTypesByType.get(accountType.accountType);
+ if (accountsForType == null) {
+ accountsForType = new ArrayList<>();
+ }
+ accountsForType.add(accountType);
+ accountTypesByType.put(accountType.accountType, accountsForType);
+ }
+
+ /** Return list of all known, contact writable {@link AccountWithDataSet}'s. */
+ @Override
+ public List<AccountWithDataSet> getAccounts(boolean contactWritableOnly) {
+ ensureAccountsLoaded();
+ return contactWritableOnly ? mContactWritableAccounts : mAccounts;
+ }
+
+ /** Return the list of all known, group writable {@link AccountWithDataSet}'s. */
+ public List<AccountWithDataSet> getGroupWritableAccounts() {
+ ensureAccountsLoaded();
+ return mGroupWritableAccounts;
+ }
+
+ /**
+ * Find the best {@link DataKind} matching the requested {@link AccountType#accountType}, {@link
+ * AccountType#dataSet}, and {@link DataKind#mimeType}. If no direct match found, we try searching
+ * {@link FallbackAccountType}.
+ */
+ @Override
+ public DataKind getKindOrFallback(AccountType type, String mimeType) {
+ ensureAccountsLoaded();
+ DataKind kind = null;
+
+ // Try finding account type and kind matching request
+ if (type != null) {
+ kind = type.getKindForMimetype(mimeType);
+ }
+
+ if (kind == null) {
+ // Nothing found, so try fallback as last resort
+ kind = mFallbackAccountType.getKindForMimetype(mimeType);
+ }
+
+ if (kind == null) {
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "Unknown type=" + type + ", mime=" + mimeType);
+ }
+ }
+
+ return kind;
+ }
+
+ /** Return {@link AccountType} for the given account type and data set. */
+ @Override
+ public AccountType getAccountType(AccountTypeWithDataSet accountTypeWithDataSet) {
+ ensureAccountsLoaded();
+ synchronized (this) {
+ AccountType type = mAccountTypesWithDataSets.get(accountTypeWithDataSet);
+ return type != null ? type : mFallbackAccountType;
+ }
+ }
+
+ /**
+ * @return Unmodifiable map from {@link AccountTypeWithDataSet}s to {@link AccountType}s which
+ * support the "invite" feature and have one or more account. This is an unfiltered list. See
+ * {@link #getUsableInvitableAccountTypes()}.
+ */
+ private Map<AccountTypeWithDataSet, AccountType> getAllInvitableAccountTypes() {
+ ensureAccountsLoaded();
+ return mInvitableAccountTypes;
+ }
+
+ @Override
+ public Map<AccountTypeWithDataSet, AccountType> getUsableInvitableAccountTypes() {
+ ensureAccountsLoaded();
+ // Since this method is not thread-safe, it's possible for multiple threads to encounter
+ // the situation where (1) the cache has not been initialized yet or
+ // (2) an async task to refresh the account type list in the cache has already been
+ // started. Hence we use {@link AtomicBoolean}s and return cached values immediately
+ // while we compute the actual result in the background. We use this approach instead of
+ // using "synchronized" because computing the account type list involves a DB read, and
+ // can potentially cause a deadlock situation if this method is called from code which
+ // holds the DB lock. The trade-off of potentially having an incorrect list of invitable
+ // account types for a short period of time seems more manageable than enforcing the
+ // context in which this method is called.
+
+ // Computing the list of usable invitable account types is done on the fly as requested.
+ // If this method has never been called before, then block until the list has been computed.
+ if (!mInvitablesCacheIsInitialized.get()) {
+ mInvitableAccountTypeCache.setCachedValue(findUsableInvitableAccountTypes(mContext));
+ mInvitablesCacheIsInitialized.set(true);
+ } else {
+ // Otherwise, there is a value in the cache. If the value has expired and
+ // an async task has not already been started by another thread, then kick off a new
+ // async task to compute the list.
+ if (mInvitableAccountTypeCache.isExpired()
+ && mInvitablesTaskIsRunning.compareAndSet(false, true)) {
+ new FindInvitablesTask().execute();
+ }
+ }
+
+ return mInvitableAccountTypeCache.getCachedValue();
+ }
+
+ /**
+ * Return all usable {@link AccountType}s that support the "invite" feature from the list of all
+ * potential invitable account types (retrieved from {@link #getAllInvitableAccountTypes}). A
+ * usable invitable account type means: (1) there is at least 1 raw contact in the database with
+ * that account type, and (2) the app contributing the account type is not disabled.
+ *
+ * <p>Warning: Don't use on the UI thread because this can scan the database.
+ */
+ private Map<AccountTypeWithDataSet, AccountType> findUsableInvitableAccountTypes(
+ Context context) {
+ Map<AccountTypeWithDataSet, AccountType> allInvitables = getAllInvitableAccountTypes();
+ if (allInvitables.isEmpty()) {
+ return EMPTY_UNMODIFIABLE_ACCOUNT_TYPE_MAP;
+ }
+
+ final Map<AccountTypeWithDataSet, AccountType> result = new ArrayMap<>();
+ result.putAll(allInvitables);
+
+ final PackageManager packageManager = context.getPackageManager();
+ for (AccountTypeWithDataSet accountTypeWithDataSet : allInvitables.keySet()) {
+ AccountType accountType = allInvitables.get(accountTypeWithDataSet);
+
+ // Make sure that account types don't come from apps that are disabled.
+ Intent invitableIntent = MoreContactUtils.getInvitableIntent(accountType, SAMPLE_CONTACT_URI);
+ if (invitableIntent == null) {
+ result.remove(accountTypeWithDataSet);
+ continue;
+ }
+ ResolveInfo resolveInfo =
+ packageManager.resolveActivity(invitableIntent, PackageManager.MATCH_DEFAULT_ONLY);
+ if (resolveInfo == null) {
+ // If we can't find an activity to start for this intent, then there's no point in
+ // showing this option to the user.
+ result.remove(accountTypeWithDataSet);
+ continue;
+ }
+
+ // Make sure that there is at least 1 raw contact with this account type. This check
+ // is non-trivial and should not be done on the UI thread.
+ if (!accountTypeWithDataSet.hasData(context)) {
+ result.remove(accountTypeWithDataSet);
+ }
+ }
+
+ return Collections.unmodifiableMap(result);
+ }
+
+ @Override
+ public List<AccountType> getAccountTypes(boolean contactWritableOnly) {
+ ensureAccountsLoaded();
+ final List<AccountType> accountTypes = new ArrayList<>();
+ synchronized (this) {
+ for (AccountType type : mAccountTypesWithDataSets.values()) {
+ if (!contactWritableOnly || type.areContactsWritable()) {
+ accountTypes.add(type);
+ }
+ }
+ }
+ return accountTypes;
+ }
+
+ /**
+ * This cache holds a list of invitable {@link AccountTypeWithDataSet}s, in the form of a {@link
+ * Map<AccountTypeWithDataSet, AccountType>}. Note that the cached value is valid only for {@link
+ * #TIME_TO_LIVE} milliseconds.
+ */
+ private static final class InvitableAccountTypeCache {
+
+ /**
+ * The cached {@link #mInvitableAccountTypes} list expires after this number of milliseconds has
+ * elapsed.
+ */
+ private static final long TIME_TO_LIVE = 60000;
+
+ private Map<AccountTypeWithDataSet, AccountType> mInvitableAccountTypes;
+
+ private long mTimeLastSet;
+
+ /**
+ * Returns true if the data in this cache is stale and needs to be refreshed. Returns false
+ * otherwise.
+ */
+ public boolean isExpired() {
+ return SystemClock.elapsedRealtime() - mTimeLastSet > TIME_TO_LIVE;
+ }
+
+ /**
+ * Returns the cached value. Note that the caller is responsible for checking {@link
+ * #isExpired()} to ensure that the value is not stale.
+ */
+ public Map<AccountTypeWithDataSet, AccountType> getCachedValue() {
+ return mInvitableAccountTypes;
+ }
+
+ public void setCachedValue(Map<AccountTypeWithDataSet, AccountType> map) {
+ mInvitableAccountTypes = map;
+ mTimeLastSet = SystemClock.elapsedRealtime();
+ }
+ }
+
+ /**
+ * Background task to find all usable {@link AccountType}s that support the "invite" feature from
+ * the list of all potential invitable account types. Once the work is completed, the list of
+ * account types is stored in the {@link AccountTypeManager}'s {@link InvitableAccountTypeCache}.
+ */
+ private class FindInvitablesTask
+ extends AsyncTask<Void, Void, Map<AccountTypeWithDataSet, AccountType>> {
+
+ @Override
+ protected Map<AccountTypeWithDataSet, AccountType> doInBackground(Void... params) {
+ return findUsableInvitableAccountTypes(mContext);
+ }
+
+ @Override
+ protected void onPostExecute(Map<AccountTypeWithDataSet, AccountType> accountTypes) {
+ mInvitableAccountTypeCache.setCachedValue(accountTypes);
+ mInvitablesTaskIsRunning.set(false);
+ }
+ }
+}
diff --git a/java/com/android/contacts/common/model/BuilderWrapper.java b/java/com/android/contacts/common/model/BuilderWrapper.java
new file mode 100644
index 000000000..9c666e59c
--- /dev/null
+++ b/java/com/android/contacts/common/model/BuilderWrapper.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2015 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.common.model;
+
+import android.content.ContentProviderOperation.Builder;
+
+/**
+ * This class is created for the purpose of compatibility and make the type of
+ * ContentProviderOperation available on pre-M SDKs. Since ContentProviderOperation is usually
+ * created by Builder and we don’t have access to the type via Builder, so we need to create a
+ * wrapper class for Builder first and include type. Then we could use the builder and the type in
+ * this class to create a wrapper of ContentProviderOperation.
+ */
+public class BuilderWrapper {
+
+ private Builder mBuilder;
+ private int mType;
+
+ public BuilderWrapper(Builder builder, int type) {
+ mBuilder = builder;
+ mType = type;
+ }
+
+ public int getType() {
+ return mType;
+ }
+
+ public void setType(int mType) {
+ this.mType = mType;
+ }
+
+ public Builder getBuilder() {
+ return mBuilder;
+ }
+
+ public void setBuilder(Builder mBuilder) {
+ this.mBuilder = mBuilder;
+ }
+}
diff --git a/java/com/android/contacts/common/model/CPOWrapper.java b/java/com/android/contacts/common/model/CPOWrapper.java
new file mode 100644
index 000000000..4a67e6700
--- /dev/null
+++ b/java/com/android/contacts/common/model/CPOWrapper.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2015 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.common.model;
+
+import android.content.ContentProviderOperation;
+
+/**
+ * This class is created for the purpose of compatibility and make the type of
+ * ContentProviderOperation available on pre-M SDKs.
+ */
+public class CPOWrapper {
+
+ private ContentProviderOperation mOperation;
+ private int mType;
+
+ public CPOWrapper(ContentProviderOperation builder, int type) {
+ mOperation = builder;
+ mType = type;
+ }
+
+ public int getType() {
+ return mType;
+ }
+
+ public void setType(int type) {
+ this.mType = type;
+ }
+
+ public ContentProviderOperation getOperation() {
+ return mOperation;
+ }
+
+ public void setOperation(ContentProviderOperation operation) {
+ this.mOperation = operation;
+ }
+}
diff --git a/java/com/android/contacts/common/model/Contact.java b/java/com/android/contacts/common/model/Contact.java
new file mode 100644
index 000000000..ad0b66efe
--- /dev/null
+++ b/java/com/android/contacts/common/model/Contact.java
@@ -0,0 +1,384 @@
+/*
+ * Copyright (C) 2012 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.common.model;
+
+import android.content.ContentValues;
+import android.net.Uri;
+import android.provider.ContactsContract.CommonDataKinds.Photo;
+import android.provider.ContactsContract.Data;
+import android.provider.ContactsContract.Directory;
+import android.provider.ContactsContract.DisplayNameSources;
+import android.support.annotation.VisibleForTesting;
+import com.android.contacts.common.GroupMetaData;
+import com.android.contacts.common.model.account.AccountType;
+import com.google.common.collect.ImmutableList;
+import java.util.ArrayList;
+
+/**
+ * A Contact represents a single person or logical entity as perceived by the user. The information
+ * about a contact can come from multiple data sources, which are each represented by a RawContact
+ * object. Thus, a Contact is associated with a collection of RawContact objects.
+ *
+ * <p>The aggregation of raw contacts into a single contact is performed automatically, and it is
+ * also possible for users to manually split and join raw contacts into various contacts.
+ *
+ * <p>Only the {@link ContactLoader} class can create a Contact object with various flags to allow
+ * partial loading of contact data. Thus, an instance of this class should be treated as a read-only
+ * object.
+ */
+public class Contact {
+
+ private final Uri mRequestedUri;
+ private final Uri mLookupUri;
+ private final Uri mUri;
+ private final long mDirectoryId;
+ private final String mLookupKey;
+ private final long mId;
+ private final long mNameRawContactId;
+ private final int mDisplayNameSource;
+ private final long mPhotoId;
+ private final String mPhotoUri;
+ private final String mDisplayName;
+ private final String mAltDisplayName;
+ private final String mPhoneticName;
+ private final boolean mStarred;
+ private final Integer mPresence;
+ private final boolean mSendToVoicemail;
+ private final String mCustomRingtone;
+ private final boolean mIsUserProfile;
+ private final Contact.Status mStatus;
+ private final Exception mException;
+ private ImmutableList<RawContact> mRawContacts;
+ private ImmutableList<AccountType> mInvitableAccountTypes;
+ private String mDirectoryDisplayName;
+ private String mDirectoryType;
+ private String mDirectoryAccountType;
+ private String mDirectoryAccountName;
+ private int mDirectoryExportSupport;
+ private ImmutableList<GroupMetaData> mGroups;
+ private byte[] mPhotoBinaryData;
+ /**
+ * Small version of the contact photo loaded from a blob instead of from a file. If a large
+ * contact photo is not available yet, then this has the same value as mPhotoBinaryData.
+ */
+ private byte[] mThumbnailPhotoBinaryData;
+
+ /** Constructor for special results, namely "no contact found" and "error". */
+ private Contact(Uri requestedUri, Contact.Status status, Exception exception) {
+ if (status == Status.ERROR && exception == null) {
+ throw new IllegalArgumentException("ERROR result must have exception");
+ }
+ mStatus = status;
+ mException = exception;
+ mRequestedUri = requestedUri;
+ mLookupUri = null;
+ mUri = null;
+ mDirectoryId = -1;
+ mLookupKey = null;
+ mId = -1;
+ mRawContacts = null;
+ mNameRawContactId = -1;
+ mDisplayNameSource = DisplayNameSources.UNDEFINED;
+ mPhotoId = -1;
+ mPhotoUri = null;
+ mDisplayName = null;
+ mAltDisplayName = null;
+ mPhoneticName = null;
+ mStarred = false;
+ mPresence = null;
+ mInvitableAccountTypes = null;
+ mSendToVoicemail = false;
+ mCustomRingtone = null;
+ mIsUserProfile = false;
+ }
+
+ /** Constructor to call when contact was found */
+ public Contact(
+ Uri requestedUri,
+ Uri uri,
+ Uri lookupUri,
+ long directoryId,
+ String lookupKey,
+ long id,
+ long nameRawContactId,
+ int displayNameSource,
+ long photoId,
+ String photoUri,
+ String displayName,
+ String altDisplayName,
+ String phoneticName,
+ boolean starred,
+ Integer presence,
+ boolean sendToVoicemail,
+ String customRingtone,
+ boolean isUserProfile) {
+ mStatus = Status.LOADED;
+ mException = null;
+ mRequestedUri = requestedUri;
+ mLookupUri = lookupUri;
+ mUri = uri;
+ mDirectoryId = directoryId;
+ mLookupKey = lookupKey;
+ mId = id;
+ mRawContacts = null;
+ mNameRawContactId = nameRawContactId;
+ mDisplayNameSource = displayNameSource;
+ mPhotoId = photoId;
+ mPhotoUri = photoUri;
+ mDisplayName = displayName;
+ mAltDisplayName = altDisplayName;
+ mPhoneticName = phoneticName;
+ mStarred = starred;
+ mPresence = presence;
+ mInvitableAccountTypes = null;
+ mSendToVoicemail = sendToVoicemail;
+ mCustomRingtone = customRingtone;
+ mIsUserProfile = isUserProfile;
+ }
+
+ public Contact(Uri requestedUri, Contact from) {
+ mRequestedUri = requestedUri;
+
+ mStatus = from.mStatus;
+ mException = from.mException;
+ mLookupUri = from.mLookupUri;
+ mUri = from.mUri;
+ mDirectoryId = from.mDirectoryId;
+ mLookupKey = from.mLookupKey;
+ mId = from.mId;
+ mNameRawContactId = from.mNameRawContactId;
+ mDisplayNameSource = from.mDisplayNameSource;
+ mPhotoId = from.mPhotoId;
+ mPhotoUri = from.mPhotoUri;
+ mDisplayName = from.mDisplayName;
+ mAltDisplayName = from.mAltDisplayName;
+ mPhoneticName = from.mPhoneticName;
+ mStarred = from.mStarred;
+ mPresence = from.mPresence;
+ mRawContacts = from.mRawContacts;
+ mInvitableAccountTypes = from.mInvitableAccountTypes;
+
+ mDirectoryDisplayName = from.mDirectoryDisplayName;
+ mDirectoryType = from.mDirectoryType;
+ mDirectoryAccountType = from.mDirectoryAccountType;
+ mDirectoryAccountName = from.mDirectoryAccountName;
+ mDirectoryExportSupport = from.mDirectoryExportSupport;
+
+ mGroups = from.mGroups;
+
+ mPhotoBinaryData = from.mPhotoBinaryData;
+ mSendToVoicemail = from.mSendToVoicemail;
+ mCustomRingtone = from.mCustomRingtone;
+ mIsUserProfile = from.mIsUserProfile;
+ }
+
+ public static Contact forError(Uri requestedUri, Exception exception) {
+ return new Contact(requestedUri, Status.ERROR, exception);
+ }
+
+ public static Contact forNotFound(Uri requestedUri) {
+ return new Contact(requestedUri, Status.NOT_FOUND, null);
+ }
+
+ /** @param exportSupport See {@link Directory#EXPORT_SUPPORT}. */
+ public void setDirectoryMetaData(
+ String displayName,
+ String directoryType,
+ String accountType,
+ String accountName,
+ int exportSupport) {
+ mDirectoryDisplayName = displayName;
+ mDirectoryType = directoryType;
+ mDirectoryAccountType = accountType;
+ mDirectoryAccountName = accountName;
+ mDirectoryExportSupport = exportSupport;
+ }
+
+ /**
+ * Returns the URI for the contact that contains both the lookup key and the ID. This is the best
+ * URI to reference a contact. For directory contacts, this is the same a the URI as returned by
+ * {@link #getUri()}
+ */
+ public Uri getLookupUri() {
+ return mLookupUri;
+ }
+
+ public String getLookupKey() {
+ return mLookupKey;
+ }
+
+ /**
+ * Returns the contact Uri that was passed to the provider to make the query. This is the same as
+ * the requested Uri, unless the requested Uri doesn't specify a Contact: If it either references
+ * a Raw-Contact or a Person (a pre-Eclair style Uri), this Uri will always reference the full
+ * aggregate contact.
+ */
+ public Uri getUri() {
+ return mUri;
+ }
+
+ /** Returns the contact ID. */
+ @VisibleForTesting
+ public long getId() {
+ return mId;
+ }
+
+ /**
+ * @return true when an exception happened during loading, in which case {@link #getException}
+ * returns the actual exception object.
+ */
+ public boolean isError() {
+ return mStatus == Status.ERROR;
+ }
+
+ public Exception getException() {
+ return mException;
+ }
+
+ /** @return true if the specified contact is successfully loaded. */
+ public boolean isLoaded() {
+ return mStatus == Status.LOADED;
+ }
+
+ public long getNameRawContactId() {
+ return mNameRawContactId;
+ }
+
+ public int getDisplayNameSource() {
+ return mDisplayNameSource;
+ }
+
+ public long getPhotoId() {
+ return mPhotoId;
+ }
+
+ public String getPhotoUri() {
+ return mPhotoUri;
+ }
+
+ public String getDisplayName() {
+ return mDisplayName;
+ }
+
+ public boolean getStarred() {
+ return mStarred;
+ }
+
+ public Integer getPresence() {
+ return mPresence;
+ }
+
+ /**
+ * This can return non-null invitable account types only if the {@link ContactLoader} was
+ * configured to load invitable account types in its constructor.
+ */
+ public ImmutableList<AccountType> getInvitableAccountTypes() {
+ return mInvitableAccountTypes;
+ }
+
+ /* package */ void setInvitableAccountTypes(ImmutableList<AccountType> accountTypes) {
+ mInvitableAccountTypes = accountTypes;
+ }
+
+ public ImmutableList<RawContact> getRawContacts() {
+ return mRawContacts;
+ }
+
+ /* package */ void setRawContacts(ImmutableList<RawContact> rawContacts) {
+ mRawContacts = rawContacts;
+ }
+
+ public long getDirectoryId() {
+ return mDirectoryId;
+ }
+
+ public boolean isDirectoryEntry() {
+ return mDirectoryId != -1
+ && mDirectoryId != Directory.DEFAULT
+ && mDirectoryId != Directory.LOCAL_INVISIBLE;
+ }
+
+ /* package */ void setPhotoBinaryData(byte[] photoBinaryData) {
+ mPhotoBinaryData = photoBinaryData;
+ }
+
+ public byte[] getThumbnailPhotoBinaryData() {
+ return mThumbnailPhotoBinaryData;
+ }
+
+ /* package */ void setThumbnailPhotoBinaryData(byte[] photoBinaryData) {
+ mThumbnailPhotoBinaryData = photoBinaryData;
+ }
+
+ public ArrayList<ContentValues> getContentValues() {
+ if (mRawContacts.size() != 1) {
+ throw new IllegalStateException("Cannot extract content values from an aggregated contact");
+ }
+
+ RawContact rawContact = mRawContacts.get(0);
+ ArrayList<ContentValues> result = rawContact.getContentValues();
+
+ // If the photo was loaded using the URI, create an entry for the photo
+ // binary data.
+ if (mPhotoId == 0 && mPhotoBinaryData != null) {
+ ContentValues photo = new ContentValues();
+ photo.put(Data.MIMETYPE, Photo.CONTENT_ITEM_TYPE);
+ photo.put(Photo.PHOTO, mPhotoBinaryData);
+ result.add(photo);
+ }
+
+ return result;
+ }
+
+ /**
+ * This can return non-null group meta-data only if the {@link ContactLoader} was configured to
+ * load group metadata in its constructor.
+ */
+ public ImmutableList<GroupMetaData> getGroupMetaData() {
+ return mGroups;
+ }
+
+ /* package */ void setGroupMetaData(ImmutableList<GroupMetaData> groups) {
+ mGroups = groups;
+ }
+
+ public boolean isUserProfile() {
+ return mIsUserProfile;
+ }
+
+ @Override
+ public String toString() {
+ return "{requested="
+ + mRequestedUri
+ + ",lookupkey="
+ + mLookupKey
+ + ",uri="
+ + mUri
+ + ",status="
+ + mStatus
+ + "}";
+ }
+
+ private enum Status {
+ /** Contact is successfully loaded */
+ LOADED,
+ /** There was an error loading the contact */
+ ERROR,
+ /** Contact is not found */
+ NOT_FOUND,
+ }
+}
diff --git a/java/com/android/contacts/common/model/ContactLoader.java b/java/com/android/contacts/common/model/ContactLoader.java
new file mode 100644
index 000000000..eb16bffcd
--- /dev/null
+++ b/java/com/android/contacts/common/model/ContactLoader.java
@@ -0,0 +1,998 @@
+/*
+ * Copyright (C) 2010 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.common.model;
+
+import android.content.AsyncTaskLoader;
+import android.content.ContentResolver;
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.content.res.AssetFileDescriptor;
+import android.content.res.Resources;
+import android.database.Cursor;
+import android.net.Uri;
+import android.provider.ContactsContract;
+import android.provider.ContactsContract.CommonDataKinds.GroupMembership;
+import android.provider.ContactsContract.Contacts;
+import android.provider.ContactsContract.Data;
+import android.provider.ContactsContract.Directory;
+import android.provider.ContactsContract.Groups;
+import android.provider.ContactsContract.RawContacts;
+import android.text.TextUtils;
+import android.util.Log;
+import com.android.contacts.common.GeoUtil;
+import com.android.contacts.common.GroupMetaData;
+import com.android.contacts.common.model.account.AccountType;
+import com.android.contacts.common.model.account.AccountTypeWithDataSet;
+import com.android.contacts.common.model.dataitem.DataItem;
+import com.android.contacts.common.model.dataitem.PhoneDataItem;
+import com.android.contacts.common.model.dataitem.PhotoDataItem;
+import com.android.contacts.common.util.Constants;
+import com.android.contacts.common.util.ContactLoaderUtils;
+import com.android.contacts.common.util.UriUtils;
+import com.android.dialer.compat.CompatUtils;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import com.google.common.collect.Sets;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+/** Loads a single Contact and all it constituent RawContacts. */
+public class ContactLoader extends AsyncTaskLoader<Contact> {
+
+ private static final String TAG = ContactLoader.class.getSimpleName();
+
+ /** A short-lived cache that can be set by {@link #cacheResult()} */
+ private static Contact sCachedResult = null;
+
+ private final Uri mRequestedUri;
+ private final Set<Long> mNotifiedRawContactIds = Sets.newHashSet();
+ private Uri mLookupUri;
+ private boolean mLoadGroupMetaData;
+ private boolean mLoadInvitableAccountTypes;
+ private boolean mPostViewNotification;
+ private boolean mComputeFormattedPhoneNumber;
+ private Contact mContact;
+ private ForceLoadContentObserver mObserver;
+
+ public ContactLoader(Context context, Uri lookupUri, boolean postViewNotification) {
+ this(context, lookupUri, false, false, postViewNotification, false);
+ }
+
+ public ContactLoader(
+ Context context,
+ Uri lookupUri,
+ boolean loadGroupMetaData,
+ boolean loadInvitableAccountTypes,
+ boolean postViewNotification,
+ boolean computeFormattedPhoneNumber) {
+ super(context);
+ mLookupUri = lookupUri;
+ mRequestedUri = lookupUri;
+ mLoadGroupMetaData = loadGroupMetaData;
+ mLoadInvitableAccountTypes = loadInvitableAccountTypes;
+ mPostViewNotification = postViewNotification;
+ mComputeFormattedPhoneNumber = computeFormattedPhoneNumber;
+ }
+
+ /**
+ * Parses a {@link Contact} stored as a JSON string in a lookup URI.
+ *
+ * @param lookupUri The contact information to parse .
+ * @return The parsed {@code Contact} information.
+ */
+ public static Contact parseEncodedContactEntity(Uri lookupUri) {
+ try {
+ return loadEncodedContactEntity(lookupUri, lookupUri);
+ } catch (JSONException je) {
+ return null;
+ }
+ }
+
+ private static Contact loadEncodedContactEntity(Uri uri, Uri lookupUri) throws JSONException {
+ final String jsonString = uri.getEncodedFragment();
+ final JSONObject json = new JSONObject(jsonString);
+
+ final long directoryId =
+ Long.valueOf(uri.getQueryParameter(ContactsContract.DIRECTORY_PARAM_KEY));
+
+ final String displayName = json.optString(Contacts.DISPLAY_NAME);
+ final String altDisplayName = json.optString(Contacts.DISPLAY_NAME_ALTERNATIVE, displayName);
+ final int displayNameSource = json.getInt(Contacts.DISPLAY_NAME_SOURCE);
+ final String photoUri = json.optString(Contacts.PHOTO_URI, null);
+ final Contact contact =
+ new Contact(
+ uri,
+ uri,
+ lookupUri,
+ directoryId,
+ null /* lookupKey */,
+ -1 /* id */,
+ -1 /* nameRawContactId */,
+ displayNameSource,
+ 0 /* photoId */,
+ photoUri,
+ displayName,
+ altDisplayName,
+ null /* phoneticName */,
+ false /* starred */,
+ null /* presence */,
+ false /* sendToVoicemail */,
+ null /* customRingtone */,
+ false /* isUserProfile */);
+
+ final String accountName = json.optString(RawContacts.ACCOUNT_NAME, null);
+ final String directoryName = uri.getQueryParameter(Directory.DISPLAY_NAME);
+ if (accountName != null) {
+ final String accountType = json.getString(RawContacts.ACCOUNT_TYPE);
+ contact.setDirectoryMetaData(
+ directoryName,
+ null,
+ accountName,
+ accountType,
+ json.optInt(Directory.EXPORT_SUPPORT, Directory.EXPORT_SUPPORT_SAME_ACCOUNT_ONLY));
+ } else {
+ contact.setDirectoryMetaData(
+ directoryName,
+ null,
+ null,
+ null,
+ json.optInt(Directory.EXPORT_SUPPORT, Directory.EXPORT_SUPPORT_ANY_ACCOUNT));
+ }
+
+ final ContentValues values = new ContentValues();
+ values.put(Data._ID, -1);
+ values.put(Data.CONTACT_ID, -1);
+ final RawContact rawContact = new RawContact(values);
+
+ final JSONObject items = json.getJSONObject(Contacts.CONTENT_ITEM_TYPE);
+ final Iterator keys = items.keys();
+ while (keys.hasNext()) {
+ final String mimetype = (String) keys.next();
+
+ // Could be single object or array.
+ final JSONObject obj = items.optJSONObject(mimetype);
+ if (obj == null) {
+ final JSONArray array = items.getJSONArray(mimetype);
+ for (int i = 0; i < array.length(); i++) {
+ final JSONObject item = array.getJSONObject(i);
+ processOneRecord(rawContact, item, mimetype);
+ }
+ } else {
+ processOneRecord(rawContact, obj, mimetype);
+ }
+ }
+
+ contact.setRawContacts(new ImmutableList.Builder<RawContact>().add(rawContact).build());
+ return contact;
+ }
+
+ private static void processOneRecord(RawContact rawContact, JSONObject item, String mimetype)
+ throws JSONException {
+ final ContentValues itemValues = new ContentValues();
+ itemValues.put(Data.MIMETYPE, mimetype);
+ itemValues.put(Data._ID, -1);
+
+ final Iterator iterator = item.keys();
+ while (iterator.hasNext()) {
+ String name = (String) iterator.next();
+ final Object o = item.get(name);
+ if (o instanceof String) {
+ itemValues.put(name, (String) o);
+ } else if (o instanceof Integer) {
+ itemValues.put(name, (Integer) o);
+ }
+ }
+ rawContact.addDataItemValues(itemValues);
+ }
+
+ @Override
+ public Contact loadInBackground() {
+ Log.e(TAG, "loadInBackground=" + mLookupUri);
+ try {
+ final ContentResolver resolver = getContext().getContentResolver();
+ final Uri uriCurrentFormat = ContactLoaderUtils.ensureIsContactUri(resolver, mLookupUri);
+ final Contact cachedResult = sCachedResult;
+ sCachedResult = null;
+ // Is this the same Uri as what we had before already? In that case, reuse that result
+ final Contact result;
+ final boolean resultIsCached;
+ if (cachedResult != null && UriUtils.areEqual(cachedResult.getLookupUri(), mLookupUri)) {
+ // We are using a cached result from earlier. Below, we should make sure
+ // we are not doing any more network or disc accesses
+ result = new Contact(mRequestedUri, cachedResult);
+ resultIsCached = true;
+ } else {
+ if (uriCurrentFormat.getLastPathSegment().equals(Constants.LOOKUP_URI_ENCODED)) {
+ result = loadEncodedContactEntity(uriCurrentFormat, mLookupUri);
+ } else {
+ result = loadContactEntity(resolver, uriCurrentFormat);
+ }
+ resultIsCached = false;
+ }
+ if (result.isLoaded()) {
+ if (result.isDirectoryEntry()) {
+ if (!resultIsCached) {
+ loadDirectoryMetaData(result);
+ }
+ } else if (mLoadGroupMetaData) {
+ if (result.getGroupMetaData() == null) {
+ loadGroupMetaData(result);
+ }
+ }
+ if (mComputeFormattedPhoneNumber) {
+ computeFormattedPhoneNumbers(result);
+ }
+ if (!resultIsCached) {
+ loadPhotoBinaryData(result);
+ }
+
+ // Note ME profile should never have "Add connection"
+ if (mLoadInvitableAccountTypes && result.getInvitableAccountTypes() == null) {
+ loadInvitableAccountTypes(result);
+ }
+ }
+ return result;
+ } catch (Exception e) {
+ Log.e(TAG, "Error loading the contact: " + mLookupUri, e);
+ return Contact.forError(mRequestedUri, e);
+ }
+ }
+
+ private Contact loadContactEntity(ContentResolver resolver, Uri contactUri) {
+ Uri entityUri = Uri.withAppendedPath(contactUri, Contacts.Entity.CONTENT_DIRECTORY);
+ Cursor cursor =
+ resolver.query(entityUri, ContactQuery.COLUMNS, null, null, Contacts.Entity.RAW_CONTACT_ID);
+ if (cursor == null) {
+ Log.e(TAG, "No cursor returned in loadContactEntity");
+ return Contact.forNotFound(mRequestedUri);
+ }
+
+ try {
+ if (!cursor.moveToFirst()) {
+ cursor.close();
+ return Contact.forNotFound(mRequestedUri);
+ }
+
+ // Create the loaded contact starting with the header data.
+ Contact contact = loadContactHeaderData(cursor, contactUri);
+
+ // Fill in the raw contacts, which is wrapped in an Entity and any
+ // status data. Initially, result has empty entities and statuses.
+ long currentRawContactId = -1;
+ RawContact rawContact = null;
+ ImmutableList.Builder<RawContact> rawContactsBuilder =
+ new ImmutableList.Builder<RawContact>();
+ do {
+ long rawContactId = cursor.getLong(ContactQuery.RAW_CONTACT_ID);
+ if (rawContactId != currentRawContactId) {
+ // First time to see this raw contact id, so create a new entity, and
+ // add it to the result's entities.
+ currentRawContactId = rawContactId;
+ rawContact = new RawContact(loadRawContactValues(cursor));
+ rawContactsBuilder.add(rawContact);
+ }
+ if (!cursor.isNull(ContactQuery.DATA_ID)) {
+ ContentValues data = loadDataValues(cursor);
+ rawContact.addDataItemValues(data);
+ }
+ } while (cursor.moveToNext());
+
+ contact.setRawContacts(rawContactsBuilder.build());
+
+ return contact;
+ } finally {
+ cursor.close();
+ }
+ }
+
+ /**
+ * Looks for the photo data item in entities. If found, a thumbnail will be stored. A larger photo
+ * will also be stored if available.
+ */
+ private void loadPhotoBinaryData(Contact contactData) {
+ loadThumbnailBinaryData(contactData);
+
+ // Try to load the large photo from a file using the photo URI.
+ String photoUri = contactData.getPhotoUri();
+ if (photoUri != null) {
+ try {
+ final InputStream inputStream;
+ final AssetFileDescriptor fd;
+ final Uri uri = Uri.parse(photoUri);
+ final String scheme = uri.getScheme();
+ if ("http".equals(scheme) || "https".equals(scheme)) {
+ // Support HTTP urls that might come from extended directories
+ inputStream = new URL(photoUri).openStream();
+ fd = null;
+ } else {
+ fd = getContext().getContentResolver().openAssetFileDescriptor(uri, "r");
+ inputStream = fd.createInputStream();
+ }
+ byte[] buffer = new byte[16 * 1024];
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ try {
+ int size;
+ while ((size = inputStream.read(buffer)) != -1) {
+ baos.write(buffer, 0, size);
+ }
+ contactData.setPhotoBinaryData(baos.toByteArray());
+ } finally {
+ inputStream.close();
+ if (fd != null) {
+ fd.close();
+ }
+ }
+ return;
+ } catch (IOException ioe) {
+ // Just fall back to the case below.
+ }
+ }
+
+ // If we couldn't load from a file, fall back to the data blob.
+ contactData.setPhotoBinaryData(contactData.getThumbnailPhotoBinaryData());
+ }
+
+ private void loadThumbnailBinaryData(Contact contactData) {
+ final long photoId = contactData.getPhotoId();
+ if (photoId <= 0) {
+ // No photo ID
+ return;
+ }
+
+ for (RawContact rawContact : contactData.getRawContacts()) {
+ for (DataItem dataItem : rawContact.getDataItems()) {
+ if (dataItem.getId() == photoId) {
+ if (!(dataItem instanceof PhotoDataItem)) {
+ break;
+ }
+
+ final PhotoDataItem photo = (PhotoDataItem) dataItem;
+ contactData.setThumbnailPhotoBinaryData(photo.getPhoto());
+ break;
+ }
+ }
+ }
+ }
+
+ /** Sets the "invitable" account types to {@link Contact#mInvitableAccountTypes}. */
+ private void loadInvitableAccountTypes(Contact contactData) {
+ final ImmutableList.Builder<AccountType> resultListBuilder =
+ new ImmutableList.Builder<AccountType>();
+ if (!contactData.isUserProfile()) {
+ Map<AccountTypeWithDataSet, AccountType> invitables =
+ AccountTypeManager.getInstance(getContext()).getUsableInvitableAccountTypes();
+ if (!invitables.isEmpty()) {
+ final Map<AccountTypeWithDataSet, AccountType> resultMap = Maps.newHashMap(invitables);
+
+ // Remove the ones that already have a raw contact in the current contact
+ for (RawContact rawContact : contactData.getRawContacts()) {
+ final AccountTypeWithDataSet type =
+ AccountTypeWithDataSet.get(
+ rawContact.getAccountTypeString(), rawContact.getDataSet());
+ resultMap.remove(type);
+ }
+
+ resultListBuilder.addAll(resultMap.values());
+ }
+ }
+
+ // Set to mInvitableAccountTypes
+ contactData.setInvitableAccountTypes(resultListBuilder.build());
+ }
+
+ /** Extracts Contact level columns from the cursor. */
+ private Contact loadContactHeaderData(final Cursor cursor, Uri contactUri) {
+ final String directoryParameter =
+ contactUri.getQueryParameter(ContactsContract.DIRECTORY_PARAM_KEY);
+ final long directoryId =
+ directoryParameter == null ? Directory.DEFAULT : Long.parseLong(directoryParameter);
+ final long contactId = cursor.getLong(ContactQuery.CONTACT_ID);
+ final String lookupKey = cursor.getString(ContactQuery.LOOKUP_KEY);
+ final long nameRawContactId = cursor.getLong(ContactQuery.NAME_RAW_CONTACT_ID);
+ final int displayNameSource = cursor.getInt(ContactQuery.DISPLAY_NAME_SOURCE);
+ final String displayName = cursor.getString(ContactQuery.DISPLAY_NAME);
+ final String altDisplayName = cursor.getString(ContactQuery.ALT_DISPLAY_NAME);
+ final String phoneticName = cursor.getString(ContactQuery.PHONETIC_NAME);
+ final long photoId = cursor.getLong(ContactQuery.PHOTO_ID);
+ final String photoUri = cursor.getString(ContactQuery.PHOTO_URI);
+ final boolean starred = cursor.getInt(ContactQuery.STARRED) != 0;
+ final Integer presence =
+ cursor.isNull(ContactQuery.CONTACT_PRESENCE)
+ ? null
+ : cursor.getInt(ContactQuery.CONTACT_PRESENCE);
+ final boolean sendToVoicemail = cursor.getInt(ContactQuery.SEND_TO_VOICEMAIL) == 1;
+ final String customRingtone = cursor.getString(ContactQuery.CUSTOM_RINGTONE);
+ final boolean isUserProfile = cursor.getInt(ContactQuery.IS_USER_PROFILE) == 1;
+
+ Uri lookupUri;
+ if (directoryId == Directory.DEFAULT || directoryId == Directory.LOCAL_INVISIBLE) {
+ lookupUri =
+ ContentUris.withAppendedId(
+ Uri.withAppendedPath(Contacts.CONTENT_LOOKUP_URI, lookupKey), contactId);
+ } else {
+ lookupUri = contactUri;
+ }
+
+ return new Contact(
+ mRequestedUri,
+ contactUri,
+ lookupUri,
+ directoryId,
+ lookupKey,
+ contactId,
+ nameRawContactId,
+ displayNameSource,
+ photoId,
+ photoUri,
+ displayName,
+ altDisplayName,
+ phoneticName,
+ starred,
+ presence,
+ sendToVoicemail,
+ customRingtone,
+ isUserProfile);
+ }
+
+ /** Extracts RawContact level columns from the cursor. */
+ private ContentValues loadRawContactValues(Cursor cursor) {
+ ContentValues cv = new ContentValues();
+
+ cv.put(RawContacts._ID, cursor.getLong(ContactQuery.RAW_CONTACT_ID));
+
+ cursorColumnToContentValues(cursor, cv, ContactQuery.ACCOUNT_NAME);
+ cursorColumnToContentValues(cursor, cv, ContactQuery.ACCOUNT_TYPE);
+ cursorColumnToContentValues(cursor, cv, ContactQuery.DATA_SET);
+ cursorColumnToContentValues(cursor, cv, ContactQuery.DIRTY);
+ cursorColumnToContentValues(cursor, cv, ContactQuery.VERSION);
+ cursorColumnToContentValues(cursor, cv, ContactQuery.SOURCE_ID);
+ cursorColumnToContentValues(cursor, cv, ContactQuery.SYNC1);
+ cursorColumnToContentValues(cursor, cv, ContactQuery.SYNC2);
+ cursorColumnToContentValues(cursor, cv, ContactQuery.SYNC3);
+ cursorColumnToContentValues(cursor, cv, ContactQuery.SYNC4);
+ cursorColumnToContentValues(cursor, cv, ContactQuery.DELETED);
+ cursorColumnToContentValues(cursor, cv, ContactQuery.CONTACT_ID);
+ cursorColumnToContentValues(cursor, cv, ContactQuery.STARRED);
+
+ return cv;
+ }
+
+ /** Extracts Data level columns from the cursor. */
+ private ContentValues loadDataValues(Cursor cursor) {
+ ContentValues cv = new ContentValues();
+
+ cv.put(Data._ID, cursor.getLong(ContactQuery.DATA_ID));
+
+ cursorColumnToContentValues(cursor, cv, ContactQuery.DATA1);
+ cursorColumnToContentValues(cursor, cv, ContactQuery.DATA2);
+ cursorColumnToContentValues(cursor, cv, ContactQuery.DATA3);
+ cursorColumnToContentValues(cursor, cv, ContactQuery.DATA4);
+ cursorColumnToContentValues(cursor, cv, ContactQuery.DATA5);
+ cursorColumnToContentValues(cursor, cv, ContactQuery.DATA6);
+ cursorColumnToContentValues(cursor, cv, ContactQuery.DATA7);
+ cursorColumnToContentValues(cursor, cv, ContactQuery.DATA8);
+ cursorColumnToContentValues(cursor, cv, ContactQuery.DATA9);
+ cursorColumnToContentValues(cursor, cv, ContactQuery.DATA10);
+ cursorColumnToContentValues(cursor, cv, ContactQuery.DATA11);
+ cursorColumnToContentValues(cursor, cv, ContactQuery.DATA12);
+ cursorColumnToContentValues(cursor, cv, ContactQuery.DATA13);
+ cursorColumnToContentValues(cursor, cv, ContactQuery.DATA14);
+ cursorColumnToContentValues(cursor, cv, ContactQuery.DATA15);
+ cursorColumnToContentValues(cursor, cv, ContactQuery.DATA_SYNC1);
+ cursorColumnToContentValues(cursor, cv, ContactQuery.DATA_SYNC2);
+ cursorColumnToContentValues(cursor, cv, ContactQuery.DATA_SYNC3);
+ cursorColumnToContentValues(cursor, cv, ContactQuery.DATA_SYNC4);
+ cursorColumnToContentValues(cursor, cv, ContactQuery.DATA_VERSION);
+ cursorColumnToContentValues(cursor, cv, ContactQuery.IS_PRIMARY);
+ cursorColumnToContentValues(cursor, cv, ContactQuery.IS_SUPERPRIMARY);
+ cursorColumnToContentValues(cursor, cv, ContactQuery.MIMETYPE);
+ cursorColumnToContentValues(cursor, cv, ContactQuery.GROUP_SOURCE_ID);
+ cursorColumnToContentValues(cursor, cv, ContactQuery.CHAT_CAPABILITY);
+ cursorColumnToContentValues(cursor, cv, ContactQuery.TIMES_USED);
+ cursorColumnToContentValues(cursor, cv, ContactQuery.LAST_TIME_USED);
+ if (CompatUtils.isMarshmallowCompatible()) {
+ cursorColumnToContentValues(cursor, cv, ContactQuery.CARRIER_PRESENCE);
+ }
+
+ return cv;
+ }
+
+ private void cursorColumnToContentValues(Cursor cursor, ContentValues values, int index) {
+ switch (cursor.getType(index)) {
+ case Cursor.FIELD_TYPE_NULL:
+ // don't put anything in the content values
+ break;
+ case Cursor.FIELD_TYPE_INTEGER:
+ values.put(ContactQuery.COLUMNS[index], cursor.getLong(index));
+ break;
+ case Cursor.FIELD_TYPE_STRING:
+ values.put(ContactQuery.COLUMNS[index], cursor.getString(index));
+ break;
+ case Cursor.FIELD_TYPE_BLOB:
+ values.put(ContactQuery.COLUMNS[index], cursor.getBlob(index));
+ break;
+ default:
+ throw new IllegalStateException("Invalid or unhandled data type");
+ }
+ }
+
+ private void loadDirectoryMetaData(Contact result) {
+ long directoryId = result.getDirectoryId();
+
+ Cursor cursor =
+ getContext()
+ .getContentResolver()
+ .query(
+ ContentUris.withAppendedId(Directory.CONTENT_URI, directoryId),
+ DirectoryQuery.COLUMNS,
+ null,
+ null,
+ null);
+ if (cursor == null) {
+ return;
+ }
+ try {
+ if (cursor.moveToFirst()) {
+ final String displayName = cursor.getString(DirectoryQuery.DISPLAY_NAME);
+ final String packageName = cursor.getString(DirectoryQuery.PACKAGE_NAME);
+ final int typeResourceId = cursor.getInt(DirectoryQuery.TYPE_RESOURCE_ID);
+ final String accountType = cursor.getString(DirectoryQuery.ACCOUNT_TYPE);
+ final String accountName = cursor.getString(DirectoryQuery.ACCOUNT_NAME);
+ final int exportSupport = cursor.getInt(DirectoryQuery.EXPORT_SUPPORT);
+ String directoryType = null;
+ if (!TextUtils.isEmpty(packageName)) {
+ PackageManager pm = getContext().getPackageManager();
+ try {
+ Resources resources = pm.getResourcesForApplication(packageName);
+ directoryType = resources.getString(typeResourceId);
+ } catch (NameNotFoundException e) {
+ Log.w(
+ TAG, "Contact directory resource not found: " + packageName + "." + typeResourceId);
+ }
+ }
+
+ result.setDirectoryMetaData(
+ displayName, directoryType, accountType, accountName, exportSupport);
+ }
+ } finally {
+ cursor.close();
+ }
+ }
+
+ /**
+ * Loads groups meta-data for all groups associated with all constituent raw contacts' accounts.
+ */
+ private void loadGroupMetaData(Contact result) {
+ StringBuilder selection = new StringBuilder();
+ ArrayList<String> selectionArgs = new ArrayList<String>();
+ final HashSet<AccountKey> accountsSeen = new HashSet<>();
+ for (RawContact rawContact : result.getRawContacts()) {
+ final String accountName = rawContact.getAccountName();
+ final String accountType = rawContact.getAccountTypeString();
+ final String dataSet = rawContact.getDataSet();
+ final AccountKey accountKey = new AccountKey(accountName, accountType, dataSet);
+ if (accountName != null && accountType != null && !accountsSeen.contains(accountKey)) {
+ accountsSeen.add(accountKey);
+ if (selection.length() != 0) {
+ selection.append(" OR ");
+ }
+ selection.append("(" + Groups.ACCOUNT_NAME + "=? AND " + Groups.ACCOUNT_TYPE + "=?");
+ selectionArgs.add(accountName);
+ selectionArgs.add(accountType);
+
+ if (dataSet != null) {
+ selection.append(" AND " + Groups.DATA_SET + "=?");
+ selectionArgs.add(dataSet);
+ } else {
+ selection.append(" AND " + Groups.DATA_SET + " IS NULL");
+ }
+ selection.append(")");
+ }
+ }
+ final ImmutableList.Builder<GroupMetaData> groupListBuilder =
+ new ImmutableList.Builder<GroupMetaData>();
+ final Cursor cursor =
+ getContext()
+ .getContentResolver()
+ .query(
+ Groups.CONTENT_URI,
+ GroupQuery.COLUMNS,
+ selection.toString(),
+ selectionArgs.toArray(new String[0]),
+ null);
+ if (cursor != null) {
+ try {
+ while (cursor.moveToNext()) {
+ final String accountName = cursor.getString(GroupQuery.ACCOUNT_NAME);
+ final String accountType = cursor.getString(GroupQuery.ACCOUNT_TYPE);
+ final String dataSet = cursor.getString(GroupQuery.DATA_SET);
+ final long groupId = cursor.getLong(GroupQuery.ID);
+ final String title = cursor.getString(GroupQuery.TITLE);
+ final boolean defaultGroup =
+ !cursor.isNull(GroupQuery.AUTO_ADD) && cursor.getInt(GroupQuery.AUTO_ADD) != 0;
+ final boolean favorites =
+ !cursor.isNull(GroupQuery.FAVORITES) && cursor.getInt(GroupQuery.FAVORITES) != 0;
+
+ groupListBuilder.add(
+ new GroupMetaData(
+ accountName, accountType, dataSet, groupId, title, defaultGroup, favorites));
+ }
+ } finally {
+ cursor.close();
+ }
+ }
+ result.setGroupMetaData(groupListBuilder.build());
+ }
+
+ /**
+ * Iterates over all data items that represent phone numbers are tries to calculate a formatted
+ * number. This function can safely be called several times as no unformatted data is overwritten
+ */
+ private void computeFormattedPhoneNumbers(Contact contactData) {
+ final String countryIso = GeoUtil.getCurrentCountryIso(getContext());
+ final ImmutableList<RawContact> rawContacts = contactData.getRawContacts();
+ final int rawContactCount = rawContacts.size();
+ for (int rawContactIndex = 0; rawContactIndex < rawContactCount; rawContactIndex++) {
+ final RawContact rawContact = rawContacts.get(rawContactIndex);
+ final List<DataItem> dataItems = rawContact.getDataItems();
+ final int dataCount = dataItems.size();
+ for (int dataIndex = 0; dataIndex < dataCount; dataIndex++) {
+ final DataItem dataItem = dataItems.get(dataIndex);
+ if (dataItem instanceof PhoneDataItem) {
+ final PhoneDataItem phoneDataItem = (PhoneDataItem) dataItem;
+ phoneDataItem.computeFormattedPhoneNumber(countryIso);
+ }
+ }
+ }
+ }
+
+ @Override
+ public void deliverResult(Contact result) {
+ unregisterObserver();
+
+ // The creator isn't interested in any further updates
+ if (isReset() || result == null) {
+ return;
+ }
+
+ mContact = result;
+
+ if (result.isLoaded()) {
+ mLookupUri = result.getLookupUri();
+
+ if (!result.isDirectoryEntry()) {
+ Log.i(TAG, "Registering content observer for " + mLookupUri);
+ if (mObserver == null) {
+ mObserver = new ForceLoadContentObserver();
+ }
+ getContext().getContentResolver().registerContentObserver(mLookupUri, true, mObserver);
+ }
+
+ if (mPostViewNotification) {
+ // inform the source of the data that this contact is being looked at
+ postViewNotificationToSyncAdapter();
+ }
+ }
+
+ super.deliverResult(mContact);
+ }
+
+ /**
+ * Posts a message to the contributing sync adapters that have opted-in, notifying them that the
+ * contact has just been loaded
+ */
+ private void postViewNotificationToSyncAdapter() {
+ Context context = getContext();
+ for (RawContact rawContact : mContact.getRawContacts()) {
+ final long rawContactId = rawContact.getId();
+ if (mNotifiedRawContactIds.contains(rawContactId)) {
+ continue; // Already notified for this raw contact.
+ }
+ mNotifiedRawContactIds.add(rawContactId);
+ final AccountType accountType = rawContact.getAccountType(context);
+ final String serviceName = accountType.getViewContactNotifyServiceClassName();
+ final String servicePackageName = accountType.getViewContactNotifyServicePackageName();
+ if (!TextUtils.isEmpty(serviceName) && !TextUtils.isEmpty(servicePackageName)) {
+ final Uri uri = ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId);
+ final Intent intent = new Intent();
+ intent.setClassName(servicePackageName, serviceName);
+ intent.setAction(Intent.ACTION_VIEW);
+ intent.setDataAndType(uri, RawContacts.CONTENT_ITEM_TYPE);
+ try {
+ context.startService(intent);
+ } catch (Exception e) {
+ Log.e(TAG, "Error sending message to source-app", e);
+ }
+ }
+ }
+ }
+
+ private void unregisterObserver() {
+ if (mObserver != null) {
+ getContext().getContentResolver().unregisterContentObserver(mObserver);
+ mObserver = null;
+ }
+ }
+
+ public Uri getLookupUri() {
+ return mLookupUri;
+ }
+
+ public void setLookupUri(Uri lookupUri) {
+ mLookupUri = lookupUri;
+ }
+
+ @Override
+ protected void onStartLoading() {
+ if (mContact != null) {
+ deliverResult(mContact);
+ }
+
+ if (takeContentChanged() || mContact == null) {
+ forceLoad();
+ }
+ }
+
+ @Override
+ protected void onStopLoading() {
+ cancelLoad();
+ }
+
+ @Override
+ protected void onReset() {
+ super.onReset();
+ cancelLoad();
+ unregisterObserver();
+ mContact = null;
+ }
+
+ /**
+ * Projection used for the query that loads all data for the entire contact (except for social
+ * stream items).
+ */
+ private static class ContactQuery {
+
+ public static final int NAME_RAW_CONTACT_ID = 0;
+ public static final int DISPLAY_NAME_SOURCE = 1;
+ public static final int LOOKUP_KEY = 2;
+ public static final int DISPLAY_NAME = 3;
+ public static final int ALT_DISPLAY_NAME = 4;
+ public static final int PHONETIC_NAME = 5;
+ public static final int PHOTO_ID = 6;
+ public static final int STARRED = 7;
+ public static final int CONTACT_PRESENCE = 8;
+ public static final int CONTACT_STATUS = 9;
+ public static final int CONTACT_STATUS_TIMESTAMP = 10;
+ public static final int CONTACT_STATUS_RES_PACKAGE = 11;
+ public static final int CONTACT_STATUS_LABEL = 12;
+ public static final int CONTACT_ID = 13;
+ public static final int RAW_CONTACT_ID = 14;
+ public static final int ACCOUNT_NAME = 15;
+ public static final int ACCOUNT_TYPE = 16;
+ public static final int DATA_SET = 17;
+ public static final int DIRTY = 18;
+ public static final int VERSION = 19;
+ public static final int SOURCE_ID = 20;
+ public static final int SYNC1 = 21;
+ public static final int SYNC2 = 22;
+ public static final int SYNC3 = 23;
+ public static final int SYNC4 = 24;
+ public static final int DELETED = 25;
+ public static final int DATA_ID = 26;
+ public static final int DATA1 = 27;
+ public static final int DATA2 = 28;
+ public static final int DATA3 = 29;
+ public static final int DATA4 = 30;
+ public static final int DATA5 = 31;
+ public static final int DATA6 = 32;
+ public static final int DATA7 = 33;
+ public static final int DATA8 = 34;
+ public static final int DATA9 = 35;
+ public static final int DATA10 = 36;
+ public static final int DATA11 = 37;
+ public static final int DATA12 = 38;
+ public static final int DATA13 = 39;
+ public static final int DATA14 = 40;
+ public static final int DATA15 = 41;
+ public static final int DATA_SYNC1 = 42;
+ public static final int DATA_SYNC2 = 43;
+ public static final int DATA_SYNC3 = 44;
+ public static final int DATA_SYNC4 = 45;
+ public static final int DATA_VERSION = 46;
+ public static final int IS_PRIMARY = 47;
+ public static final int IS_SUPERPRIMARY = 48;
+ public static final int MIMETYPE = 49;
+ public static final int GROUP_SOURCE_ID = 50;
+ public static final int PRESENCE = 51;
+ public static final int CHAT_CAPABILITY = 52;
+ public static final int STATUS = 53;
+ public static final int STATUS_RES_PACKAGE = 54;
+ public static final int STATUS_ICON = 55;
+ public static final int STATUS_LABEL = 56;
+ public static final int STATUS_TIMESTAMP = 57;
+ public static final int PHOTO_URI = 58;
+ public static final int SEND_TO_VOICEMAIL = 59;
+ public static final int CUSTOM_RINGTONE = 60;
+ public static final int IS_USER_PROFILE = 61;
+ public static final int TIMES_USED = 62;
+ public static final int LAST_TIME_USED = 63;
+ public static final int CARRIER_PRESENCE = 64;
+ static final String[] COLUMNS_INTERNAL =
+ new String[] {
+ Contacts.NAME_RAW_CONTACT_ID,
+ Contacts.DISPLAY_NAME_SOURCE,
+ Contacts.LOOKUP_KEY,
+ Contacts.DISPLAY_NAME,
+ Contacts.DISPLAY_NAME_ALTERNATIVE,
+ Contacts.PHONETIC_NAME,
+ Contacts.PHOTO_ID,
+ Contacts.STARRED,
+ Contacts.CONTACT_PRESENCE,
+ Contacts.CONTACT_STATUS,
+ Contacts.CONTACT_STATUS_TIMESTAMP,
+ Contacts.CONTACT_STATUS_RES_PACKAGE,
+ Contacts.CONTACT_STATUS_LABEL,
+ Contacts.Entity.CONTACT_ID,
+ Contacts.Entity.RAW_CONTACT_ID,
+ RawContacts.ACCOUNT_NAME,
+ RawContacts.ACCOUNT_TYPE,
+ RawContacts.DATA_SET,
+ RawContacts.DIRTY,
+ RawContacts.VERSION,
+ RawContacts.SOURCE_ID,
+ RawContacts.SYNC1,
+ RawContacts.SYNC2,
+ RawContacts.SYNC3,
+ RawContacts.SYNC4,
+ RawContacts.DELETED,
+ Contacts.Entity.DATA_ID,
+ Data.DATA1,
+ Data.DATA2,
+ Data.DATA3,
+ Data.DATA4,
+ Data.DATA5,
+ Data.DATA6,
+ Data.DATA7,
+ Data.DATA8,
+ Data.DATA9,
+ Data.DATA10,
+ Data.DATA11,
+ Data.DATA12,
+ Data.DATA13,
+ Data.DATA14,
+ Data.DATA15,
+ Data.SYNC1,
+ Data.SYNC2,
+ Data.SYNC3,
+ Data.SYNC4,
+ Data.DATA_VERSION,
+ Data.IS_PRIMARY,
+ Data.IS_SUPER_PRIMARY,
+ Data.MIMETYPE,
+ GroupMembership.GROUP_SOURCE_ID,
+ Data.PRESENCE,
+ Data.CHAT_CAPABILITY,
+ Data.STATUS,
+ Data.STATUS_RES_PACKAGE,
+ Data.STATUS_ICON,
+ Data.STATUS_LABEL,
+ Data.STATUS_TIMESTAMP,
+ Contacts.PHOTO_URI,
+ Contacts.SEND_TO_VOICEMAIL,
+ Contacts.CUSTOM_RINGTONE,
+ Contacts.IS_USER_PROFILE,
+ Data.TIMES_USED,
+ Data.LAST_TIME_USED
+ };
+ static final String[] COLUMNS;
+
+ static {
+ List<String> projectionList = Lists.newArrayList(COLUMNS_INTERNAL);
+ if (CompatUtils.isMarshmallowCompatible()) {
+ projectionList.add(Data.CARRIER_PRESENCE);
+ }
+ COLUMNS = projectionList.toArray(new String[projectionList.size()]);
+ }
+ }
+
+ /** Projection used for the query that loads all data for the entire contact. */
+ private static class DirectoryQuery {
+
+ public static final int DISPLAY_NAME = 0;
+ public static final int PACKAGE_NAME = 1;
+ public static final int TYPE_RESOURCE_ID = 2;
+ public static final int ACCOUNT_TYPE = 3;
+ public static final int ACCOUNT_NAME = 4;
+ public static final int EXPORT_SUPPORT = 5;
+ static final String[] COLUMNS =
+ new String[] {
+ Directory.DISPLAY_NAME,
+ Directory.PACKAGE_NAME,
+ Directory.TYPE_RESOURCE_ID,
+ Directory.ACCOUNT_TYPE,
+ Directory.ACCOUNT_NAME,
+ Directory.EXPORT_SUPPORT,
+ };
+ }
+
+ private static class GroupQuery {
+
+ public static final int ACCOUNT_NAME = 0;
+ public static final int ACCOUNT_TYPE = 1;
+ public static final int DATA_SET = 2;
+ public static final int ID = 3;
+ public static final int TITLE = 4;
+ public static final int AUTO_ADD = 5;
+ public static final int FAVORITES = 6;
+ static final String[] COLUMNS =
+ new String[] {
+ Groups.ACCOUNT_NAME,
+ Groups.ACCOUNT_TYPE,
+ Groups.DATA_SET,
+ Groups._ID,
+ Groups.TITLE,
+ Groups.AUTO_ADD,
+ Groups.FAVORITES,
+ };
+ }
+
+ private static class AccountKey {
+
+ private final String mAccountName;
+ private final String mAccountType;
+ private final String mDataSet;
+
+ public AccountKey(String accountName, String accountType, String dataSet) {
+ mAccountName = accountName;
+ mAccountType = accountType;
+ mDataSet = dataSet;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mAccountName, mAccountType, mDataSet);
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (!(obj instanceof AccountKey)) {
+ return false;
+ }
+ final AccountKey other = (AccountKey) obj;
+ return Objects.equals(mAccountName, other.mAccountName)
+ && Objects.equals(mAccountType, other.mAccountType)
+ && Objects.equals(mDataSet, other.mDataSet);
+ }
+ }
+}
diff --git a/java/com/android/contacts/common/model/RawContact.java b/java/com/android/contacts/common/model/RawContact.java
new file mode 100644
index 000000000..9efc8a878
--- /dev/null
+++ b/java/com/android/contacts/common/model/RawContact.java
@@ -0,0 +1,351 @@
+/*
+ * Copyright (C) 2012 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.common.model;
+
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.Entity;
+import android.net.Uri;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.provider.ContactsContract.Contacts;
+import android.provider.ContactsContract.Data;
+import android.provider.ContactsContract.RawContacts;
+import com.android.contacts.common.model.account.AccountType;
+import com.android.contacts.common.model.account.AccountWithDataSet;
+import com.android.contacts.common.model.dataitem.DataItem;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * RawContact represents a single raw contact in the raw contacts database. It has specialized
+ * getters/setters for raw contact items, and also contains a collection of DataItem objects. A
+ * RawContact contains the information from a single account.
+ *
+ * <p>This allows RawContact objects to be thought of as a class with raw contact fields (like
+ * account type, name, data set, sync state, etc.) and a list of DataItem objects that represent
+ * contact information elements (like phone numbers, email, address, etc.).
+ */
+public final class RawContact implements Parcelable {
+
+ /** Create for building the parcelable. */
+ public static final Parcelable.Creator<RawContact> CREATOR =
+ new Parcelable.Creator<RawContact>() {
+
+ @Override
+ public RawContact createFromParcel(Parcel parcel) {
+ return new RawContact(parcel);
+ }
+
+ @Override
+ public RawContact[] newArray(int i) {
+ return new RawContact[i];
+ }
+ };
+
+ private final ContentValues mValues;
+ private final ArrayList<NamedDataItem> mDataItems;
+ private AccountTypeManager mAccountTypeManager;
+
+ /** A RawContact object can be created with or without a context. */
+ public RawContact() {
+ this(new ContentValues());
+ }
+
+ public RawContact(ContentValues values) {
+ mValues = values;
+ mDataItems = new ArrayList<NamedDataItem>();
+ }
+
+ /**
+ * Constructor for the parcelable.
+ *
+ * @param parcel The parcel to de-serialize from.
+ */
+ private RawContact(Parcel parcel) {
+ mValues = parcel.readParcelable(ContentValues.class.getClassLoader());
+ mDataItems = new ArrayList<>();
+ parcel.readTypedList(mDataItems, NamedDataItem.CREATOR);
+ }
+
+ public static RawContact createFrom(Entity entity) {
+ final ContentValues values = entity.getEntityValues();
+ final ArrayList<Entity.NamedContentValues> subValues = entity.getSubValues();
+
+ RawContact rawContact = new RawContact(values);
+ for (Entity.NamedContentValues subValue : subValues) {
+ rawContact.addNamedDataItemValues(subValue.uri, subValue.values);
+ }
+ return rawContact;
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel parcel, int i) {
+ parcel.writeParcelable(mValues, i);
+ parcel.writeTypedList(mDataItems);
+ }
+
+ public AccountTypeManager getAccountTypeManager(Context context) {
+ if (mAccountTypeManager == null) {
+ mAccountTypeManager = AccountTypeManager.getInstance(context);
+ }
+ return mAccountTypeManager;
+ }
+
+ public ContentValues getValues() {
+ return mValues;
+ }
+
+ /** Returns the id of the raw contact. */
+ public Long getId() {
+ return getValues().getAsLong(RawContacts._ID);
+ }
+
+ /** Returns the account name of the raw contact. */
+ public String getAccountName() {
+ return getValues().getAsString(RawContacts.ACCOUNT_NAME);
+ }
+
+ /** Returns the account type of the raw contact. */
+ public String getAccountTypeString() {
+ return getValues().getAsString(RawContacts.ACCOUNT_TYPE);
+ }
+
+ /** Returns the data set of the raw contact. */
+ public String getDataSet() {
+ return getValues().getAsString(RawContacts.DATA_SET);
+ }
+
+ public boolean isDirty() {
+ return getValues().getAsBoolean(RawContacts.DIRTY);
+ }
+
+ public String getSourceId() {
+ return getValues().getAsString(RawContacts.SOURCE_ID);
+ }
+
+ public String getSync1() {
+ return getValues().getAsString(RawContacts.SYNC1);
+ }
+
+ public String getSync2() {
+ return getValues().getAsString(RawContacts.SYNC2);
+ }
+
+ public String getSync3() {
+ return getValues().getAsString(RawContacts.SYNC3);
+ }
+
+ public String getSync4() {
+ return getValues().getAsString(RawContacts.SYNC4);
+ }
+
+ public boolean isDeleted() {
+ return getValues().getAsBoolean(RawContacts.DELETED);
+ }
+
+ public long getContactId() {
+ return getValues().getAsLong(Contacts.Entity.CONTACT_ID);
+ }
+
+ public boolean isStarred() {
+ return getValues().getAsBoolean(Contacts.STARRED);
+ }
+
+ public AccountType getAccountType(Context context) {
+ return getAccountTypeManager(context).getAccountType(getAccountTypeString(), getDataSet());
+ }
+
+ /**
+ * Sets the account name, account type, and data set strings. Valid combinations for account-name,
+ * account-type, data-set 1) null, null, null (local account) 2) non-null, non-null, null (valid
+ * account without data-set) 3) non-null, non-null, non-null (valid account with data-set)
+ */
+ private void setAccount(String accountName, String accountType, String dataSet) {
+ final ContentValues values = getValues();
+ if (accountName == null) {
+ if (accountType == null && dataSet == null) {
+ // This is a local account
+ values.putNull(RawContacts.ACCOUNT_NAME);
+ values.putNull(RawContacts.ACCOUNT_TYPE);
+ values.putNull(RawContacts.DATA_SET);
+ return;
+ }
+ } else {
+ if (accountType != null) {
+ // This is a valid account, either with or without a dataSet.
+ values.put(RawContacts.ACCOUNT_NAME, accountName);
+ values.put(RawContacts.ACCOUNT_TYPE, accountType);
+ if (dataSet == null) {
+ values.putNull(RawContacts.DATA_SET);
+ } else {
+ values.put(RawContacts.DATA_SET, dataSet);
+ }
+ return;
+ }
+ }
+ throw new IllegalArgumentException(
+ "Not a valid combination of account name, type, and data set.");
+ }
+
+ public void setAccount(AccountWithDataSet accountWithDataSet) {
+ if (accountWithDataSet != null) {
+ setAccount(accountWithDataSet.name, accountWithDataSet.type, accountWithDataSet.dataSet);
+ } else {
+ setAccount(null, null, null);
+ }
+ }
+
+ public void setAccountToLocal() {
+ setAccount(null, null, null);
+ }
+
+ /** Creates and inserts a DataItem object that wraps the content values, and returns it. */
+ public void addDataItemValues(ContentValues values) {
+ addNamedDataItemValues(Data.CONTENT_URI, values);
+ }
+
+ public NamedDataItem addNamedDataItemValues(Uri uri, ContentValues values) {
+ final NamedDataItem namedItem = new NamedDataItem(uri, values);
+ mDataItems.add(namedItem);
+ return namedItem;
+ }
+
+ public ArrayList<ContentValues> getContentValues() {
+ final ArrayList<ContentValues> list = new ArrayList<>(mDataItems.size());
+ for (NamedDataItem dataItem : mDataItems) {
+ if (Data.CONTENT_URI.equals(dataItem.mUri)) {
+ list.add(dataItem.mContentValues);
+ }
+ }
+ return list;
+ }
+
+ public List<DataItem> getDataItems() {
+ final ArrayList<DataItem> list = new ArrayList<>(mDataItems.size());
+ for (NamedDataItem dataItem : mDataItems) {
+ if (Data.CONTENT_URI.equals(dataItem.mUri)) {
+ list.add(DataItem.createFrom(dataItem.mContentValues));
+ }
+ }
+ return list;
+ }
+
+ public String toString() {
+ final StringBuilder sb = new StringBuilder();
+ sb.append("RawContact: ").append(mValues);
+ for (RawContact.NamedDataItem namedDataItem : mDataItems) {
+ sb.append("\n ").append(namedDataItem.mUri);
+ sb.append("\n -> ").append(namedDataItem.mContentValues);
+ }
+ return sb.toString();
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mValues, mDataItems);
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (obj == null) {
+ return false;
+ }
+ if (getClass() != obj.getClass()) {
+ return false;
+ }
+
+ RawContact other = (RawContact) obj;
+ return Objects.equals(mValues, other.mValues) && Objects.equals(mDataItems, other.mDataItems);
+ }
+
+ public static final class NamedDataItem implements Parcelable {
+
+ public static final Parcelable.Creator<NamedDataItem> CREATOR =
+ new Parcelable.Creator<NamedDataItem>() {
+
+ @Override
+ public NamedDataItem createFromParcel(Parcel parcel) {
+ return new NamedDataItem(parcel);
+ }
+
+ @Override
+ public NamedDataItem[] newArray(int i) {
+ return new NamedDataItem[i];
+ }
+ };
+ public final Uri mUri;
+ // This use to be a DataItem. DataItem creation is now delayed until the point of request
+ // since there is no benefit to storing them here due to the multiple inheritance.
+ // Eventually instanceof still has to be used anyways to determine which sub-class of
+ // DataItem it is. And having parent DataItem's here makes it very difficult to serialize or
+ // parcelable.
+ //
+ // Instead of having a common DataItem super class, we should refactor this to be a generic
+ // Object where the object is a concrete class that no longer relies on ContentValues.
+ // (this will also make the classes easier to use).
+ // Since instanceof is used later anyways, having a list of Objects won't hurt and is no
+ // worse than having a DataItem.
+ public final ContentValues mContentValues;
+
+ public NamedDataItem(Uri uri, ContentValues values) {
+ this.mUri = uri;
+ this.mContentValues = values;
+ }
+
+ public NamedDataItem(Parcel parcel) {
+ this.mUri = parcel.readParcelable(Uri.class.getClassLoader());
+ this.mContentValues = parcel.readParcelable(ContentValues.class.getClassLoader());
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel parcel, int i) {
+ parcel.writeParcelable(mUri, i);
+ parcel.writeParcelable(mContentValues, i);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mUri, mContentValues);
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (obj == null) {
+ return false;
+ }
+ if (getClass() != obj.getClass()) {
+ return false;
+ }
+
+ final NamedDataItem other = (NamedDataItem) obj;
+ return Objects.equals(mUri, other.mUri)
+ && Objects.equals(mContentValues, other.mContentValues);
+ }
+ }
+}
diff --git a/java/com/android/contacts/common/model/account/AccountType.java b/java/com/android/contacts/common/model/account/AccountType.java
new file mode 100644
index 000000000..1ae485a5f
--- /dev/null
+++ b/java/com/android/contacts/common/model/account/AccountType.java
@@ -0,0 +1,501 @@
+/*
+ * Copyright (C) 2009 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.common.model.account;
+
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.graphics.drawable.Drawable;
+import android.provider.ContactsContract.CommonDataKinds.Phone;
+import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
+import android.provider.ContactsContract.Contacts;
+import android.provider.ContactsContract.RawContacts;
+import android.support.annotation.VisibleForTesting;
+import android.util.ArrayMap;
+import android.view.inputmethod.EditorInfo;
+import android.widget.EditText;
+import com.android.contacts.common.R;
+import com.android.contacts.common.model.dataitem.DataKind;
+import java.text.Collator;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Internal structure that represents constraints and styles for a specific data source, such as the
+ * various data types they support, including details on how those types should be rendered and
+ * edited.
+ *
+ * <p>In the future this may be inflated from XML defined by a data source.
+ */
+public abstract class AccountType {
+
+ private static final String TAG = "AccountType";
+ /** {@link Comparator} to sort by {@link DataKind#weight}. */
+ private static Comparator<DataKind> sWeightComparator =
+ new Comparator<DataKind>() {
+ @Override
+ public int compare(DataKind object1, DataKind object2) {
+ return object1.weight - object2.weight;
+ }
+ };
+ /** The {@link RawContacts#ACCOUNT_TYPE} these constraints apply to. */
+ public String accountType = null;
+ /** The {@link RawContacts#DATA_SET} these constraints apply to. */
+ public String dataSet = null;
+ /**
+ * Package that resources should be loaded from. Will be null for embedded types, in which case
+ * resources are stored in this package itself.
+ *
+ * <p>TODO Clean up {@link #resourcePackageName}, {@link #syncAdapterPackageName} and {@link
+ * #getViewContactNotifyServicePackageName()}.
+ *
+ * <p>There's the following invariants: - {@link #syncAdapterPackageName} is always set to the
+ * actual sync adapter package name. - {@link #resourcePackageName} too is set to the same value,
+ * unless {@link #isEmbedded()}, in which case it'll be null. There's an unfortunate exception of
+ * {@link FallbackAccountType}. Even though it {@link #isEmbedded()}, but we set non-null to
+ * {@link #resourcePackageName} for unit tests.
+ */
+ public String resourcePackageName;
+ /**
+ * The package name for the authenticator (for the embedded types, i.e. Google and Exchange) or
+ * the sync adapter (for external type, including extensions).
+ */
+ public String syncAdapterPackageName;
+
+ public int titleRes;
+ public int iconRes;
+ protected boolean mIsInitialized;
+ /** Set of {@link DataKind} supported by this source. */
+ private ArrayList<DataKind> mKinds = new ArrayList<>();
+ /** Lookup map of {@link #mKinds} on {@link DataKind#mimeType}. */
+ private Map<String, DataKind> mMimeKinds = new ArrayMap<>();
+
+ /**
+ * Return a string resource loaded from the given package (or the current package if {@code
+ * packageName} is null), unless {@code resId} is -1, in which case it returns {@code
+ * defaultValue}.
+ *
+ * <p>(The behavior is undefined if the resource or package doesn't exist.)
+ */
+ @VisibleForTesting
+ static CharSequence getResourceText(
+ Context context, String packageName, int resId, String defaultValue) {
+ if (resId != -1 && packageName != null) {
+ final PackageManager pm = context.getPackageManager();
+ return pm.getText(packageName, resId, null);
+ } else if (resId != -1) {
+ return context.getText(resId);
+ } else {
+ return defaultValue;
+ }
+ }
+
+ public static Drawable getDisplayIcon(
+ Context context, int titleRes, int iconRes, String syncAdapterPackageName) {
+ if (titleRes != -1 && syncAdapterPackageName != null) {
+ final PackageManager pm = context.getPackageManager();
+ return pm.getDrawable(syncAdapterPackageName, iconRes, null);
+ } else if (titleRes != -1) {
+ return context.getResources().getDrawable(iconRes);
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * Whether this account type was able to be fully initialized. This may be false if (for example)
+ * the package name associated with the account type could not be found.
+ */
+ public final boolean isInitialized() {
+ return mIsInitialized;
+ }
+
+ /**
+ * @return Whether this type is an "embedded" type. i.e. any of {@link FallbackAccountType},
+ * {@link GoogleAccountType} or {@link ExternalAccountType}.
+ * <p>If an embedded type cannot be initialized (i.e. if {@link #isInitialized()} returns
+ * {@code false}) it's considered critical, and the application will crash. On the other hand
+ * if it's not an embedded type, we just skip loading the type.
+ */
+ public boolean isEmbedded() {
+ return true;
+ }
+
+ public boolean isExtension() {
+ return false;
+ }
+
+ /**
+ * @return True if contacts can be created and edited using this app. If false, there could still
+ * be an external editor as provided by {@link #getEditContactActivityClassName()} or {@link
+ * #getCreateContactActivityClassName()}
+ */
+ public abstract boolean areContactsWritable();
+
+ /**
+ * Returns an optional custom edit activity.
+ *
+ * <p>Only makes sense for non-embedded account types. The activity class should reside in the
+ * sync adapter package as determined by {@link #syncAdapterPackageName}.
+ */
+ public String getEditContactActivityClassName() {
+ return null;
+ }
+
+ /**
+ * Returns an optional custom new contact activity.
+ *
+ * <p>Only makes sense for non-embedded account types. The activity class should reside in the
+ * sync adapter package as determined by {@link #syncAdapterPackageName}.
+ */
+ public String getCreateContactActivityClassName() {
+ return null;
+ }
+
+ /**
+ * Returns an optional custom invite contact activity.
+ *
+ * <p>Only makes sense for non-embedded account types. The activity class should reside in the
+ * sync adapter package as determined by {@link #syncAdapterPackageName}.
+ */
+ public String getInviteContactActivityClassName() {
+ return null;
+ }
+
+ /**
+ * Returns an optional service that can be launched whenever a contact is being looked at. This
+ * allows the sync adapter to provide more up-to-date information.
+ *
+ * <p>The service class should reside in the sync adapter package as determined by {@link
+ * #getViewContactNotifyServicePackageName()}.
+ */
+ public String getViewContactNotifyServiceClassName() {
+ return null;
+ }
+
+ /**
+ * TODO This is way too hacky should be removed.
+ *
+ * <p>This is introduced for {@link GoogleAccountType} where {@link #syncAdapterPackageName} is
+ * the authenticator package name but the notification service is in the sync adapter package. See
+ * {@link #resourcePackageName} -- we should clean up those.
+ */
+ public String getViewContactNotifyServicePackageName() {
+ return syncAdapterPackageName;
+ }
+
+ /** Returns an optional Activity string that can be used to view the group. */
+ public String getViewGroupActivity() {
+ return null;
+ }
+
+ public CharSequence getDisplayLabel(Context context) {
+ // Note this resource is defined in the sync adapter package, not resourcePackageName.
+ return getResourceText(context, syncAdapterPackageName, titleRes, accountType);
+ }
+
+ /** @return resource ID for the "invite contact" action label, or -1 if not defined. */
+ protected int getInviteContactActionResId() {
+ return -1;
+ }
+
+ /** @return resource ID for the "view group" label, or -1 if not defined. */
+ protected int getViewGroupLabelResId() {
+ return -1;
+ }
+
+ /** Returns {@link AccountTypeWithDataSet} for this type. */
+ public AccountTypeWithDataSet getAccountTypeAndDataSet() {
+ return AccountTypeWithDataSet.get(accountType, dataSet);
+ }
+
+ /**
+ * Returns a list of additional package names that should be inspected as additional external
+ * account types. This allows for a primary account type to indicate other packages that may not
+ * be sync adapters but which still provide contact data, perhaps under a separate data set within
+ * the account.
+ */
+ public List<String> getExtensionPackageNames() {
+ return new ArrayList<String>();
+ }
+
+ /**
+ * Returns an optional custom label for the "invite contact" action, which will be shown on the
+ * contact card. (If not defined, returns null.)
+ */
+ public CharSequence getInviteContactActionLabel(Context context) {
+ // Note this resource is defined in the sync adapter package, not resourcePackageName.
+ return getResourceText(context, syncAdapterPackageName, getInviteContactActionResId(), "");
+ }
+
+ /**
+ * Returns a label for the "view group" action. If not defined, this falls back to our own "View
+ * Updates" string
+ */
+ public CharSequence getViewGroupLabel(Context context) {
+ // Note this resource is defined in the sync adapter package, not resourcePackageName.
+ final CharSequence customTitle =
+ getResourceText(context, syncAdapterPackageName, getViewGroupLabelResId(), null);
+
+ return customTitle == null ? context.getText(R.string.view_updates_from_group) : customTitle;
+ }
+
+ public Drawable getDisplayIcon(Context context) {
+ return getDisplayIcon(context, titleRes, iconRes, syncAdapterPackageName);
+ }
+
+ /** Whether or not groups created under this account type have editable membership lists. */
+ public abstract boolean isGroupMembershipEditable();
+
+ /** Return list of {@link DataKind} supported, sorted by {@link DataKind#weight}. */
+ public ArrayList<DataKind> getSortedDataKinds() {
+ // TODO: optimize by marking if already sorted
+ Collections.sort(mKinds, sWeightComparator);
+ return mKinds;
+ }
+
+ /** Find the {@link DataKind} for a specific MIME-type, if it's handled by this data source. */
+ public DataKind getKindForMimetype(String mimeType) {
+ return this.mMimeKinds.get(mimeType);
+ }
+
+ /** Add given {@link DataKind} to list of those provided by this source. */
+ public DataKind addKind(DataKind kind) throws DefinitionException {
+ if (kind.mimeType == null) {
+ throw new DefinitionException("null is not a valid mime type");
+ }
+ if (mMimeKinds.get(kind.mimeType) != null) {
+ throw new DefinitionException("mime type '" + kind.mimeType + "' is already registered");
+ }
+
+ kind.resourcePackageName = this.resourcePackageName;
+ this.mKinds.add(kind);
+ this.mMimeKinds.put(kind.mimeType, kind);
+ return kind;
+ }
+
+ /**
+ * Generic method of inflating a given {@link ContentValues} into a user-readable {@link
+ * CharSequence}. For example, an inflater could combine the multiple columns of {@link
+ * StructuredPostal} together using a string resource before presenting to the user.
+ */
+ public interface StringInflater {
+
+ CharSequence inflateUsing(Context context, ContentValues values);
+ }
+
+ protected static class DefinitionException extends Exception {
+
+ public DefinitionException(String message) {
+ super(message);
+ }
+
+ public DefinitionException(String message, Exception inner) {
+ super(message, inner);
+ }
+ }
+
+ /**
+ * Description of a specific "type" or "label" of a {@link DataKind} row, such as {@link
+ * Phone#TYPE_WORK}. Includes constraints on total number of rows a {@link Contacts} may have of
+ * this type, and details on how user-defined labels are stored.
+ */
+ public static class EditType {
+
+ public int rawValue;
+ public int labelRes;
+ public boolean secondary;
+ /**
+ * The number of entries allowed for the type. -1 if not specified.
+ *
+ * @see DataKind#typeOverallMax
+ */
+ public int specificMax;
+
+ public String customColumn;
+
+ public EditType(int rawValue, int labelRes) {
+ this.rawValue = rawValue;
+ this.labelRes = labelRes;
+ this.specificMax = -1;
+ }
+
+ public EditType setSecondary(boolean secondary) {
+ this.secondary = secondary;
+ return this;
+ }
+
+ public EditType setSpecificMax(int specificMax) {
+ this.specificMax = specificMax;
+ return this;
+ }
+
+ public EditType setCustomColumn(String customColumn) {
+ this.customColumn = customColumn;
+ return this;
+ }
+
+ @Override
+ public boolean equals(Object object) {
+ if (object instanceof EditType) {
+ final EditType other = (EditType) object;
+ return other.rawValue == rawValue;
+ }
+ return false;
+ }
+
+ @Override
+ public int hashCode() {
+ return rawValue;
+ }
+
+ @Override
+ public String toString() {
+ return this.getClass().getSimpleName()
+ + " rawValue="
+ + rawValue
+ + " labelRes="
+ + labelRes
+ + " secondary="
+ + secondary
+ + " specificMax="
+ + specificMax
+ + " customColumn="
+ + customColumn;
+ }
+ }
+
+ public static class EventEditType extends EditType {
+
+ private boolean mYearOptional;
+
+ public EventEditType(int rawValue, int labelRes) {
+ super(rawValue, labelRes);
+ }
+
+ public boolean isYearOptional() {
+ return mYearOptional;
+ }
+
+ public EventEditType setYearOptional(boolean yearOptional) {
+ mYearOptional = yearOptional;
+ return this;
+ }
+
+ @Override
+ public String toString() {
+ return super.toString() + " mYearOptional=" + mYearOptional;
+ }
+ }
+
+ /**
+ * Description of a user-editable field on a {@link DataKind} row, such as {@link Phone#NUMBER}.
+ * Includes flags to apply to an {@link EditText}, and the column where this field is stored.
+ */
+ public static final class EditField {
+
+ public String column;
+ public int titleRes;
+ public int inputType;
+ public int minLines;
+ public boolean optional;
+ public boolean shortForm;
+ public boolean longForm;
+
+ public EditField(String column, int titleRes) {
+ this.column = column;
+ this.titleRes = titleRes;
+ }
+
+ public EditField(String column, int titleRes, int inputType) {
+ this(column, titleRes);
+ this.inputType = inputType;
+ }
+
+ public EditField setOptional(boolean optional) {
+ this.optional = optional;
+ return this;
+ }
+
+ public EditField setShortForm(boolean shortForm) {
+ this.shortForm = shortForm;
+ return this;
+ }
+
+ public EditField setLongForm(boolean longForm) {
+ this.longForm = longForm;
+ return this;
+ }
+
+ public EditField setMinLines(int minLines) {
+ this.minLines = minLines;
+ return this;
+ }
+
+ public boolean isMultiLine() {
+ return (inputType & EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE) != 0;
+ }
+
+ @Override
+ public String toString() {
+ return this.getClass().getSimpleName()
+ + ":"
+ + " column="
+ + column
+ + " titleRes="
+ + titleRes
+ + " inputType="
+ + inputType
+ + " minLines="
+ + minLines
+ + " optional="
+ + optional
+ + " shortForm="
+ + shortForm
+ + " longForm="
+ + longForm;
+ }
+ }
+
+ /**
+ * Compare two {@link AccountType} by their {@link AccountType#getDisplayLabel} with the current
+ * locale.
+ */
+ public static class DisplayLabelComparator implements Comparator<AccountType> {
+
+ private final Context mContext;
+ /** {@link Comparator} for the current locale. */
+ private final Collator mCollator = Collator.getInstance();
+
+ public DisplayLabelComparator(Context context) {
+ mContext = context;
+ }
+
+ private String getDisplayLabel(AccountType type) {
+ CharSequence label = type.getDisplayLabel(mContext);
+ return (label == null) ? "" : label.toString();
+ }
+
+ @Override
+ public int compare(AccountType lhs, AccountType rhs) {
+ return mCollator.compare(getDisplayLabel(lhs), getDisplayLabel(rhs));
+ }
+ }
+}
diff --git a/java/com/android/contacts/common/model/account/AccountTypeWithDataSet.java b/java/com/android/contacts/common/model/account/AccountTypeWithDataSet.java
new file mode 100644
index 000000000..a32ebe139
--- /dev/null
+++ b/java/com/android/contacts/common/model/account/AccountTypeWithDataSet.java
@@ -0,0 +1,103 @@
+/*
+ * 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.common.model.account;
+
+import android.content.Context;
+import android.database.Cursor;
+import android.net.Uri;
+import android.provider.BaseColumns;
+import android.provider.ContactsContract;
+import android.provider.ContactsContract.RawContacts;
+import android.text.TextUtils;
+import java.util.Objects;
+
+/** Encapsulates an "account type" string and a "data set" string. */
+public class AccountTypeWithDataSet {
+
+ private static final String[] ID_PROJECTION = new String[] {BaseColumns._ID};
+ private static final Uri RAW_CONTACTS_URI_LIMIT_1 =
+ RawContacts.CONTENT_URI
+ .buildUpon()
+ .appendQueryParameter(ContactsContract.LIMIT_PARAM_KEY, "1")
+ .build();
+
+ /** account type. Can be null for fallback type. */
+ public final String accountType;
+
+ /** dataSet may be null, but never be "". */
+ public final String dataSet;
+
+ private AccountTypeWithDataSet(String accountType, String dataSet) {
+ this.accountType = TextUtils.isEmpty(accountType) ? null : accountType;
+ this.dataSet = TextUtils.isEmpty(dataSet) ? null : dataSet;
+ }
+
+ public static AccountTypeWithDataSet get(String accountType, String dataSet) {
+ return new AccountTypeWithDataSet(accountType, dataSet);
+ }
+
+ /**
+ * Return true if there are any contacts in the database with this account type and data set.
+ * Touches DB. Don't use in the UI thread.
+ */
+ public boolean hasData(Context context) {
+ final String BASE_SELECTION = RawContacts.ACCOUNT_TYPE + " = ?";
+ final String selection;
+ final String[] args;
+ if (TextUtils.isEmpty(dataSet)) {
+ selection = BASE_SELECTION + " AND " + RawContacts.DATA_SET + " IS NULL";
+ args = new String[] {accountType};
+ } else {
+ selection = BASE_SELECTION + " AND " + RawContacts.DATA_SET + " = ?";
+ args = new String[] {accountType, dataSet};
+ }
+
+ final Cursor c =
+ context
+ .getContentResolver()
+ .query(RAW_CONTACTS_URI_LIMIT_1, ID_PROJECTION, selection, args, null);
+ if (c == null) {
+ return false;
+ }
+ try {
+ return c.moveToFirst();
+ } finally {
+ c.close();
+ }
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (!(o instanceof AccountTypeWithDataSet)) {
+ return false;
+ }
+
+ AccountTypeWithDataSet other = (AccountTypeWithDataSet) o;
+ return Objects.equals(accountType, other.accountType) && Objects.equals(dataSet, other.dataSet);
+ }
+
+ @Override
+ public int hashCode() {
+ return (accountType == null ? 0 : accountType.hashCode())
+ ^ (dataSet == null ? 0 : dataSet.hashCode());
+ }
+
+ @Override
+ public String toString() {
+ return "[" + accountType + "/" + dataSet + "]";
+ }
+}
diff --git a/java/com/android/contacts/common/model/account/AccountWithDataSet.java b/java/com/android/contacts/common/model/account/AccountWithDataSet.java
new file mode 100644
index 000000000..71faf509c
--- /dev/null
+++ b/java/com/android/contacts/common/model/account/AccountWithDataSet.java
@@ -0,0 +1,229 @@
+/*
+ * 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.common.model.account;
+
+import android.accounts.Account;
+import android.content.Context;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Parcel;
+import android.os.Parcelable;
+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.Objects;
+import java.util.regex.Pattern;
+
+/** Wrapper for an account that includes a data set (which may be null). */
+public class AccountWithDataSet implements Parcelable {
+
+ // 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];
+ }
+ };
+ 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));
+ private static final String[] ID_PROJECTION = new String[] {BaseColumns._ID};
+ private static final Uri RAW_CONTACTS_URI_LIMIT_1 =
+ RawContacts.CONTENT_URI
+ .buildUpon()
+ .appendQueryParameter(ContactsContract.LIMIT_PARAM_KEY, "1")
+ .build();
+ public final String name;
+ public final String type;
+ public final String dataSet;
+ private final AccountTypeWithDataSet mAccountTypeWithDataSet;
+
+ public AccountWithDataSet(String name, String type, String dataSet) {
+ this.name = emptyToNull(name);
+ this.type = emptyToNull(type);
+ this.dataSet = emptyToNull(dataSet);
+ mAccountTypeWithDataSet = AccountTypeWithDataSet.get(type, dataSet);
+ }
+
+ public AccountWithDataSet(Parcel in) {
+ this.name = in.readString();
+ this.type = in.readString();
+ this.dataSet = in.readString();
+ mAccountTypeWithDataSet = AccountTypeWithDataSet.get(type, dataSet);
+ }
+
+ private static String emptyToNull(String text) {
+ return TextUtils.isEmpty(text) ? null : text;
+ }
+
+ private static StringBuilder addStringified(StringBuilder sb, AccountWithDataSet account) {
+ if (!TextUtils.isEmpty(account.name)) {
+ sb.append(account.name);
+ }
+ sb.append(STRINGIFY_SEPARATOR);
+ if (!TextUtils.isEmpty(account.type)) {
+ sb.append(account.type);
+ }
+ sb.append(STRINGIFY_SEPARATOR);
+ if (!TextUtils.isEmpty(account.dataSet)) {
+ sb.append(account.dataSet);
+ }
+
+ return sb;
+ }
+
+ /**
+ * Unpack a string created by {@link #stringify}.
+ *
+ * @throws IllegalArgumentException if it's an invalid string.
+ */
+ public static AccountWithDataSet unstringify(String s) {
+ final String[] array = STRINGIFY_SEPARATOR_PAT.split(s, 3);
+ if (array.length < 3) {
+ throw new IllegalArgumentException("Invalid string " + s);
+ }
+ 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.
+ *
+ * @throws IllegalArgumentException if it's an invalid string.
+ */
+ public static List<AccountWithDataSet> unstringifyList(String s) {
+ final ArrayList<AccountWithDataSet> ret = new ArrayList<>();
+ 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;
+ }
+
+ public boolean isLocalAccount() {
+ return name == null && type == null;
+ }
+
+ public Account getAccountOrNull() {
+ if (name != null && type != null) {
+ return new Account(name, type);
+ }
+ return null;
+ }
+
+ public int describeContents() {
+ return 0;
+ }
+
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeString(name);
+ dest.writeString(type);
+ dest.writeString(dataSet);
+ }
+
+ public AccountTypeWithDataSet getAccountTypeWithDataSet() {
+ return mAccountTypeWithDataSet;
+ }
+
+ /**
+ * Return {@code true} if this account has any contacts in the database. Touches DB. Don't use in
+ * the UI thread.
+ */
+ public boolean hasData(Context context) {
+ final String BASE_SELECTION =
+ RawContacts.ACCOUNT_TYPE + " = ?" + " AND " + RawContacts.ACCOUNT_NAME + " = ?";
+ final String selection;
+ final String[] args;
+ if (TextUtils.isEmpty(dataSet)) {
+ selection = BASE_SELECTION + " AND " + RawContacts.DATA_SET + " IS NULL";
+ args = new String[] {type, name};
+ } else {
+ selection = BASE_SELECTION + " AND " + RawContacts.DATA_SET + " = ?";
+ args = new String[] {type, name, dataSet};
+ }
+
+ final Cursor c =
+ context
+ .getContentResolver()
+ .query(RAW_CONTACTS_URI_LIMIT_1, ID_PROJECTION, selection, args, null);
+ if (c == null) {
+ return false;
+ }
+ try {
+ return c.moveToFirst();
+ } finally {
+ c.close();
+ }
+ }
+
+ public boolean equals(Object obj) {
+ if (obj instanceof AccountWithDataSet) {
+ AccountWithDataSet other = (AccountWithDataSet) obj;
+ return Objects.equals(name, other.name)
+ && Objects.equals(type, other.type)
+ && Objects.equals(dataSet, other.dataSet);
+ }
+ return false;
+ }
+
+ public int hashCode() {
+ int result = 17;
+ result = 31 * result + (name != null ? name.hashCode() : 0);
+ result = 31 * result + (type != null ? type.hashCode() : 0);
+ result = 31 * result + (dataSet != null ? dataSet.hashCode() : 0);
+ return result;
+ }
+
+ public String toString() {
+ return "AccountWithDataSet {name=" + name + ", type=" + type + ", dataSet=" + dataSet + "}";
+ }
+
+ /** Pack the instance into a string. */
+ public String stringify() {
+ return addStringified(new StringBuilder(), this).toString();
+ }
+}
diff --git a/java/com/android/contacts/common/model/account/BaseAccountType.java b/java/com/android/contacts/common/model/account/BaseAccountType.java
new file mode 100644
index 000000000..21b555917
--- /dev/null
+++ b/java/com/android/contacts/common/model/account/BaseAccountType.java
@@ -0,0 +1,1890 @@
+/*
+ * Copyright (C) 2009 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.common.model.account;
+
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.res.Resources;
+import android.provider.ContactsContract.CommonDataKinds.BaseTypes;
+import android.provider.ContactsContract.CommonDataKinds.Email;
+import android.provider.ContactsContract.CommonDataKinds.Event;
+import android.provider.ContactsContract.CommonDataKinds.GroupMembership;
+import android.provider.ContactsContract.CommonDataKinds.Im;
+import android.provider.ContactsContract.CommonDataKinds.Nickname;
+import android.provider.ContactsContract.CommonDataKinds.Note;
+import android.provider.ContactsContract.CommonDataKinds.Organization;
+import android.provider.ContactsContract.CommonDataKinds.Phone;
+import android.provider.ContactsContract.CommonDataKinds.Photo;
+import android.provider.ContactsContract.CommonDataKinds.Relation;
+import android.provider.ContactsContract.CommonDataKinds.SipAddress;
+import android.provider.ContactsContract.CommonDataKinds.StructuredName;
+import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
+import android.provider.ContactsContract.CommonDataKinds.Website;
+import android.util.ArrayMap;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.inputmethod.EditorInfo;
+import com.android.contacts.common.R;
+import com.android.contacts.common.model.dataitem.DataKind;
+import com.android.contacts.common.util.CommonDateUtils;
+import com.android.contacts.common.util.ContactDisplayUtils;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
+public abstract class BaseAccountType extends AccountType {
+
+ public static final StringInflater ORGANIZATION_BODY_INFLATER =
+ new StringInflater() {
+ @Override
+ public CharSequence inflateUsing(Context context, ContentValues values) {
+ final CharSequence companyValue =
+ values.containsKey(Organization.COMPANY)
+ ? values.getAsString(Organization.COMPANY)
+ : null;
+ final CharSequence titleValue =
+ values.containsKey(Organization.TITLE)
+ ? values.getAsString(Organization.TITLE)
+ : null;
+
+ if (companyValue != null && titleValue != null) {
+ return companyValue + ": " + titleValue;
+ } else if (companyValue == null) {
+ return titleValue;
+ } else {
+ return companyValue;
+ }
+ }
+ };
+ protected static final int FLAGS_PHONE = EditorInfo.TYPE_CLASS_PHONE;
+ protected static final int FLAGS_EMAIL =
+ EditorInfo.TYPE_CLASS_TEXT | EditorInfo.TYPE_TEXT_VARIATION_EMAIL_ADDRESS;
+ protected static final int FLAGS_PERSON_NAME =
+ EditorInfo.TYPE_CLASS_TEXT
+ | EditorInfo.TYPE_TEXT_FLAG_CAP_WORDS
+ | EditorInfo.TYPE_TEXT_VARIATION_PERSON_NAME;
+ protected static final int FLAGS_PHONETIC =
+ EditorInfo.TYPE_CLASS_TEXT | EditorInfo.TYPE_TEXT_VARIATION_PHONETIC;
+ protected static final int FLAGS_GENERIC_NAME =
+ EditorInfo.TYPE_CLASS_TEXT | EditorInfo.TYPE_TEXT_FLAG_CAP_WORDS;
+ protected static final int FLAGS_NOTE =
+ EditorInfo.TYPE_CLASS_TEXT
+ | EditorInfo.TYPE_TEXT_FLAG_CAP_SENTENCES
+ | EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE;
+ protected static final int FLAGS_EVENT = EditorInfo.TYPE_CLASS_TEXT;
+ protected static final int FLAGS_WEBSITE =
+ EditorInfo.TYPE_CLASS_TEXT | EditorInfo.TYPE_TEXT_VARIATION_URI;
+ protected static final int FLAGS_POSTAL =
+ EditorInfo.TYPE_CLASS_TEXT
+ | EditorInfo.TYPE_TEXT_VARIATION_POSTAL_ADDRESS
+ | EditorInfo.TYPE_TEXT_FLAG_CAP_WORDS
+ | EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE;
+ protected static final int FLAGS_SIP_ADDRESS =
+ EditorInfo.TYPE_CLASS_TEXT
+ | EditorInfo.TYPE_TEXT_VARIATION_EMAIL_ADDRESS; // since SIP addresses have the same
+ // basic format as email addresses
+ protected static final int FLAGS_RELATION =
+ EditorInfo.TYPE_CLASS_TEXT
+ | EditorInfo.TYPE_TEXT_FLAG_CAP_WORDS
+ | EditorInfo.TYPE_TEXT_VARIATION_PERSON_NAME;
+
+ // Specify the maximum number of lines that can be used to display various field types. If no
+ // value is specified for a particular type, we use the default value from {@link DataKind}.
+ protected static final int MAX_LINES_FOR_POSTAL_ADDRESS = 10;
+ protected static final int MAX_LINES_FOR_GROUP = 10;
+ protected static final int MAX_LINES_FOR_NOTE = 100;
+ private static final String TAG = "BaseAccountType";
+
+ public BaseAccountType() {
+ this.accountType = null;
+ this.dataSet = null;
+ this.titleRes = R.string.account_phone;
+ this.iconRes = R.mipmap.ic_contacts_launcher;
+ }
+
+ protected static EditType buildPhoneType(int type) {
+ return new EditType(type, Phone.getTypeLabelResource(type));
+ }
+
+ protected static EditType buildEmailType(int type) {
+ return new EditType(type, Email.getTypeLabelResource(type));
+ }
+
+ protected static EditType buildPostalType(int type) {
+ return new EditType(type, StructuredPostal.getTypeLabelResource(type));
+ }
+
+ protected static EditType buildImType(int type) {
+ return new EditType(type, Im.getProtocolLabelResource(type));
+ }
+
+ protected static EditType buildEventType(int type, boolean yearOptional) {
+ return new EventEditType(type, Event.getTypeResource(type)).setYearOptional(yearOptional);
+ }
+
+ protected static EditType buildRelationType(int type) {
+ return new EditType(type, Relation.getTypeLabelResource(type));
+ }
+
+ // Utility methods to keep code shorter.
+ private static boolean getAttr(AttributeSet attrs, String attribute, boolean defaultValue) {
+ return attrs.getAttributeBooleanValue(null, attribute, defaultValue);
+ }
+
+ private static int getAttr(AttributeSet attrs, String attribute, int defaultValue) {
+ return attrs.getAttributeIntValue(null, attribute, defaultValue);
+ }
+
+ private static String getAttr(AttributeSet attrs, String attribute) {
+ return attrs.getAttributeValue(null, attribute);
+ }
+
+ protected DataKind addDataKindStructuredName(Context context) throws DefinitionException {
+ DataKind kind =
+ addKind(
+ new DataKind(
+ StructuredName.CONTENT_ITEM_TYPE, R.string.nameLabelsGroup, Weight.NONE, true));
+ kind.actionHeader = new SimpleInflater(R.string.nameLabelsGroup);
+ kind.actionBody = new SimpleInflater(Nickname.NAME);
+ kind.typeOverallMax = 1;
+
+ kind.fieldList = new ArrayList<>();
+ kind.fieldList.add(
+ new EditField(StructuredName.DISPLAY_NAME, R.string.full_name, FLAGS_PERSON_NAME));
+ kind.fieldList.add(
+ new EditField(StructuredName.PREFIX, R.string.name_prefix, FLAGS_PERSON_NAME)
+ .setLongForm(true));
+ kind.fieldList.add(
+ new EditField(StructuredName.FAMILY_NAME, R.string.name_family, FLAGS_PERSON_NAME)
+ .setLongForm(true));
+ kind.fieldList.add(
+ new EditField(StructuredName.MIDDLE_NAME, R.string.name_middle, FLAGS_PERSON_NAME)
+ .setLongForm(true));
+ kind.fieldList.add(
+ new EditField(StructuredName.GIVEN_NAME, R.string.name_given, FLAGS_PERSON_NAME)
+ .setLongForm(true));
+ kind.fieldList.add(
+ new EditField(StructuredName.SUFFIX, R.string.name_suffix, FLAGS_PERSON_NAME)
+ .setLongForm(true));
+ kind.fieldList.add(
+ new EditField(
+ StructuredName.PHONETIC_FAMILY_NAME, R.string.name_phonetic_family, FLAGS_PHONETIC));
+ kind.fieldList.add(
+ new EditField(
+ StructuredName.PHONETIC_MIDDLE_NAME, R.string.name_phonetic_middle, FLAGS_PHONETIC));
+ kind.fieldList.add(
+ new EditField(
+ StructuredName.PHONETIC_GIVEN_NAME, R.string.name_phonetic_given, FLAGS_PHONETIC));
+
+ return kind;
+ }
+
+ protected DataKind addDataKindDisplayName(Context context) throws DefinitionException {
+ DataKind kind =
+ addKind(
+ new DataKind(
+ DataKind.PSEUDO_MIME_TYPE_DISPLAY_NAME,
+ R.string.nameLabelsGroup,
+ Weight.NONE,
+ true));
+ kind.actionHeader = new SimpleInflater(R.string.nameLabelsGroup);
+ kind.actionBody = new SimpleInflater(Nickname.NAME);
+ kind.typeOverallMax = 1;
+
+ kind.fieldList = new ArrayList<>();
+ kind.fieldList.add(
+ new EditField(StructuredName.DISPLAY_NAME, R.string.full_name, FLAGS_PERSON_NAME)
+ .setShortForm(true));
+
+ boolean displayOrderPrimary =
+ context.getResources().getBoolean(R.bool.config_editor_field_order_primary);
+
+ if (!displayOrderPrimary) {
+ kind.fieldList.add(
+ new EditField(StructuredName.PREFIX, R.string.name_prefix, FLAGS_PERSON_NAME)
+ .setLongForm(true));
+ kind.fieldList.add(
+ new EditField(StructuredName.FAMILY_NAME, R.string.name_family, FLAGS_PERSON_NAME)
+ .setLongForm(true));
+ kind.fieldList.add(
+ new EditField(StructuredName.MIDDLE_NAME, R.string.name_middle, FLAGS_PERSON_NAME)
+ .setLongForm(true));
+ kind.fieldList.add(
+ new EditField(StructuredName.GIVEN_NAME, R.string.name_given, FLAGS_PERSON_NAME)
+ .setLongForm(true));
+ kind.fieldList.add(
+ new EditField(StructuredName.SUFFIX, R.string.name_suffix, FLAGS_PERSON_NAME)
+ .setLongForm(true));
+ } else {
+ kind.fieldList.add(
+ new EditField(StructuredName.PREFIX, R.string.name_prefix, FLAGS_PERSON_NAME)
+ .setLongForm(true));
+ kind.fieldList.add(
+ new EditField(StructuredName.GIVEN_NAME, R.string.name_given, FLAGS_PERSON_NAME)
+ .setLongForm(true));
+ kind.fieldList.add(
+ new EditField(StructuredName.MIDDLE_NAME, R.string.name_middle, FLAGS_PERSON_NAME)
+ .setLongForm(true));
+ kind.fieldList.add(
+ new EditField(StructuredName.FAMILY_NAME, R.string.name_family, FLAGS_PERSON_NAME)
+ .setLongForm(true));
+ kind.fieldList.add(
+ new EditField(StructuredName.SUFFIX, R.string.name_suffix, FLAGS_PERSON_NAME)
+ .setLongForm(true));
+ }
+
+ return kind;
+ }
+
+ protected DataKind addDataKindPhoneticName(Context context) throws DefinitionException {
+ DataKind kind =
+ addKind(
+ new DataKind(
+ DataKind.PSEUDO_MIME_TYPE_PHONETIC_NAME,
+ R.string.name_phonetic,
+ Weight.NONE,
+ true));
+ kind.actionHeader = new SimpleInflater(R.string.nameLabelsGroup);
+ kind.actionBody = new SimpleInflater(Nickname.NAME);
+ kind.typeOverallMax = 1;
+
+ kind.fieldList = new ArrayList<>();
+ kind.fieldList.add(
+ new EditField(DataKind.PSEUDO_COLUMN_PHONETIC_NAME, R.string.name_phonetic, FLAGS_PHONETIC)
+ .setShortForm(true));
+ kind.fieldList.add(
+ new EditField(
+ StructuredName.PHONETIC_FAMILY_NAME, R.string.name_phonetic_family, FLAGS_PHONETIC)
+ .setLongForm(true));
+ kind.fieldList.add(
+ new EditField(
+ StructuredName.PHONETIC_MIDDLE_NAME, R.string.name_phonetic_middle, FLAGS_PHONETIC)
+ .setLongForm(true));
+ kind.fieldList.add(
+ new EditField(
+ StructuredName.PHONETIC_GIVEN_NAME, R.string.name_phonetic_given, FLAGS_PHONETIC)
+ .setLongForm(true));
+
+ return kind;
+ }
+
+ protected DataKind addDataKindNickname(Context context) throws DefinitionException {
+ DataKind kind =
+ addKind(
+ new DataKind(
+ Nickname.CONTENT_ITEM_TYPE, R.string.nicknameLabelsGroup, Weight.NICKNAME, true));
+ kind.typeOverallMax = 1;
+ kind.actionHeader = new SimpleInflater(R.string.nicknameLabelsGroup);
+ kind.actionBody = new SimpleInflater(Nickname.NAME);
+ kind.defaultValues = new ContentValues();
+ kind.defaultValues.put(Nickname.TYPE, Nickname.TYPE_DEFAULT);
+
+ kind.fieldList = new ArrayList<>();
+ kind.fieldList.add(
+ new EditField(Nickname.NAME, R.string.nicknameLabelsGroup, FLAGS_PERSON_NAME));
+
+ return kind;
+ }
+
+ protected DataKind addDataKindPhone(Context context) throws DefinitionException {
+ DataKind kind =
+ addKind(
+ new DataKind(Phone.CONTENT_ITEM_TYPE, R.string.phoneLabelsGroup, Weight.PHONE, true));
+ kind.iconAltRes = R.drawable.ic_message_24dp;
+ kind.iconAltDescriptionRes = R.string.sms;
+ kind.actionHeader = new PhoneActionInflater();
+ kind.actionAltHeader = new PhoneActionAltInflater();
+ kind.actionBody = new SimpleInflater(Phone.NUMBER);
+ kind.typeColumn = Phone.TYPE;
+ kind.typeList = new ArrayList<>();
+ kind.typeList.add(buildPhoneType(Phone.TYPE_MOBILE));
+ kind.typeList.add(buildPhoneType(Phone.TYPE_HOME));
+ kind.typeList.add(buildPhoneType(Phone.TYPE_WORK));
+ kind.typeList.add(buildPhoneType(Phone.TYPE_FAX_WORK).setSecondary(true));
+ kind.typeList.add(buildPhoneType(Phone.TYPE_FAX_HOME).setSecondary(true));
+ kind.typeList.add(buildPhoneType(Phone.TYPE_PAGER).setSecondary(true));
+ kind.typeList.add(buildPhoneType(Phone.TYPE_OTHER));
+ kind.typeList.add(
+ buildPhoneType(Phone.TYPE_CUSTOM).setSecondary(true).setCustomColumn(Phone.LABEL));
+ kind.typeList.add(buildPhoneType(Phone.TYPE_CALLBACK).setSecondary(true));
+ kind.typeList.add(buildPhoneType(Phone.TYPE_CAR).setSecondary(true));
+ kind.typeList.add(buildPhoneType(Phone.TYPE_COMPANY_MAIN).setSecondary(true));
+ kind.typeList.add(buildPhoneType(Phone.TYPE_ISDN).setSecondary(true));
+ kind.typeList.add(buildPhoneType(Phone.TYPE_MAIN).setSecondary(true));
+ kind.typeList.add(buildPhoneType(Phone.TYPE_OTHER_FAX).setSecondary(true));
+ kind.typeList.add(buildPhoneType(Phone.TYPE_RADIO).setSecondary(true));
+ kind.typeList.add(buildPhoneType(Phone.TYPE_TELEX).setSecondary(true));
+ kind.typeList.add(buildPhoneType(Phone.TYPE_TTY_TDD).setSecondary(true));
+ kind.typeList.add(buildPhoneType(Phone.TYPE_WORK_MOBILE).setSecondary(true));
+ kind.typeList.add(buildPhoneType(Phone.TYPE_WORK_PAGER).setSecondary(true));
+ kind.typeList.add(buildPhoneType(Phone.TYPE_ASSISTANT).setSecondary(true));
+ kind.typeList.add(buildPhoneType(Phone.TYPE_MMS).setSecondary(true));
+
+ kind.fieldList = new ArrayList<>();
+ kind.fieldList.add(new EditField(Phone.NUMBER, R.string.phoneLabelsGroup, FLAGS_PHONE));
+
+ return kind;
+ }
+
+ protected DataKind addDataKindEmail(Context context) throws DefinitionException {
+ DataKind kind =
+ addKind(
+ new DataKind(Email.CONTENT_ITEM_TYPE, R.string.emailLabelsGroup, Weight.EMAIL, true));
+ kind.actionHeader = new EmailActionInflater();
+ kind.actionBody = new SimpleInflater(Email.DATA);
+ kind.typeColumn = Email.TYPE;
+ kind.typeList = new ArrayList<>();
+ kind.typeList.add(buildEmailType(Email.TYPE_HOME));
+ kind.typeList.add(buildEmailType(Email.TYPE_WORK));
+ kind.typeList.add(buildEmailType(Email.TYPE_OTHER));
+ kind.typeList.add(buildEmailType(Email.TYPE_MOBILE));
+ kind.typeList.add(
+ buildEmailType(Email.TYPE_CUSTOM).setSecondary(true).setCustomColumn(Email.LABEL));
+
+ kind.fieldList = new ArrayList<>();
+ kind.fieldList.add(new EditField(Email.DATA, R.string.emailLabelsGroup, FLAGS_EMAIL));
+
+ return kind;
+ }
+
+ protected DataKind addDataKindStructuredPostal(Context context) throws DefinitionException {
+ DataKind kind =
+ addKind(
+ new DataKind(
+ StructuredPostal.CONTENT_ITEM_TYPE,
+ R.string.postalLabelsGroup,
+ Weight.STRUCTURED_POSTAL,
+ true));
+ kind.actionHeader = new PostalActionInflater();
+ kind.actionBody = new SimpleInflater(StructuredPostal.FORMATTED_ADDRESS);
+ kind.typeColumn = StructuredPostal.TYPE;
+ kind.typeList = new ArrayList<>();
+ kind.typeList.add(buildPostalType(StructuredPostal.TYPE_HOME));
+ kind.typeList.add(buildPostalType(StructuredPostal.TYPE_WORK));
+ kind.typeList.add(buildPostalType(StructuredPostal.TYPE_OTHER));
+ kind.typeList.add(
+ buildPostalType(StructuredPostal.TYPE_CUSTOM)
+ .setSecondary(true)
+ .setCustomColumn(StructuredPostal.LABEL));
+
+ kind.fieldList = new ArrayList<>();
+ kind.fieldList.add(
+ new EditField(StructuredPostal.FORMATTED_ADDRESS, R.string.postal_address, FLAGS_POSTAL));
+
+ kind.maxLinesForDisplay = MAX_LINES_FOR_POSTAL_ADDRESS;
+
+ return kind;
+ }
+
+ protected DataKind addDataKindIm(Context context) throws DefinitionException {
+ DataKind kind =
+ addKind(new DataKind(Im.CONTENT_ITEM_TYPE, R.string.imLabelsGroup, Weight.IM, true));
+ kind.actionHeader = new ImActionInflater();
+ kind.actionBody = new SimpleInflater(Im.DATA);
+
+ // NOTE: even though a traditional "type" exists, for editing
+ // purposes we're using the protocol to pick labels
+
+ kind.defaultValues = new ContentValues();
+ kind.defaultValues.put(Im.TYPE, Im.TYPE_OTHER);
+
+ kind.typeColumn = Im.PROTOCOL;
+ kind.typeList = new ArrayList<>();
+ kind.typeList.add(buildImType(Im.PROTOCOL_AIM));
+ kind.typeList.add(buildImType(Im.PROTOCOL_MSN));
+ kind.typeList.add(buildImType(Im.PROTOCOL_YAHOO));
+ kind.typeList.add(buildImType(Im.PROTOCOL_SKYPE));
+ kind.typeList.add(buildImType(Im.PROTOCOL_QQ));
+ kind.typeList.add(buildImType(Im.PROTOCOL_GOOGLE_TALK));
+ kind.typeList.add(buildImType(Im.PROTOCOL_ICQ));
+ kind.typeList.add(buildImType(Im.PROTOCOL_JABBER));
+ kind.typeList.add(
+ buildImType(Im.PROTOCOL_CUSTOM).setSecondary(true).setCustomColumn(Im.CUSTOM_PROTOCOL));
+
+ kind.fieldList = new ArrayList<>();
+ kind.fieldList.add(new EditField(Im.DATA, R.string.imLabelsGroup, FLAGS_EMAIL));
+
+ return kind;
+ }
+
+ protected DataKind addDataKindOrganization(Context context) throws DefinitionException {
+ DataKind kind =
+ addKind(
+ new DataKind(
+ Organization.CONTENT_ITEM_TYPE,
+ R.string.organizationLabelsGroup,
+ Weight.ORGANIZATION,
+ true));
+ kind.actionHeader = new SimpleInflater(R.string.organizationLabelsGroup);
+ kind.actionBody = ORGANIZATION_BODY_INFLATER;
+ kind.typeOverallMax = 1;
+
+ kind.fieldList = new ArrayList<>();
+ kind.fieldList.add(
+ new EditField(Organization.COMPANY, R.string.ghostData_company, FLAGS_GENERIC_NAME));
+ kind.fieldList.add(
+ new EditField(Organization.TITLE, R.string.ghostData_title, FLAGS_GENERIC_NAME));
+
+ return kind;
+ }
+
+ protected DataKind addDataKindPhoto(Context context) throws DefinitionException {
+ DataKind kind = addKind(new DataKind(Photo.CONTENT_ITEM_TYPE, -1, Weight.NONE, true));
+ kind.typeOverallMax = 1;
+ kind.fieldList = new ArrayList<>();
+ kind.fieldList.add(new EditField(Photo.PHOTO, -1, -1));
+ return kind;
+ }
+
+ protected DataKind addDataKindNote(Context context) throws DefinitionException {
+ DataKind kind =
+ addKind(new DataKind(Note.CONTENT_ITEM_TYPE, R.string.label_notes, Weight.NOTE, true));
+ kind.typeOverallMax = 1;
+ kind.actionHeader = new SimpleInflater(R.string.label_notes);
+ kind.actionBody = new SimpleInflater(Note.NOTE);
+ kind.fieldList = new ArrayList<>();
+ kind.fieldList.add(new EditField(Note.NOTE, R.string.label_notes, FLAGS_NOTE));
+
+ kind.maxLinesForDisplay = MAX_LINES_FOR_NOTE;
+
+ return kind;
+ }
+
+ protected DataKind addDataKindWebsite(Context context) throws DefinitionException {
+ DataKind kind =
+ addKind(
+ new DataKind(
+ Website.CONTENT_ITEM_TYPE, R.string.websiteLabelsGroup, Weight.WEBSITE, true));
+ kind.actionHeader = new SimpleInflater(R.string.websiteLabelsGroup);
+ kind.actionBody = new SimpleInflater(Website.URL);
+ kind.defaultValues = new ContentValues();
+ kind.defaultValues.put(Website.TYPE, Website.TYPE_OTHER);
+
+ kind.fieldList = new ArrayList<>();
+ kind.fieldList.add(new EditField(Website.URL, R.string.websiteLabelsGroup, FLAGS_WEBSITE));
+
+ return kind;
+ }
+
+ protected DataKind addDataKindSipAddress(Context context) throws DefinitionException {
+ DataKind kind =
+ addKind(
+ new DataKind(
+ SipAddress.CONTENT_ITEM_TYPE,
+ R.string.label_sip_address,
+ Weight.SIP_ADDRESS,
+ true));
+
+ kind.actionHeader = new SimpleInflater(R.string.label_sip_address);
+ kind.actionBody = new SimpleInflater(SipAddress.SIP_ADDRESS);
+ kind.fieldList = new ArrayList<>();
+ kind.fieldList.add(
+ new EditField(SipAddress.SIP_ADDRESS, R.string.label_sip_address, FLAGS_SIP_ADDRESS));
+ kind.typeOverallMax = 1;
+
+ return kind;
+ }
+
+ protected DataKind addDataKindGroupMembership(Context context) throws DefinitionException {
+ DataKind kind =
+ addKind(
+ new DataKind(
+ GroupMembership.CONTENT_ITEM_TYPE,
+ R.string.groupsLabel,
+ Weight.GROUP_MEMBERSHIP,
+ true));
+
+ kind.typeOverallMax = 1;
+ kind.fieldList = new ArrayList<>();
+ kind.fieldList.add(new EditField(GroupMembership.GROUP_ROW_ID, -1, -1));
+
+ kind.maxLinesForDisplay = MAX_LINES_FOR_GROUP;
+
+ return kind;
+ }
+
+ @Override
+ public boolean isGroupMembershipEditable() {
+ return false;
+ }
+
+ /** Parses the content of the EditSchema tag in contacts.xml. */
+ protected final void parseEditSchema(Context context, XmlPullParser parser, AttributeSet attrs)
+ throws XmlPullParserException, IOException, DefinitionException {
+
+ final int outerDepth = parser.getDepth();
+ int type;
+ while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
+ && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) {
+ final int depth = parser.getDepth();
+ if (type != XmlPullParser.START_TAG || depth != outerDepth + 1) {
+ continue; // Not direct child tag
+ }
+
+ final String tag = parser.getName();
+
+ if (Tag.DATA_KIND.equals(tag)) {
+ for (DataKind kind : KindParser.INSTANCE.parseDataKindTag(context, parser, attrs)) {
+ addKind(kind);
+ }
+ } else {
+ Log.w(TAG, "Skipping unknown tag " + tag);
+ }
+ }
+ }
+
+ private interface Tag {
+
+ String DATA_KIND = "DataKind";
+ String TYPE = "Type";
+ }
+
+ private interface Attr {
+
+ String MAX_OCCURRENCE = "maxOccurs";
+ String DATE_WITH_TIME = "dateWithTime";
+ String YEAR_OPTIONAL = "yearOptional";
+ String KIND = "kind";
+ String TYPE = "type";
+ }
+
+ protected interface Weight {
+
+ int NONE = -1;
+ int PHONE = 10;
+ int EMAIL = 15;
+ int STRUCTURED_POSTAL = 25;
+ int NICKNAME = 111;
+ int EVENT = 120;
+ int ORGANIZATION = 125;
+ int NOTE = 130;
+ int IM = 140;
+ int SIP_ADDRESS = 145;
+ int GROUP_MEMBERSHIP = 150;
+ int WEBSITE = 160;
+ int RELATIONSHIP = 999;
+ }
+
+ /**
+ * Simple inflater that assumes a string resource has a "%s" that will be filled from the given
+ * column.
+ */
+ public static class SimpleInflater implements StringInflater {
+
+ private final int mStringRes;
+ private final String mColumnName;
+
+ public SimpleInflater(int stringRes) {
+ this(stringRes, null);
+ }
+
+ public SimpleInflater(String columnName) {
+ this(-1, columnName);
+ }
+
+ public SimpleInflater(int stringRes, String columnName) {
+ mStringRes = stringRes;
+ mColumnName = columnName;
+ }
+
+ @Override
+ public CharSequence inflateUsing(Context context, ContentValues values) {
+ final boolean validColumn = values.containsKey(mColumnName);
+ final boolean validString = mStringRes > 0;
+
+ final CharSequence stringValue = validString ? context.getText(mStringRes) : null;
+ final CharSequence columnValue = validColumn ? values.getAsString(mColumnName) : null;
+
+ if (validString && validColumn) {
+ return String.format(stringValue.toString(), columnValue);
+ } else if (validString) {
+ return stringValue;
+ } else if (validColumn) {
+ return columnValue;
+ } else {
+ return null;
+ }
+ }
+
+ @Override
+ public String toString() {
+ return this.getClass().getSimpleName()
+ + " mStringRes="
+ + mStringRes
+ + " mColumnName"
+ + mColumnName;
+ }
+
+ public String getColumnNameForTest() {
+ return mColumnName;
+ }
+ }
+
+ public abstract static class CommonInflater implements StringInflater {
+
+ protected abstract int getTypeLabelResource(Integer type);
+
+ protected boolean isCustom(Integer type) {
+ return type == BaseTypes.TYPE_CUSTOM;
+ }
+
+ protected String getTypeColumn() {
+ return Phone.TYPE;
+ }
+
+ protected String getLabelColumn() {
+ return Phone.LABEL;
+ }
+
+ protected CharSequence getTypeLabel(Resources res, Integer type, CharSequence label) {
+ final int labelRes = getTypeLabelResource(type);
+ if (type == null) {
+ return res.getText(labelRes);
+ } else if (isCustom(type)) {
+ return res.getString(labelRes, label == null ? "" : label);
+ } else {
+ return res.getText(labelRes);
+ }
+ }
+
+ @Override
+ public CharSequence inflateUsing(Context context, ContentValues values) {
+ final Integer type = values.getAsInteger(getTypeColumn());
+ final String label = values.getAsString(getLabelColumn());
+ return getTypeLabel(context.getResources(), type, label);
+ }
+
+ @Override
+ public String toString() {
+ return this.getClass().getSimpleName();
+ }
+ }
+
+ public static class PhoneActionInflater extends CommonInflater {
+
+ @Override
+ protected boolean isCustom(Integer type) {
+ return ContactDisplayUtils.isCustomPhoneType(type);
+ }
+
+ @Override
+ protected int getTypeLabelResource(Integer type) {
+ return ContactDisplayUtils.getPhoneLabelResourceId(type);
+ }
+ }
+
+ public static class PhoneActionAltInflater extends CommonInflater {
+
+ @Override
+ protected boolean isCustom(Integer type) {
+ return ContactDisplayUtils.isCustomPhoneType(type);
+ }
+
+ @Override
+ protected int getTypeLabelResource(Integer type) {
+ return ContactDisplayUtils.getSmsLabelResourceId(type);
+ }
+ }
+
+ public static class EmailActionInflater extends CommonInflater {
+
+ @Override
+ protected int getTypeLabelResource(Integer type) {
+ if (type == null) {
+ return R.string.email;
+ }
+ switch (type) {
+ case Email.TYPE_HOME:
+ return R.string.email_home;
+ case Email.TYPE_WORK:
+ return R.string.email_work;
+ case Email.TYPE_OTHER:
+ return R.string.email_other;
+ case Email.TYPE_MOBILE:
+ return R.string.email_mobile;
+ default:
+ return R.string.email_custom;
+ }
+ }
+ }
+
+ public static class EventActionInflater extends CommonInflater {
+
+ @Override
+ protected int getTypeLabelResource(Integer type) {
+ return Event.getTypeResource(type);
+ }
+ }
+
+ public static class RelationActionInflater extends CommonInflater {
+
+ @Override
+ protected int getTypeLabelResource(Integer type) {
+ return Relation.getTypeLabelResource(type == null ? Relation.TYPE_CUSTOM : type);
+ }
+ }
+
+ public static class PostalActionInflater extends CommonInflater {
+
+ @Override
+ protected int getTypeLabelResource(Integer type) {
+ if (type == null) {
+ return R.string.map_other;
+ }
+ switch (type) {
+ case StructuredPostal.TYPE_HOME:
+ return R.string.map_home;
+ case StructuredPostal.TYPE_WORK:
+ return R.string.map_work;
+ case StructuredPostal.TYPE_OTHER:
+ return R.string.map_other;
+ default:
+ return R.string.map_custom;
+ }
+ }
+ }
+
+ public static class ImActionInflater extends CommonInflater {
+
+ @Override
+ protected String getTypeColumn() {
+ return Im.PROTOCOL;
+ }
+
+ @Override
+ protected String getLabelColumn() {
+ return Im.CUSTOM_PROTOCOL;
+ }
+
+ @Override
+ protected int getTypeLabelResource(Integer type) {
+ if (type == null) {
+ return R.string.chat;
+ }
+ switch (type) {
+ case Im.PROTOCOL_AIM:
+ return R.string.chat_aim;
+ case Im.PROTOCOL_MSN:
+ return R.string.chat_msn;
+ case Im.PROTOCOL_YAHOO:
+ return R.string.chat_yahoo;
+ case Im.PROTOCOL_SKYPE:
+ return R.string.chat_skype;
+ case Im.PROTOCOL_QQ:
+ return R.string.chat_qq;
+ case Im.PROTOCOL_GOOGLE_TALK:
+ return R.string.chat_gtalk;
+ case Im.PROTOCOL_ICQ:
+ return R.string.chat_icq;
+ case Im.PROTOCOL_JABBER:
+ return R.string.chat_jabber;
+ case Im.PROTOCOL_NETMEETING:
+ return R.string.chat;
+ default:
+ return R.string.chat;
+ }
+ }
+ }
+
+ // TODO Extract it to its own class, and move all KindBuilders to it as well.
+ private static class KindParser {
+
+ public static final KindParser INSTANCE = new KindParser();
+
+ private final Map<String, KindBuilder> mBuilders = new ArrayMap<>();
+
+ private KindParser() {
+ addBuilder(new NameKindBuilder());
+ addBuilder(new NicknameKindBuilder());
+ addBuilder(new PhoneKindBuilder());
+ addBuilder(new EmailKindBuilder());
+ addBuilder(new StructuredPostalKindBuilder());
+ addBuilder(new ImKindBuilder());
+ addBuilder(new OrganizationKindBuilder());
+ addBuilder(new PhotoKindBuilder());
+ addBuilder(new NoteKindBuilder());
+ addBuilder(new WebsiteKindBuilder());
+ addBuilder(new SipAddressKindBuilder());
+ addBuilder(new GroupMembershipKindBuilder());
+ addBuilder(new EventKindBuilder());
+ addBuilder(new RelationshipKindBuilder());
+ }
+
+ private void addBuilder(KindBuilder builder) {
+ mBuilders.put(builder.getTagName(), builder);
+ }
+
+ /**
+ * Takes a {@link XmlPullParser} at the start of a DataKind tag, parses it and returns {@link
+ * DataKind}s. (Usually just one, but there are three for the "name" kind.)
+ *
+ * <p>This method returns a list, because we need to add 3 kinds for the name data kind.
+ * (structured, display and phonetic)
+ */
+ public List<DataKind> parseDataKindTag(
+ Context context, XmlPullParser parser, AttributeSet attrs)
+ throws DefinitionException, XmlPullParserException, IOException {
+ final String kind = getAttr(attrs, Attr.KIND);
+ final KindBuilder builder = mBuilders.get(kind);
+ if (builder != null) {
+ return builder.parseDataKind(context, parser, attrs);
+ } else {
+ throw new DefinitionException("Undefined data kind '" + kind + "'");
+ }
+ }
+ }
+
+ private abstract static class KindBuilder {
+
+ public abstract String getTagName();
+
+ /** DataKind tag parser specific to each kind. Subclasses must implement it. */
+ public abstract List<DataKind> parseDataKind(
+ Context context, XmlPullParser parser, AttributeSet attrs)
+ throws DefinitionException, XmlPullParserException, IOException;
+
+ /** Creates a new {@link DataKind}, and also parses the child Type tags in the DataKind tag. */
+ protected final DataKind newDataKind(
+ Context context,
+ XmlPullParser parser,
+ AttributeSet attrs,
+ boolean isPseudo,
+ String mimeType,
+ String typeColumn,
+ int titleRes,
+ int weight,
+ StringInflater actionHeader,
+ StringInflater actionBody)
+ throws DefinitionException, XmlPullParserException, IOException {
+
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "Adding DataKind: " + mimeType);
+ }
+
+ final DataKind kind = new DataKind(mimeType, titleRes, weight, true);
+ kind.typeColumn = typeColumn;
+ kind.actionHeader = actionHeader;
+ kind.actionBody = actionBody;
+ kind.fieldList = new ArrayList<>();
+
+ // Get more information from the tag...
+ // A pseudo data kind doesn't have corresponding tag the XML, so we skip this.
+ if (!isPseudo) {
+ kind.typeOverallMax = getAttr(attrs, Attr.MAX_OCCURRENCE, -1);
+
+ // Process "Type" tags.
+ // If a kind has the type column, contacts.xml must have at least one type
+ // definition. Otherwise, it mustn't have a type definition.
+ if (kind.typeColumn != null) {
+ // Parse and add types.
+ kind.typeList = new ArrayList<>();
+ parseTypes(context, parser, attrs, kind, true);
+ if (kind.typeList.size() == 0) {
+ throw new DefinitionException("Kind " + kind.mimeType + " must have at least one type");
+ }
+ } else {
+ // Make sure it has no types.
+ parseTypes(context, parser, attrs, kind, false /* can't have types */);
+ }
+ }
+
+ return kind;
+ }
+
+ /**
+ * Parses Type elements in a DataKind element, and if {@code canHaveTypes} is true adds them to
+ * the given {@link DataKind}. Otherwise the {@link DataKind} can't have a type, so throws
+ * {@link DefinitionException}.
+ */
+ private void parseTypes(
+ Context context,
+ XmlPullParser parser,
+ AttributeSet attrs,
+ DataKind kind,
+ boolean canHaveTypes)
+ throws DefinitionException, XmlPullParserException, IOException {
+ final int outerDepth = parser.getDepth();
+ int type;
+ while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
+ && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) {
+ final int depth = parser.getDepth();
+ if (type != XmlPullParser.START_TAG || depth != outerDepth + 1) {
+ continue; // Not direct child tag
+ }
+
+ final String tag = parser.getName();
+ if (Tag.TYPE.equals(tag)) {
+ if (canHaveTypes) {
+ kind.typeList.add(parseTypeTag(parser, attrs, kind));
+ } else {
+ throw new DefinitionException("Kind " + kind.mimeType + " can't have types");
+ }
+ } else {
+ throw new DefinitionException("Unknown tag: " + tag);
+ }
+ }
+ }
+
+ /**
+ * Parses a single Type element and returns an {@link EditType} built from it. Uses {@link
+ * #buildEditTypeForTypeTag} defined in subclasses to actually build an {@link EditType}.
+ */
+ private EditType parseTypeTag(XmlPullParser parser, AttributeSet attrs, DataKind kind)
+ throws DefinitionException {
+
+ final String typeName = getAttr(attrs, Attr.TYPE);
+
+ final EditType et = buildEditTypeForTypeTag(attrs, typeName);
+ if (et == null) {
+ throw new DefinitionException(
+ "Undefined type '" + typeName + "' for data kind '" + kind.mimeType + "'");
+ }
+ et.specificMax = getAttr(attrs, Attr.MAX_OCCURRENCE, -1);
+
+ return et;
+ }
+
+ /**
+ * Returns an {@link EditType} for the given "type". Subclasses may optionally use the
+ * attributes in the tag to set optional values. (e.g. "yearOptional" for the event kind)
+ */
+ protected EditType buildEditTypeForTypeTag(AttributeSet attrs, String type) {
+ return null;
+ }
+
+ protected final void throwIfList(DataKind kind) throws DefinitionException {
+ if (kind.typeOverallMax != 1) {
+ throw new DefinitionException("Kind " + kind.mimeType + " must have 'overallMax=\"1\"'");
+ }
+ }
+ }
+
+ /** DataKind parser for Name. (structured, display, phonetic) */
+ private static class NameKindBuilder extends KindBuilder {
+
+ private static void checkAttributeTrue(boolean value, String attrName)
+ throws DefinitionException {
+ if (!value) {
+ throw new DefinitionException(attrName + " must be true");
+ }
+ }
+
+ @Override
+ public String getTagName() {
+ return "name";
+ }
+
+ @Override
+ public List<DataKind> parseDataKind(Context context, XmlPullParser parser, AttributeSet attrs)
+ throws DefinitionException, XmlPullParserException, IOException {
+
+ // Build 3 data kinds:
+ // - StructuredName.CONTENT_ITEM_TYPE
+ // - DataKind.PSEUDO_MIME_TYPE_DISPLAY_NAME
+ // - DataKind.PSEUDO_MIME_TYPE_PHONETIC_NAME
+
+ final boolean displayOrderPrimary =
+ context.getResources().getBoolean(R.bool.config_editor_field_order_primary);
+
+ final boolean supportsDisplayName = getAttr(attrs, "supportsDisplayName", false);
+ final boolean supportsPrefix = getAttr(attrs, "supportsPrefix", false);
+ final boolean supportsMiddleName = getAttr(attrs, "supportsMiddleName", false);
+ final boolean supportsSuffix = getAttr(attrs, "supportsSuffix", false);
+ final boolean supportsPhoneticFamilyName =
+ getAttr(attrs, "supportsPhoneticFamilyName", false);
+ final boolean supportsPhoneticMiddleName =
+ getAttr(attrs, "supportsPhoneticMiddleName", false);
+ final boolean supportsPhoneticGivenName = getAttr(attrs, "supportsPhoneticGivenName", false);
+
+ // For now, every things must be supported.
+ checkAttributeTrue(supportsDisplayName, "supportsDisplayName");
+ checkAttributeTrue(supportsPrefix, "supportsPrefix");
+ checkAttributeTrue(supportsMiddleName, "supportsMiddleName");
+ checkAttributeTrue(supportsSuffix, "supportsSuffix");
+ checkAttributeTrue(supportsPhoneticFamilyName, "supportsPhoneticFamilyName");
+ checkAttributeTrue(supportsPhoneticMiddleName, "supportsPhoneticMiddleName");
+ checkAttributeTrue(supportsPhoneticGivenName, "supportsPhoneticGivenName");
+
+ final List<DataKind> kinds = new ArrayList<>();
+
+ // Structured name
+ final DataKind ks =
+ newDataKind(
+ context,
+ parser,
+ attrs,
+ false,
+ StructuredName.CONTENT_ITEM_TYPE,
+ null,
+ R.string.nameLabelsGroup,
+ Weight.NONE,
+ new SimpleInflater(R.string.nameLabelsGroup),
+ new SimpleInflater(Nickname.NAME));
+
+ throwIfList(ks);
+ kinds.add(ks);
+
+ // Note about setLongForm/setShortForm below.
+ // We need to set this only when the type supports display name. (=supportsDisplayName)
+ // Otherwise (i.e. Exchange) we don't set these flags, but instead make some fields
+ // "optional".
+
+ ks.fieldList.add(
+ new EditField(StructuredName.DISPLAY_NAME, R.string.full_name, FLAGS_PERSON_NAME));
+ ks.fieldList.add(
+ new EditField(StructuredName.PREFIX, R.string.name_prefix, FLAGS_PERSON_NAME)
+ .setLongForm(true));
+ ks.fieldList.add(
+ new EditField(StructuredName.FAMILY_NAME, R.string.name_family, FLAGS_PERSON_NAME)
+ .setLongForm(true));
+ ks.fieldList.add(
+ new EditField(StructuredName.MIDDLE_NAME, R.string.name_middle, FLAGS_PERSON_NAME)
+ .setLongForm(true));
+ ks.fieldList.add(
+ new EditField(StructuredName.GIVEN_NAME, R.string.name_given, FLAGS_PERSON_NAME)
+ .setLongForm(true));
+ ks.fieldList.add(
+ new EditField(StructuredName.SUFFIX, R.string.name_suffix, FLAGS_PERSON_NAME)
+ .setLongForm(true));
+ ks.fieldList.add(
+ new EditField(
+ StructuredName.PHONETIC_FAMILY_NAME, R.string.name_phonetic_family, FLAGS_PHONETIC));
+ ks.fieldList.add(
+ new EditField(
+ StructuredName.PHONETIC_MIDDLE_NAME, R.string.name_phonetic_middle, FLAGS_PHONETIC));
+ ks.fieldList.add(
+ new EditField(
+ StructuredName.PHONETIC_GIVEN_NAME, R.string.name_phonetic_given, FLAGS_PHONETIC));
+
+ // Display name
+ final DataKind kd =
+ newDataKind(
+ context,
+ parser,
+ attrs,
+ true,
+ DataKind.PSEUDO_MIME_TYPE_DISPLAY_NAME,
+ null,
+ R.string.nameLabelsGroup,
+ Weight.NONE,
+ new SimpleInflater(R.string.nameLabelsGroup),
+ new SimpleInflater(Nickname.NAME));
+ kd.typeOverallMax = 1;
+ kinds.add(kd);
+
+ kd.fieldList.add(
+ new EditField(StructuredName.DISPLAY_NAME, R.string.full_name, FLAGS_PERSON_NAME)
+ .setShortForm(true));
+
+ if (!displayOrderPrimary) {
+ kd.fieldList.add(
+ new EditField(StructuredName.PREFIX, R.string.name_prefix, FLAGS_PERSON_NAME)
+ .setLongForm(true));
+ kd.fieldList.add(
+ new EditField(StructuredName.FAMILY_NAME, R.string.name_family, FLAGS_PERSON_NAME)
+ .setLongForm(true));
+ kd.fieldList.add(
+ new EditField(StructuredName.MIDDLE_NAME, R.string.name_middle, FLAGS_PERSON_NAME)
+ .setLongForm(true));
+ kd.fieldList.add(
+ new EditField(StructuredName.GIVEN_NAME, R.string.name_given, FLAGS_PERSON_NAME)
+ .setLongForm(true));
+ kd.fieldList.add(
+ new EditField(StructuredName.SUFFIX, R.string.name_suffix, FLAGS_PERSON_NAME)
+ .setLongForm(true));
+ } else {
+ kd.fieldList.add(
+ new EditField(StructuredName.PREFIX, R.string.name_prefix, FLAGS_PERSON_NAME)
+ .setLongForm(true));
+ kd.fieldList.add(
+ new EditField(StructuredName.GIVEN_NAME, R.string.name_given, FLAGS_PERSON_NAME)
+ .setLongForm(true));
+ kd.fieldList.add(
+ new EditField(StructuredName.MIDDLE_NAME, R.string.name_middle, FLAGS_PERSON_NAME)
+ .setLongForm(true));
+ kd.fieldList.add(
+ new EditField(StructuredName.FAMILY_NAME, R.string.name_family, FLAGS_PERSON_NAME)
+ .setLongForm(true));
+ kd.fieldList.add(
+ new EditField(StructuredName.SUFFIX, R.string.name_suffix, FLAGS_PERSON_NAME)
+ .setLongForm(true));
+ }
+
+ // Phonetic name
+ final DataKind kp =
+ newDataKind(
+ context,
+ parser,
+ attrs,
+ true,
+ DataKind.PSEUDO_MIME_TYPE_PHONETIC_NAME,
+ null,
+ R.string.name_phonetic,
+ Weight.NONE,
+ new SimpleInflater(R.string.nameLabelsGroup),
+ new SimpleInflater(Nickname.NAME));
+ kp.typeOverallMax = 1;
+ kinds.add(kp);
+
+ // We may want to change the order depending on displayOrderPrimary too.
+ kp.fieldList.add(
+ new EditField(
+ DataKind.PSEUDO_COLUMN_PHONETIC_NAME, R.string.name_phonetic, FLAGS_PHONETIC)
+ .setShortForm(true));
+ kp.fieldList.add(
+ new EditField(
+ StructuredName.PHONETIC_FAMILY_NAME,
+ R.string.name_phonetic_family,
+ FLAGS_PHONETIC)
+ .setLongForm(true));
+ kp.fieldList.add(
+ new EditField(
+ StructuredName.PHONETIC_MIDDLE_NAME,
+ R.string.name_phonetic_middle,
+ FLAGS_PHONETIC)
+ .setLongForm(true));
+ kp.fieldList.add(
+ new EditField(
+ StructuredName.PHONETIC_GIVEN_NAME, R.string.name_phonetic_given, FLAGS_PHONETIC)
+ .setLongForm(true));
+ return kinds;
+ }
+ }
+
+ private static class NicknameKindBuilder extends KindBuilder {
+
+ @Override
+ public String getTagName() {
+ return "nickname";
+ }
+
+ @Override
+ public List<DataKind> parseDataKind(Context context, XmlPullParser parser, AttributeSet attrs)
+ throws DefinitionException, XmlPullParserException, IOException {
+ final DataKind kind =
+ newDataKind(
+ context,
+ parser,
+ attrs,
+ false,
+ Nickname.CONTENT_ITEM_TYPE,
+ null,
+ R.string.nicknameLabelsGroup,
+ Weight.NICKNAME,
+ new SimpleInflater(R.string.nicknameLabelsGroup),
+ new SimpleInflater(Nickname.NAME));
+
+ kind.fieldList.add(
+ new EditField(Nickname.NAME, R.string.nicknameLabelsGroup, FLAGS_PERSON_NAME));
+
+ kind.defaultValues = new ContentValues();
+ kind.defaultValues.put(Nickname.TYPE, Nickname.TYPE_DEFAULT);
+
+ throwIfList(kind);
+ List<DataKind> result = new ArrayList<>();
+ result.add(kind);
+ return result;
+ }
+ }
+
+ private static class PhoneKindBuilder extends KindBuilder {
+
+ /** Just to avoid line-wrapping... */
+ protected static EditType build(int type, boolean secondary) {
+ return new EditType(type, Phone.getTypeLabelResource(type)).setSecondary(secondary);
+ }
+
+ @Override
+ public String getTagName() {
+ return "phone";
+ }
+
+ @Override
+ public List<DataKind> parseDataKind(Context context, XmlPullParser parser, AttributeSet attrs)
+ throws DefinitionException, XmlPullParserException, IOException {
+ final DataKind kind =
+ newDataKind(
+ context,
+ parser,
+ attrs,
+ false,
+ Phone.CONTENT_ITEM_TYPE,
+ Phone.TYPE,
+ R.string.phoneLabelsGroup,
+ Weight.PHONE,
+ new PhoneActionInflater(),
+ new SimpleInflater(Phone.NUMBER));
+
+ kind.iconAltRes = R.drawable.ic_message_24dp;
+ kind.iconAltDescriptionRes = R.string.sms;
+ kind.actionAltHeader = new PhoneActionAltInflater();
+
+ kind.fieldList.add(new EditField(Phone.NUMBER, R.string.phoneLabelsGroup, FLAGS_PHONE));
+
+ List<DataKind> result = new ArrayList<>();
+ result.add(kind);
+ return result;
+ }
+
+ @Override
+ protected EditType buildEditTypeForTypeTag(AttributeSet attrs, String type) {
+ if ("home".equals(type)) {
+ return build(Phone.TYPE_HOME, false);
+ }
+ if ("mobile".equals(type)) {
+ return build(Phone.TYPE_MOBILE, false);
+ }
+ if ("work".equals(type)) {
+ return build(Phone.TYPE_WORK, false);
+ }
+ if ("fax_work".equals(type)) {
+ return build(Phone.TYPE_FAX_WORK, true);
+ }
+ if ("fax_home".equals(type)) {
+ return build(Phone.TYPE_FAX_HOME, true);
+ }
+ if ("pager".equals(type)) {
+ return build(Phone.TYPE_PAGER, true);
+ }
+ if ("other".equals(type)) {
+ return build(Phone.TYPE_OTHER, false);
+ }
+ if ("callback".equals(type)) {
+ return build(Phone.TYPE_CALLBACK, true);
+ }
+ if ("car".equals(type)) {
+ return build(Phone.TYPE_CAR, true);
+ }
+ if ("company_main".equals(type)) {
+ return build(Phone.TYPE_COMPANY_MAIN, true);
+ }
+ if ("isdn".equals(type)) {
+ return build(Phone.TYPE_ISDN, true);
+ }
+ if ("main".equals(type)) {
+ return build(Phone.TYPE_MAIN, true);
+ }
+ if ("other_fax".equals(type)) {
+ return build(Phone.TYPE_OTHER_FAX, true);
+ }
+ if ("radio".equals(type)) {
+ return build(Phone.TYPE_RADIO, true);
+ }
+ if ("telex".equals(type)) {
+ return build(Phone.TYPE_TELEX, true);
+ }
+ if ("tty_tdd".equals(type)) {
+ return build(Phone.TYPE_TTY_TDD, true);
+ }
+ if ("work_mobile".equals(type)) {
+ return build(Phone.TYPE_WORK_MOBILE, true);
+ }
+ if ("work_pager".equals(type)) {
+ return build(Phone.TYPE_WORK_PAGER, true);
+ }
+
+ // Note "assistant" used to be a custom column for the fallback type, but not anymore.
+ if ("assistant".equals(type)) {
+ return build(Phone.TYPE_ASSISTANT, true);
+ }
+ if ("mms".equals(type)) {
+ return build(Phone.TYPE_MMS, true);
+ }
+ if ("custom".equals(type)) {
+ return build(Phone.TYPE_CUSTOM, true).setCustomColumn(Phone.LABEL);
+ }
+ return null;
+ }
+ }
+
+ private static class EmailKindBuilder extends KindBuilder {
+
+ @Override
+ public String getTagName() {
+ return "email";
+ }
+
+ @Override
+ public List<DataKind> parseDataKind(Context context, XmlPullParser parser, AttributeSet attrs)
+ throws DefinitionException, XmlPullParserException, IOException {
+ final DataKind kind =
+ newDataKind(
+ context,
+ parser,
+ attrs,
+ false,
+ Email.CONTENT_ITEM_TYPE,
+ Email.TYPE,
+ R.string.emailLabelsGroup,
+ Weight.EMAIL,
+ new EmailActionInflater(),
+ new SimpleInflater(Email.DATA));
+ kind.fieldList.add(new EditField(Email.DATA, R.string.emailLabelsGroup, FLAGS_EMAIL));
+
+ List<DataKind> result = new ArrayList<>();
+ result.add(kind);
+ return result;
+ }
+
+ @Override
+ protected EditType buildEditTypeForTypeTag(AttributeSet attrs, String type) {
+ // EditType is mutable, so we need to create a new instance every time.
+ if ("home".equals(type)) {
+ return buildEmailType(Email.TYPE_HOME);
+ }
+ if ("work".equals(type)) {
+ return buildEmailType(Email.TYPE_WORK);
+ }
+ if ("other".equals(type)) {
+ return buildEmailType(Email.TYPE_OTHER);
+ }
+ if ("mobile".equals(type)) {
+ return buildEmailType(Email.TYPE_MOBILE);
+ }
+ if ("custom".equals(type)) {
+ return buildEmailType(Email.TYPE_CUSTOM).setSecondary(true).setCustomColumn(Email.LABEL);
+ }
+ return null;
+ }
+ }
+
+ private static class StructuredPostalKindBuilder extends KindBuilder {
+
+ @Override
+ public String getTagName() {
+ return "postal";
+ }
+
+ @Override
+ public List<DataKind> parseDataKind(Context context, XmlPullParser parser, AttributeSet attrs)
+ throws DefinitionException, XmlPullParserException, IOException {
+ final DataKind kind =
+ newDataKind(
+ context,
+ parser,
+ attrs,
+ false,
+ StructuredPostal.CONTENT_ITEM_TYPE,
+ StructuredPostal.TYPE,
+ R.string.postalLabelsGroup,
+ Weight.STRUCTURED_POSTAL,
+ new PostalActionInflater(),
+ new SimpleInflater(StructuredPostal.FORMATTED_ADDRESS));
+
+ if (getAttr(attrs, "needsStructured", false)) {
+ if (Locale.JAPANESE.getLanguage().equals(Locale.getDefault().getLanguage())) {
+ // Japanese order
+ kind.fieldList.add(
+ new EditField(StructuredPostal.COUNTRY, R.string.postal_country, FLAGS_POSTAL)
+ .setOptional(true));
+ kind.fieldList.add(
+ new EditField(StructuredPostal.POSTCODE, R.string.postal_postcode, FLAGS_POSTAL));
+ kind.fieldList.add(
+ new EditField(StructuredPostal.REGION, R.string.postal_region, FLAGS_POSTAL));
+ kind.fieldList.add(
+ new EditField(StructuredPostal.CITY, R.string.postal_city, FLAGS_POSTAL));
+ kind.fieldList.add(
+ new EditField(StructuredPostal.STREET, R.string.postal_street, FLAGS_POSTAL));
+ } else {
+ // Generic order
+ kind.fieldList.add(
+ new EditField(StructuredPostal.STREET, R.string.postal_street, FLAGS_POSTAL));
+ kind.fieldList.add(
+ new EditField(StructuredPostal.CITY, R.string.postal_city, FLAGS_POSTAL));
+ kind.fieldList.add(
+ new EditField(StructuredPostal.REGION, R.string.postal_region, FLAGS_POSTAL));
+ kind.fieldList.add(
+ new EditField(StructuredPostal.POSTCODE, R.string.postal_postcode, FLAGS_POSTAL));
+ kind.fieldList.add(
+ new EditField(StructuredPostal.COUNTRY, R.string.postal_country, FLAGS_POSTAL)
+ .setOptional(true));
+ }
+ } else {
+ kind.maxLinesForDisplay = MAX_LINES_FOR_POSTAL_ADDRESS;
+ kind.fieldList.add(
+ new EditField(
+ StructuredPostal.FORMATTED_ADDRESS, R.string.postal_address, FLAGS_POSTAL));
+ }
+
+ List<DataKind> result = new ArrayList<>();
+ result.add(kind);
+ return result;
+ }
+
+ @Override
+ protected EditType buildEditTypeForTypeTag(AttributeSet attrs, String type) {
+ // EditType is mutable, so we need to create a new instance every time.
+ if ("home".equals(type)) {
+ return buildPostalType(StructuredPostal.TYPE_HOME);
+ }
+ if ("work".equals(type)) {
+ return buildPostalType(StructuredPostal.TYPE_WORK);
+ }
+ if ("other".equals(type)) {
+ return buildPostalType(StructuredPostal.TYPE_OTHER);
+ }
+ if ("custom".equals(type)) {
+ return buildPostalType(StructuredPostal.TYPE_CUSTOM)
+ .setSecondary(true)
+ .setCustomColumn(Email.LABEL);
+ }
+ return null;
+ }
+ }
+
+ private static class ImKindBuilder extends KindBuilder {
+
+ @Override
+ public String getTagName() {
+ return "im";
+ }
+
+ @Override
+ public List<DataKind> parseDataKind(Context context, XmlPullParser parser, AttributeSet attrs)
+ throws DefinitionException, XmlPullParserException, IOException {
+
+ // IM is special:
+ // - It uses "protocol" as the custom label field
+ // - Its TYPE is fixed to TYPE_OTHER
+
+ final DataKind kind =
+ newDataKind(
+ context,
+ parser,
+ attrs,
+ false,
+ Im.CONTENT_ITEM_TYPE,
+ Im.PROTOCOL,
+ R.string.imLabelsGroup,
+ Weight.IM,
+ new ImActionInflater(),
+ new SimpleInflater(Im.DATA) // header / action
+ );
+ kind.fieldList.add(new EditField(Im.DATA, R.string.imLabelsGroup, FLAGS_EMAIL));
+
+ kind.defaultValues = new ContentValues();
+ kind.defaultValues.put(Im.TYPE, Im.TYPE_OTHER);
+
+ List<DataKind> result = new ArrayList<>();
+ result.add(kind);
+ return result;
+ }
+
+ @Override
+ protected EditType buildEditTypeForTypeTag(AttributeSet attrs, String type) {
+ if ("aim".equals(type)) {
+ return buildImType(Im.PROTOCOL_AIM);
+ }
+ if ("msn".equals(type)) {
+ return buildImType(Im.PROTOCOL_MSN);
+ }
+ if ("yahoo".equals(type)) {
+ return buildImType(Im.PROTOCOL_YAHOO);
+ }
+ if ("skype".equals(type)) {
+ return buildImType(Im.PROTOCOL_SKYPE);
+ }
+ if ("qq".equals(type)) {
+ return buildImType(Im.PROTOCOL_QQ);
+ }
+ if ("google_talk".equals(type)) {
+ return buildImType(Im.PROTOCOL_GOOGLE_TALK);
+ }
+ if ("icq".equals(type)) {
+ return buildImType(Im.PROTOCOL_ICQ);
+ }
+ if ("jabber".equals(type)) {
+ return buildImType(Im.PROTOCOL_JABBER);
+ }
+ if ("custom".equals(type)) {
+ return buildImType(Im.PROTOCOL_CUSTOM)
+ .setSecondary(true)
+ .setCustomColumn(Im.CUSTOM_PROTOCOL);
+ }
+ return null;
+ }
+ }
+
+ private static class OrganizationKindBuilder extends KindBuilder {
+
+ @Override
+ public String getTagName() {
+ return "organization";
+ }
+
+ @Override
+ public List<DataKind> parseDataKind(Context context, XmlPullParser parser, AttributeSet attrs)
+ throws DefinitionException, XmlPullParserException, IOException {
+ final DataKind kind =
+ newDataKind(
+ context,
+ parser,
+ attrs,
+ false,
+ Organization.CONTENT_ITEM_TYPE,
+ null,
+ R.string.organizationLabelsGroup,
+ Weight.ORGANIZATION,
+ new SimpleInflater(R.string.organizationLabelsGroup),
+ ORGANIZATION_BODY_INFLATER);
+
+ kind.fieldList.add(
+ new EditField(Organization.COMPANY, R.string.ghostData_company, FLAGS_GENERIC_NAME));
+ kind.fieldList.add(
+ new EditField(Organization.TITLE, R.string.ghostData_title, FLAGS_GENERIC_NAME));
+
+ throwIfList(kind);
+
+ List<DataKind> result = new ArrayList<>();
+ result.add(kind);
+ return result;
+ }
+ }
+
+ private static class PhotoKindBuilder extends KindBuilder {
+
+ @Override
+ public String getTagName() {
+ return "photo";
+ }
+
+ @Override
+ public List<DataKind> parseDataKind(Context context, XmlPullParser parser, AttributeSet attrs)
+ throws DefinitionException, XmlPullParserException, IOException {
+ final DataKind kind =
+ newDataKind(
+ context,
+ parser,
+ attrs,
+ false,
+ Photo.CONTENT_ITEM_TYPE,
+ null /* no type */,
+ Weight.NONE,
+ -1,
+ null,
+ null // no header, no body
+ );
+
+ kind.fieldList.add(new EditField(Photo.PHOTO, -1, -1));
+
+ throwIfList(kind);
+
+ List<DataKind> result = new ArrayList<>();
+ result.add(kind);
+ return result;
+ }
+ }
+
+ private static class NoteKindBuilder extends KindBuilder {
+
+ @Override
+ public String getTagName() {
+ return "note";
+ }
+
+ @Override
+ public List<DataKind> parseDataKind(Context context, XmlPullParser parser, AttributeSet attrs)
+ throws DefinitionException, XmlPullParserException, IOException {
+ final DataKind kind =
+ newDataKind(
+ context,
+ parser,
+ attrs,
+ false,
+ Note.CONTENT_ITEM_TYPE,
+ null,
+ R.string.label_notes,
+ Weight.NOTE,
+ new SimpleInflater(R.string.label_notes),
+ new SimpleInflater(Note.NOTE));
+
+ kind.fieldList.add(new EditField(Note.NOTE, R.string.label_notes, FLAGS_NOTE));
+ kind.maxLinesForDisplay = MAX_LINES_FOR_NOTE;
+
+ throwIfList(kind);
+
+ List<DataKind> result = new ArrayList<>();
+ result.add(kind);
+ return result;
+ }
+ }
+
+ private static class WebsiteKindBuilder extends KindBuilder {
+
+ @Override
+ public String getTagName() {
+ return "website";
+ }
+
+ @Override
+ public List<DataKind> parseDataKind(Context context, XmlPullParser parser, AttributeSet attrs)
+ throws DefinitionException, XmlPullParserException, IOException {
+ final DataKind kind =
+ newDataKind(
+ context,
+ parser,
+ attrs,
+ false,
+ Website.CONTENT_ITEM_TYPE,
+ null,
+ R.string.websiteLabelsGroup,
+ Weight.WEBSITE,
+ new SimpleInflater(R.string.websiteLabelsGroup),
+ new SimpleInflater(Website.URL));
+
+ kind.fieldList.add(new EditField(Website.URL, R.string.websiteLabelsGroup, FLAGS_WEBSITE));
+
+ kind.defaultValues = new ContentValues();
+ kind.defaultValues.put(Website.TYPE, Website.TYPE_OTHER);
+
+ List<DataKind> result = new ArrayList<>();
+ result.add(kind);
+ return result;
+ }
+ }
+
+ private static class SipAddressKindBuilder extends KindBuilder {
+
+ @Override
+ public String getTagName() {
+ return "sip_address";
+ }
+
+ @Override
+ public List<DataKind> parseDataKind(Context context, XmlPullParser parser, AttributeSet attrs)
+ throws DefinitionException, XmlPullParserException, IOException {
+ final DataKind kind =
+ newDataKind(
+ context,
+ parser,
+ attrs,
+ false,
+ SipAddress.CONTENT_ITEM_TYPE,
+ null,
+ R.string.label_sip_address,
+ Weight.SIP_ADDRESS,
+ new SimpleInflater(R.string.label_sip_address),
+ new SimpleInflater(SipAddress.SIP_ADDRESS));
+
+ kind.fieldList.add(
+ new EditField(SipAddress.SIP_ADDRESS, R.string.label_sip_address, FLAGS_SIP_ADDRESS));
+
+ throwIfList(kind);
+
+ List<DataKind> result = new ArrayList<>();
+ result.add(kind);
+ return result;
+ }
+ }
+
+ private static class GroupMembershipKindBuilder extends KindBuilder {
+
+ @Override
+ public String getTagName() {
+ return "group_membership";
+ }
+
+ @Override
+ public List<DataKind> parseDataKind(Context context, XmlPullParser parser, AttributeSet attrs)
+ throws DefinitionException, XmlPullParserException, IOException {
+ final DataKind kind =
+ newDataKind(
+ context,
+ parser,
+ attrs,
+ false,
+ GroupMembership.CONTENT_ITEM_TYPE,
+ null,
+ R.string.groupsLabel,
+ Weight.GROUP_MEMBERSHIP,
+ null,
+ null);
+
+ kind.fieldList.add(new EditField(GroupMembership.GROUP_ROW_ID, -1, -1));
+ kind.maxLinesForDisplay = MAX_LINES_FOR_GROUP;
+
+ throwIfList(kind);
+
+ List<DataKind> result = new ArrayList<>();
+ result.add(kind);
+ return result;
+ }
+ }
+
+ /**
+ * Event DataKind parser.
+ *
+ * <p>Event DataKind is used only for Google/Exchange types, so this parser is not used for now.
+ */
+ private static class EventKindBuilder extends KindBuilder {
+
+ @Override
+ public String getTagName() {
+ return "event";
+ }
+
+ @Override
+ public List<DataKind> parseDataKind(Context context, XmlPullParser parser, AttributeSet attrs)
+ throws DefinitionException, XmlPullParserException, IOException {
+ final DataKind kind =
+ newDataKind(
+ context,
+ parser,
+ attrs,
+ false,
+ Event.CONTENT_ITEM_TYPE,
+ Event.TYPE,
+ R.string.eventLabelsGroup,
+ Weight.EVENT,
+ new EventActionInflater(),
+ new SimpleInflater(Event.START_DATE));
+
+ kind.fieldList.add(new EditField(Event.DATA, R.string.eventLabelsGroup, FLAGS_EVENT));
+
+ if (getAttr(attrs, Attr.DATE_WITH_TIME, false)) {
+ kind.dateFormatWithoutYear = CommonDateUtils.NO_YEAR_DATE_AND_TIME_FORMAT;
+ kind.dateFormatWithYear = CommonDateUtils.DATE_AND_TIME_FORMAT;
+ } else {
+ kind.dateFormatWithoutYear = CommonDateUtils.NO_YEAR_DATE_FORMAT;
+ kind.dateFormatWithYear = CommonDateUtils.FULL_DATE_FORMAT;
+ }
+
+ List<DataKind> result = new ArrayList<>();
+ result.add(kind);
+ return result;
+ }
+
+ @Override
+ protected EditType buildEditTypeForTypeTag(AttributeSet attrs, String type) {
+ final boolean yo = getAttr(attrs, Attr.YEAR_OPTIONAL, false);
+
+ if ("birthday".equals(type)) {
+ return buildEventType(Event.TYPE_BIRTHDAY, yo).setSpecificMax(1);
+ }
+ if ("anniversary".equals(type)) {
+ return buildEventType(Event.TYPE_ANNIVERSARY, yo);
+ }
+ if ("other".equals(type)) {
+ return buildEventType(Event.TYPE_OTHER, yo);
+ }
+ if ("custom".equals(type)) {
+ return buildEventType(Event.TYPE_CUSTOM, yo)
+ .setSecondary(true)
+ .setCustomColumn(Event.LABEL);
+ }
+ return null;
+ }
+ }
+
+ /**
+ * Relationship DataKind parser.
+ *
+ * <p>Relationship DataKind is used only for Google/Exchange types, so this parser is not used for
+ * now.
+ */
+ private static class RelationshipKindBuilder extends KindBuilder {
+
+ @Override
+ public String getTagName() {
+ return "relationship";
+ }
+
+ @Override
+ public List<DataKind> parseDataKind(Context context, XmlPullParser parser, AttributeSet attrs)
+ throws DefinitionException, XmlPullParserException, IOException {
+ final DataKind kind =
+ newDataKind(
+ context,
+ parser,
+ attrs,
+ false,
+ Relation.CONTENT_ITEM_TYPE,
+ Relation.TYPE,
+ R.string.relationLabelsGroup,
+ Weight.RELATIONSHIP,
+ new RelationActionInflater(),
+ new SimpleInflater(Relation.NAME));
+
+ kind.fieldList.add(
+ new EditField(Relation.DATA, R.string.relationLabelsGroup, FLAGS_RELATION));
+
+ kind.defaultValues = new ContentValues();
+ kind.defaultValues.put(Relation.TYPE, Relation.TYPE_SPOUSE);
+
+ List<DataKind> result = new ArrayList<>();
+ result.add(kind);
+ return result;
+ }
+
+ @Override
+ protected EditType buildEditTypeForTypeTag(AttributeSet attrs, String type) {
+ // EditType is mutable, so we need to create a new instance every time.
+ if ("assistant".equals(type)) {
+ return buildRelationType(Relation.TYPE_ASSISTANT);
+ }
+ if ("brother".equals(type)) {
+ return buildRelationType(Relation.TYPE_BROTHER);
+ }
+ if ("child".equals(type)) {
+ return buildRelationType(Relation.TYPE_CHILD);
+ }
+ if ("domestic_partner".equals(type)) {
+ return buildRelationType(Relation.TYPE_DOMESTIC_PARTNER);
+ }
+ if ("father".equals(type)) {
+ return buildRelationType(Relation.TYPE_FATHER);
+ }
+ if ("friend".equals(type)) {
+ return buildRelationType(Relation.TYPE_FRIEND);
+ }
+ if ("manager".equals(type)) {
+ return buildRelationType(Relation.TYPE_MANAGER);
+ }
+ if ("mother".equals(type)) {
+ return buildRelationType(Relation.TYPE_MOTHER);
+ }
+ if ("parent".equals(type)) {
+ return buildRelationType(Relation.TYPE_PARENT);
+ }
+ if ("partner".equals(type)) {
+ return buildRelationType(Relation.TYPE_PARTNER);
+ }
+ if ("referred_by".equals(type)) {
+ return buildRelationType(Relation.TYPE_REFERRED_BY);
+ }
+ if ("relative".equals(type)) {
+ return buildRelationType(Relation.TYPE_RELATIVE);
+ }
+ if ("sister".equals(type)) {
+ return buildRelationType(Relation.TYPE_SISTER);
+ }
+ if ("spouse".equals(type)) {
+ return buildRelationType(Relation.TYPE_SPOUSE);
+ }
+ if ("custom".equals(type)) {
+ return buildRelationType(Relation.TYPE_CUSTOM)
+ .setSecondary(true)
+ .setCustomColumn(Relation.LABEL);
+ }
+ return null;
+ }
+ }
+}
diff --git a/java/com/android/contacts/common/model/account/ExchangeAccountType.java b/java/com/android/contacts/common/model/account/ExchangeAccountType.java
new file mode 100644
index 000000000..a27028e80
--- /dev/null
+++ b/java/com/android/contacts/common/model/account/ExchangeAccountType.java
@@ -0,0 +1,365 @@
+/*
+ * Copyright (C) 2009 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.common.model.account;
+
+import android.content.ContentValues;
+import android.content.Context;
+import android.provider.ContactsContract.CommonDataKinds.Email;
+import android.provider.ContactsContract.CommonDataKinds.Event;
+import android.provider.ContactsContract.CommonDataKinds.Im;
+import android.provider.ContactsContract.CommonDataKinds.Nickname;
+import android.provider.ContactsContract.CommonDataKinds.Note;
+import android.provider.ContactsContract.CommonDataKinds.Organization;
+import android.provider.ContactsContract.CommonDataKinds.Phone;
+import android.provider.ContactsContract.CommonDataKinds.Photo;
+import android.provider.ContactsContract.CommonDataKinds.StructuredName;
+import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
+import android.provider.ContactsContract.CommonDataKinds.Website;
+import android.util.Log;
+import com.android.contacts.common.R;
+import com.android.contacts.common.model.dataitem.DataKind;
+import com.android.contacts.common.util.CommonDateUtils;
+import java.util.ArrayList;
+import java.util.Locale;
+
+public class ExchangeAccountType extends BaseAccountType {
+
+ private static final String TAG = "ExchangeAccountType";
+
+ private static final String ACCOUNT_TYPE_AOSP = "com.android.exchange";
+ private static final String ACCOUNT_TYPE_GOOGLE_1 = "com.google.android.exchange";
+ private static final String ACCOUNT_TYPE_GOOGLE_2 = "com.google.android.gm.exchange";
+
+ public ExchangeAccountType(Context context, String authenticatorPackageName, String type) {
+ this.accountType = type;
+ this.resourcePackageName = null;
+ this.syncAdapterPackageName = authenticatorPackageName;
+
+ try {
+ addDataKindStructuredName(context);
+ addDataKindDisplayName(context);
+ addDataKindPhoneticName(context);
+ addDataKindNickname(context);
+ addDataKindPhone(context);
+ addDataKindEmail(context);
+ addDataKindStructuredPostal(context);
+ addDataKindIm(context);
+ addDataKindOrganization(context);
+ addDataKindPhoto(context);
+ addDataKindNote(context);
+ addDataKindEvent(context);
+ addDataKindWebsite(context);
+ addDataKindGroupMembership(context);
+
+ mIsInitialized = true;
+ } catch (DefinitionException e) {
+ Log.e(TAG, "Problem building account type", e);
+ }
+ }
+
+ public static boolean isExchangeType(String type) {
+ return ACCOUNT_TYPE_AOSP.equals(type)
+ || ACCOUNT_TYPE_GOOGLE_1.equals(type)
+ || ACCOUNT_TYPE_GOOGLE_2.equals(type);
+ }
+
+ @Override
+ protected DataKind addDataKindStructuredName(Context context) throws DefinitionException {
+ DataKind kind =
+ addKind(
+ new DataKind(
+ StructuredName.CONTENT_ITEM_TYPE, R.string.nameLabelsGroup, Weight.NONE, true));
+ kind.actionHeader = new SimpleInflater(R.string.nameLabelsGroup);
+ kind.actionBody = new SimpleInflater(Nickname.NAME);
+
+ kind.typeOverallMax = 1;
+
+ kind.fieldList = new ArrayList<>();
+ kind.fieldList.add(
+ new EditField(StructuredName.PREFIX, R.string.name_prefix, FLAGS_PERSON_NAME)
+ .setOptional(true));
+ kind.fieldList.add(
+ new EditField(StructuredName.FAMILY_NAME, R.string.name_family, FLAGS_PERSON_NAME));
+ kind.fieldList.add(
+ new EditField(StructuredName.MIDDLE_NAME, R.string.name_middle, FLAGS_PERSON_NAME));
+ kind.fieldList.add(
+ new EditField(StructuredName.GIVEN_NAME, R.string.name_given, FLAGS_PERSON_NAME));
+ kind.fieldList.add(
+ new EditField(StructuredName.SUFFIX, R.string.name_suffix, FLAGS_PERSON_NAME));
+
+ kind.fieldList.add(
+ new EditField(
+ StructuredName.PHONETIC_FAMILY_NAME, R.string.name_phonetic_family, FLAGS_PHONETIC));
+ kind.fieldList.add(
+ new EditField(
+ StructuredName.PHONETIC_GIVEN_NAME, R.string.name_phonetic_given, FLAGS_PHONETIC));
+
+ return kind;
+ }
+
+ @Override
+ protected DataKind addDataKindDisplayName(Context context) throws DefinitionException {
+ DataKind kind =
+ addKind(
+ new DataKind(
+ DataKind.PSEUDO_MIME_TYPE_DISPLAY_NAME,
+ R.string.nameLabelsGroup,
+ Weight.NONE,
+ true));
+
+ boolean displayOrderPrimary =
+ context.getResources().getBoolean(R.bool.config_editor_field_order_primary);
+ kind.typeOverallMax = 1;
+
+ kind.fieldList = new ArrayList<>();
+ kind.fieldList.add(
+ new EditField(StructuredName.PREFIX, R.string.name_prefix, FLAGS_PERSON_NAME)
+ .setOptional(true));
+ if (!displayOrderPrimary) {
+ kind.fieldList.add(
+ new EditField(StructuredName.FAMILY_NAME, R.string.name_family, FLAGS_PERSON_NAME));
+ kind.fieldList.add(
+ new EditField(StructuredName.MIDDLE_NAME, R.string.name_middle, FLAGS_PERSON_NAME)
+ .setOptional(true));
+ kind.fieldList.add(
+ new EditField(StructuredName.GIVEN_NAME, R.string.name_given, FLAGS_PERSON_NAME));
+ } else {
+ kind.fieldList.add(
+ new EditField(StructuredName.GIVEN_NAME, R.string.name_given, FLAGS_PERSON_NAME));
+ kind.fieldList.add(
+ new EditField(StructuredName.MIDDLE_NAME, R.string.name_middle, FLAGS_PERSON_NAME)
+ .setOptional(true));
+ kind.fieldList.add(
+ new EditField(StructuredName.FAMILY_NAME, R.string.name_family, FLAGS_PERSON_NAME));
+ }
+ kind.fieldList.add(
+ new EditField(StructuredName.SUFFIX, R.string.name_suffix, FLAGS_PERSON_NAME)
+ .setOptional(true));
+
+ return kind;
+ }
+
+ @Override
+ protected DataKind addDataKindPhoneticName(Context context) throws DefinitionException {
+ DataKind kind =
+ addKind(
+ new DataKind(
+ DataKind.PSEUDO_MIME_TYPE_PHONETIC_NAME,
+ R.string.name_phonetic,
+ Weight.NONE,
+ true));
+ kind.actionHeader = new SimpleInflater(R.string.nameLabelsGroup);
+ kind.actionBody = new SimpleInflater(Nickname.NAME);
+
+ kind.typeOverallMax = 1;
+
+ kind.fieldList = new ArrayList<>();
+ kind.fieldList.add(
+ new EditField(
+ StructuredName.PHONETIC_FAMILY_NAME, R.string.name_phonetic_family, FLAGS_PHONETIC));
+ kind.fieldList.add(
+ new EditField(
+ StructuredName.PHONETIC_GIVEN_NAME, R.string.name_phonetic_given, FLAGS_PHONETIC));
+
+ return kind;
+ }
+
+ @Override
+ protected DataKind addDataKindNickname(Context context) throws DefinitionException {
+ final DataKind kind = super.addDataKindNickname(context);
+
+ kind.typeOverallMax = 1;
+
+ kind.fieldList = new ArrayList<>();
+ kind.fieldList.add(
+ new EditField(Nickname.NAME, R.string.nicknameLabelsGroup, FLAGS_PERSON_NAME));
+
+ return kind;
+ }
+
+ @Override
+ protected DataKind addDataKindPhone(Context context) throws DefinitionException {
+ final DataKind kind = super.addDataKindPhone(context);
+
+ kind.typeColumn = Phone.TYPE;
+ kind.typeList = new ArrayList<>();
+ kind.typeList.add(buildPhoneType(Phone.TYPE_MOBILE).setSpecificMax(1));
+ kind.typeList.add(buildPhoneType(Phone.TYPE_HOME).setSpecificMax(2));
+ kind.typeList.add(buildPhoneType(Phone.TYPE_WORK).setSpecificMax(2));
+ kind.typeList.add(buildPhoneType(Phone.TYPE_FAX_WORK).setSecondary(true).setSpecificMax(1));
+ kind.typeList.add(buildPhoneType(Phone.TYPE_FAX_HOME).setSecondary(true).setSpecificMax(1));
+ kind.typeList.add(buildPhoneType(Phone.TYPE_PAGER).setSecondary(true).setSpecificMax(1));
+ kind.typeList.add(buildPhoneType(Phone.TYPE_CAR).setSecondary(true).setSpecificMax(1));
+ kind.typeList.add(buildPhoneType(Phone.TYPE_COMPANY_MAIN).setSecondary(true).setSpecificMax(1));
+ kind.typeList.add(buildPhoneType(Phone.TYPE_MMS).setSecondary(true).setSpecificMax(1));
+ kind.typeList.add(buildPhoneType(Phone.TYPE_RADIO).setSecondary(true).setSpecificMax(1));
+ kind.typeList.add(buildPhoneType(Phone.TYPE_ASSISTANT).setSecondary(true).setSpecificMax(1));
+
+ kind.fieldList = new ArrayList<>();
+ kind.fieldList.add(new EditField(Phone.NUMBER, R.string.phoneLabelsGroup, FLAGS_PHONE));
+
+ return kind;
+ }
+
+ @Override
+ protected DataKind addDataKindEmail(Context context) throws DefinitionException {
+ final DataKind kind = super.addDataKindEmail(context);
+
+ kind.typeOverallMax = 3;
+
+ kind.fieldList = new ArrayList<>();
+ kind.fieldList.add(new EditField(Email.DATA, R.string.emailLabelsGroup, FLAGS_EMAIL));
+
+ return kind;
+ }
+
+ @Override
+ protected DataKind addDataKindStructuredPostal(Context context) throws DefinitionException {
+ final DataKind kind = super.addDataKindStructuredPostal(context);
+
+ final boolean useJapaneseOrder =
+ Locale.JAPANESE.getLanguage().equals(Locale.getDefault().getLanguage());
+ kind.typeColumn = StructuredPostal.TYPE;
+ kind.typeList = new ArrayList<>();
+ kind.typeList.add(buildPostalType(StructuredPostal.TYPE_WORK).setSpecificMax(1));
+ kind.typeList.add(buildPostalType(StructuredPostal.TYPE_HOME).setSpecificMax(1));
+ kind.typeList.add(buildPostalType(StructuredPostal.TYPE_OTHER).setSpecificMax(1));
+
+ kind.fieldList = new ArrayList<>();
+ if (useJapaneseOrder) {
+ kind.fieldList.add(
+ new EditField(StructuredPostal.COUNTRY, R.string.postal_country, FLAGS_POSTAL)
+ .setOptional(true));
+ kind.fieldList.add(
+ new EditField(StructuredPostal.POSTCODE, R.string.postal_postcode, FLAGS_POSTAL));
+ kind.fieldList.add(
+ new EditField(StructuredPostal.REGION, R.string.postal_region, FLAGS_POSTAL));
+ kind.fieldList.add(new EditField(StructuredPostal.CITY, R.string.postal_city, FLAGS_POSTAL));
+ kind.fieldList.add(
+ new EditField(StructuredPostal.STREET, R.string.postal_street, FLAGS_POSTAL));
+ } else {
+ kind.fieldList.add(
+ new EditField(StructuredPostal.STREET, R.string.postal_street, FLAGS_POSTAL));
+ kind.fieldList.add(new EditField(StructuredPostal.CITY, R.string.postal_city, FLAGS_POSTAL));
+ kind.fieldList.add(
+ new EditField(StructuredPostal.REGION, R.string.postal_region, FLAGS_POSTAL));
+ kind.fieldList.add(
+ new EditField(StructuredPostal.POSTCODE, R.string.postal_postcode, FLAGS_POSTAL));
+ kind.fieldList.add(
+ new EditField(StructuredPostal.COUNTRY, R.string.postal_country, FLAGS_POSTAL)
+ .setOptional(true));
+ }
+
+ return kind;
+ }
+
+ @Override
+ protected DataKind addDataKindIm(Context context) throws DefinitionException {
+ final DataKind kind = super.addDataKindIm(context);
+
+ // Types are not supported for IM. There can be 3 IMs, but OWA only shows only the first
+ kind.typeOverallMax = 3;
+
+ kind.defaultValues = new ContentValues();
+ kind.defaultValues.put(Im.TYPE, Im.TYPE_OTHER);
+
+ kind.fieldList = new ArrayList<>();
+ kind.fieldList.add(new EditField(Im.DATA, R.string.imLabelsGroup, FLAGS_EMAIL));
+
+ return kind;
+ }
+
+ @Override
+ protected DataKind addDataKindOrganization(Context context) throws DefinitionException {
+ final DataKind kind = super.addDataKindOrganization(context);
+
+ kind.typeOverallMax = 1;
+
+ kind.fieldList = new ArrayList<>();
+ kind.fieldList.add(
+ new EditField(Organization.COMPANY, R.string.ghostData_company, FLAGS_GENERIC_NAME));
+ kind.fieldList.add(
+ new EditField(Organization.TITLE, R.string.ghostData_title, FLAGS_GENERIC_NAME));
+
+ return kind;
+ }
+
+ @Override
+ protected DataKind addDataKindPhoto(Context context) throws DefinitionException {
+ final DataKind kind = super.addDataKindPhoto(context);
+
+ kind.typeOverallMax = 1;
+
+ kind.fieldList = new ArrayList<>();
+ kind.fieldList.add(new EditField(Photo.PHOTO, -1, -1));
+
+ return kind;
+ }
+
+ @Override
+ protected DataKind addDataKindNote(Context context) throws DefinitionException {
+ final DataKind kind = super.addDataKindNote(context);
+
+ kind.fieldList = new ArrayList<>();
+ kind.fieldList.add(new EditField(Note.NOTE, R.string.label_notes, FLAGS_NOTE));
+
+ return kind;
+ }
+
+ protected DataKind addDataKindEvent(Context context) throws DefinitionException {
+ DataKind kind =
+ addKind(
+ new DataKind(Event.CONTENT_ITEM_TYPE, R.string.eventLabelsGroup, Weight.EVENT, true));
+ kind.actionHeader = new EventActionInflater();
+ kind.actionBody = new SimpleInflater(Event.START_DATE);
+
+ kind.typeOverallMax = 1;
+
+ kind.typeColumn = Event.TYPE;
+ kind.typeList = new ArrayList<>();
+ kind.typeList.add(buildEventType(Event.TYPE_BIRTHDAY, false).setSpecificMax(1));
+
+ kind.dateFormatWithYear = CommonDateUtils.DATE_AND_TIME_FORMAT;
+
+ kind.fieldList = new ArrayList<>();
+ kind.fieldList.add(new EditField(Event.DATA, R.string.eventLabelsGroup, FLAGS_EVENT));
+
+ return kind;
+ }
+
+ @Override
+ protected DataKind addDataKindWebsite(Context context) throws DefinitionException {
+ final DataKind kind = super.addDataKindWebsite(context);
+
+ kind.typeOverallMax = 1;
+
+ kind.fieldList = new ArrayList<>();
+ kind.fieldList.add(new EditField(Website.URL, R.string.websiteLabelsGroup, FLAGS_WEBSITE));
+
+ return kind;
+ }
+
+ @Override
+ public boolean isGroupMembershipEditable() {
+ return true;
+ }
+
+ @Override
+ public boolean areContactsWritable() {
+ return true;
+ }
+}
diff --git a/java/com/android/contacts/common/model/account/ExternalAccountType.java b/java/com/android/contacts/common/model/account/ExternalAccountType.java
new file mode 100644
index 000000000..aca1f70d2
--- /dev/null
+++ b/java/com/android/contacts/common/model/account/ExternalAccountType.java
@@ -0,0 +1,443 @@
+/*
+ * Copyright (C) 2009 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.common.model.account;
+
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.content.pm.ResolveInfo;
+import android.content.pm.ServiceInfo;
+import android.content.res.Resources;
+import android.content.res.TypedArray;
+import android.content.res.XmlResourceParser;
+import android.provider.ContactsContract.CommonDataKinds.Photo;
+import android.provider.ContactsContract.CommonDataKinds.StructuredName;
+import android.support.annotation.VisibleForTesting;
+import android.text.TextUtils;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.util.Xml;
+import com.android.contacts.common.R;
+import com.android.contacts.common.model.dataitem.DataKind;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
+/** A general contacts account type descriptor. */
+public class ExternalAccountType extends BaseAccountType {
+
+ private static final String TAG = "ExternalAccountType";
+
+ private static final String SYNC_META_DATA = "android.content.SyncAdapter";
+
+ /**
+ * The metadata name for so-called "contacts.xml".
+ *
+ * <p>On LMP and later, we also accept the "alternate" name. This is to allow sync adapters to
+ * have a contacts.xml without making it visible on older platforms. If you modify this also
+ * update the corresponding list in ContactsProvider/PhotoPriorityResolver
+ */
+ private static final String[] METADATA_CONTACTS_NAMES =
+ new String[] {
+ "android.provider.ALTERNATE_CONTACTS_STRUCTURE", "android.provider.CONTACTS_STRUCTURE"
+ };
+
+ private static final String TAG_CONTACTS_SOURCE_LEGACY = "ContactsSource";
+ private static final String TAG_CONTACTS_ACCOUNT_TYPE = "ContactsAccountType";
+ private static final String TAG_CONTACTS_DATA_KIND = "ContactsDataKind";
+ private static final String TAG_EDIT_SCHEMA = "EditSchema";
+
+ private static final String ATTR_EDIT_CONTACT_ACTIVITY = "editContactActivity";
+ private static final String ATTR_CREATE_CONTACT_ACTIVITY = "createContactActivity";
+ private static final String ATTR_INVITE_CONTACT_ACTIVITY = "inviteContactActivity";
+ private static final String ATTR_INVITE_CONTACT_ACTION_LABEL = "inviteContactActionLabel";
+ private static final String ATTR_VIEW_CONTACT_NOTIFY_SERVICE = "viewContactNotifyService";
+ private static final String ATTR_VIEW_GROUP_ACTIVITY = "viewGroupActivity";
+ private static final String ATTR_VIEW_GROUP_ACTION_LABEL = "viewGroupActionLabel";
+ private static final String ATTR_DATA_SET = "dataSet";
+ private static final String ATTR_EXTENSION_PACKAGE_NAMES = "extensionPackageNames";
+
+ // The following attributes should only be set in non-sync-adapter account types. They allow
+ // for the account type and resource IDs to be specified without an associated authenticator.
+ private static final String ATTR_ACCOUNT_TYPE = "accountType";
+ private static final String ATTR_ACCOUNT_LABEL = "accountTypeLabel";
+ private static final String ATTR_ACCOUNT_ICON = "accountTypeIcon";
+
+ private final boolean mIsExtension;
+
+ private String mEditContactActivityClassName;
+ private String mCreateContactActivityClassName;
+ private String mInviteContactActivity;
+ private String mInviteActionLabelAttribute;
+ private int mInviteActionLabelResId;
+ private String mViewContactNotifyService;
+ private String mViewGroupActivity;
+ private String mViewGroupLabelAttribute;
+ private int mViewGroupLabelResId;
+ private List<String> mExtensionPackageNames;
+ private String mAccountTypeLabelAttribute;
+ private String mAccountTypeIconAttribute;
+ private boolean mHasContactsMetadata;
+ private boolean mHasEditSchema;
+
+ public ExternalAccountType(Context context, String resPackageName, boolean isExtension) {
+ this(context, resPackageName, isExtension, null);
+ }
+
+ /**
+ * Constructor used for testing to initialize with any arbitrary XML.
+ *
+ * @param injectedMetadata If non-null, it'll be used to initialize the type. Only set by tests.
+ * If null, the metadata is loaded from the specified package.
+ */
+ ExternalAccountType(
+ Context context,
+ String packageName,
+ boolean isExtension,
+ XmlResourceParser injectedMetadata) {
+ this.mIsExtension = isExtension;
+ this.resourcePackageName = packageName;
+ this.syncAdapterPackageName = packageName;
+
+ final XmlResourceParser parser;
+ if (injectedMetadata == null) {
+ parser = loadContactsXml(context, packageName);
+ } else {
+ parser = injectedMetadata;
+ }
+ boolean needLineNumberInErrorLog = true;
+ try {
+ if (parser != null) {
+ inflate(context, parser);
+ }
+
+ // Done parsing; line number no longer needed in error log.
+ needLineNumberInErrorLog = false;
+ if (mHasEditSchema) {
+ checkKindExists(StructuredName.CONTENT_ITEM_TYPE);
+ checkKindExists(DataKind.PSEUDO_MIME_TYPE_DISPLAY_NAME);
+ checkKindExists(DataKind.PSEUDO_MIME_TYPE_PHONETIC_NAME);
+ checkKindExists(Photo.CONTENT_ITEM_TYPE);
+ } else {
+ // Bring in name and photo from fallback source, which are non-optional
+ addDataKindStructuredName(context);
+ addDataKindDisplayName(context);
+ addDataKindPhoneticName(context);
+ addDataKindPhoto(context);
+ }
+ } catch (DefinitionException e) {
+ final StringBuilder error = new StringBuilder();
+ error.append("Problem reading XML");
+ if (needLineNumberInErrorLog && (parser != null)) {
+ error.append(" in line ");
+ error.append(parser.getLineNumber());
+ }
+ error.append(" for external package ");
+ error.append(packageName);
+
+ Log.e(TAG, error.toString(), e);
+ return;
+ } finally {
+ if (parser != null) {
+ parser.close();
+ }
+ }
+
+ mExtensionPackageNames = new ArrayList<String>();
+ mInviteActionLabelResId =
+ resolveExternalResId(
+ context,
+ mInviteActionLabelAttribute,
+ syncAdapterPackageName,
+ ATTR_INVITE_CONTACT_ACTION_LABEL);
+ mViewGroupLabelResId =
+ resolveExternalResId(
+ context,
+ mViewGroupLabelAttribute,
+ syncAdapterPackageName,
+ ATTR_VIEW_GROUP_ACTION_LABEL);
+ titleRes =
+ resolveExternalResId(
+ context, mAccountTypeLabelAttribute, syncAdapterPackageName, ATTR_ACCOUNT_LABEL);
+ iconRes =
+ resolveExternalResId(
+ context, mAccountTypeIconAttribute, syncAdapterPackageName, ATTR_ACCOUNT_ICON);
+
+ // If we reach this point, the account type has been successfully initialized.
+ mIsInitialized = true;
+ }
+
+ /**
+ * Returns the CONTACTS_STRUCTURE metadata (aka "contacts.xml") in the given apk package.
+ *
+ * <p>This method looks through all services in the package that handle sync adapter intents for
+ * the first one that contains CONTACTS_STRUCTURE metadata. We have to look through all sync
+ * adapters in the package in case there are contacts and other sync adapters (eg, calendar) in
+ * the same package.
+ *
+ * <p>Returns {@code null} if the package has no CONTACTS_STRUCTURE metadata. In this case the
+ * account type *will* be initialized with minimal configuration.
+ */
+ public static XmlResourceParser loadContactsXml(Context context, String resPackageName) {
+ final PackageManager pm = context.getPackageManager();
+ final Intent intent = new Intent(SYNC_META_DATA).setPackage(resPackageName);
+ final List<ResolveInfo> intentServices =
+ pm.queryIntentServices(intent, PackageManager.GET_SERVICES | PackageManager.GET_META_DATA);
+
+ if (intentServices != null) {
+ for (final ResolveInfo resolveInfo : intentServices) {
+ final ServiceInfo serviceInfo = resolveInfo.serviceInfo;
+ if (serviceInfo == null) {
+ continue;
+ }
+ for (String metadataName : METADATA_CONTACTS_NAMES) {
+ final XmlResourceParser parser = serviceInfo.loadXmlMetaData(pm, metadataName);
+ if (parser != null) {
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(
+ TAG,
+ String.format(
+ "Metadata loaded from: %s, %s, %s",
+ serviceInfo.packageName, serviceInfo.name, metadataName));
+ }
+ return parser;
+ }
+ }
+ }
+ }
+
+ // Package was found, but that doesn't contain the CONTACTS_STRUCTURE metadata.
+ return null;
+ }
+
+ /** Returns {@code TRUE} if the package contains CONTACTS_STRUCTURE metadata. */
+ public static boolean hasContactsXml(Context context, String resPackageName) {
+ return loadContactsXml(context, resPackageName) != null;
+ }
+
+ /**
+ * Takes a string in the "@xxx/yyy" format and return the resource ID for the resource in the
+ * resource package.
+ *
+ * <p>If the argument is in the invalid format or isn't a resource name, it returns -1.
+ *
+ * @param context context
+ * @param resourceName Resource name in the "@xxx/yyy" format, e.g. "@string/invite_lavbel"
+ * @param packageName name of the package containing the resource.
+ * @param xmlAttributeName attribute name which the resource came from. Used for logging.
+ */
+ @VisibleForTesting
+ static int resolveExternalResId(
+ Context context, String resourceName, String packageName, String xmlAttributeName) {
+ if (TextUtils.isEmpty(resourceName)) {
+ return -1; // Empty text is okay.
+ }
+ if (resourceName.charAt(0) != '@') {
+ Log.e(TAG, xmlAttributeName + " must be a resource name beginnig with '@'");
+ return -1;
+ }
+ final String name = resourceName.substring(1);
+ final Resources res;
+ try {
+ res = context.getPackageManager().getResourcesForApplication(packageName);
+ } catch (NameNotFoundException e) {
+ Log.e(TAG, "Unable to load package " + packageName);
+ return -1;
+ }
+ final int resId = res.getIdentifier(name, null, packageName);
+ if (resId == 0) {
+ Log.e(TAG, "Unable to load " + resourceName + " from package " + packageName);
+ return -1;
+ }
+ return resId;
+ }
+
+ private void checkKindExists(String mimeType) throws DefinitionException {
+ if (getKindForMimetype(mimeType) == null) {
+ throw new DefinitionException(mimeType + " must be supported");
+ }
+ }
+
+ @Override
+ public boolean isEmbedded() {
+ return false;
+ }
+
+ @Override
+ public boolean isExtension() {
+ return mIsExtension;
+ }
+
+ @Override
+ public boolean areContactsWritable() {
+ return mHasEditSchema;
+ }
+
+ /** Whether this account type has the android.provider.CONTACTS_STRUCTURE metadata xml. */
+ public boolean hasContactsMetadata() {
+ return mHasContactsMetadata;
+ }
+
+ @Override
+ public String getEditContactActivityClassName() {
+ return mEditContactActivityClassName;
+ }
+
+ @Override
+ public String getCreateContactActivityClassName() {
+ return mCreateContactActivityClassName;
+ }
+
+ @Override
+ public String getInviteContactActivityClassName() {
+ return mInviteContactActivity;
+ }
+
+ @Override
+ protected int getInviteContactActionResId() {
+ return mInviteActionLabelResId;
+ }
+
+ @Override
+ public String getViewContactNotifyServiceClassName() {
+ return mViewContactNotifyService;
+ }
+
+ @Override
+ public String getViewGroupActivity() {
+ return mViewGroupActivity;
+ }
+
+ @Override
+ protected int getViewGroupLabelResId() {
+ return mViewGroupLabelResId;
+ }
+
+ @Override
+ public List<String> getExtensionPackageNames() {
+ return mExtensionPackageNames;
+ }
+
+ /**
+ * Inflate this {@link AccountType} from the given parser. This may only load details matching the
+ * publicly-defined schema.
+ */
+ protected void inflate(Context context, XmlPullParser parser) throws DefinitionException {
+ final AttributeSet attrs = Xml.asAttributeSet(parser);
+
+ try {
+ int type;
+ while ((type = parser.next()) != XmlPullParser.START_TAG
+ && type != XmlPullParser.END_DOCUMENT) {
+ // Drain comments and whitespace
+ }
+
+ if (type != XmlPullParser.START_TAG) {
+ throw new IllegalStateException("No start tag found");
+ }
+
+ String rootTag = parser.getName();
+ if (!TAG_CONTACTS_ACCOUNT_TYPE.equals(rootTag)
+ && !TAG_CONTACTS_SOURCE_LEGACY.equals(rootTag)) {
+ throw new IllegalStateException(
+ "Top level element must be " + TAG_CONTACTS_ACCOUNT_TYPE + ", not " + rootTag);
+ }
+
+ mHasContactsMetadata = true;
+
+ int attributeCount = parser.getAttributeCount();
+ for (int i = 0; i < attributeCount; i++) {
+ String attr = parser.getAttributeName(i);
+ String value = parser.getAttributeValue(i);
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, attr + "=" + value);
+ }
+ if (ATTR_EDIT_CONTACT_ACTIVITY.equals(attr)) {
+ mEditContactActivityClassName = value;
+ } else if (ATTR_CREATE_CONTACT_ACTIVITY.equals(attr)) {
+ mCreateContactActivityClassName = value;
+ } else if (ATTR_INVITE_CONTACT_ACTIVITY.equals(attr)) {
+ mInviteContactActivity = value;
+ } else if (ATTR_INVITE_CONTACT_ACTION_LABEL.equals(attr)) {
+ mInviteActionLabelAttribute = value;
+ } else if (ATTR_VIEW_CONTACT_NOTIFY_SERVICE.equals(attr)) {
+ mViewContactNotifyService = value;
+ } else if (ATTR_VIEW_GROUP_ACTIVITY.equals(attr)) {
+ mViewGroupActivity = value;
+ } else if (ATTR_VIEW_GROUP_ACTION_LABEL.equals(attr)) {
+ mViewGroupLabelAttribute = value;
+ } else if (ATTR_DATA_SET.equals(attr)) {
+ dataSet = value;
+ } else if (ATTR_EXTENSION_PACKAGE_NAMES.equals(attr)) {
+ mExtensionPackageNames.add(value);
+ } else if (ATTR_ACCOUNT_TYPE.equals(attr)) {
+ accountType = value;
+ } else if (ATTR_ACCOUNT_LABEL.equals(attr)) {
+ mAccountTypeLabelAttribute = value;
+ } else if (ATTR_ACCOUNT_ICON.equals(attr)) {
+ mAccountTypeIconAttribute = value;
+ } else {
+ Log.e(TAG, "Unsupported attribute " + attr);
+ }
+ }
+
+ // Parse all children kinds
+ final int startDepth = parser.getDepth();
+ while (((type = parser.next()) != XmlPullParser.END_TAG || parser.getDepth() > startDepth)
+ && type != XmlPullParser.END_DOCUMENT) {
+
+ if (type != XmlPullParser.START_TAG || parser.getDepth() != startDepth + 1) {
+ continue; // Not a direct child tag
+ }
+
+ String tag = parser.getName();
+ if (TAG_EDIT_SCHEMA.equals(tag)) {
+ mHasEditSchema = true;
+ parseEditSchema(context, parser, attrs);
+ } else if (TAG_CONTACTS_DATA_KIND.equals(tag)) {
+ final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.ContactsDataKind);
+ final DataKind kind = new DataKind();
+
+ kind.mimeType = a.getString(R.styleable.ContactsDataKind_android_mimeType);
+ final String summaryColumn =
+ a.getString(R.styleable.ContactsDataKind_android_summaryColumn);
+ if (summaryColumn != null) {
+ // Inflate a specific column as summary when requested
+ kind.actionHeader = new SimpleInflater(summaryColumn);
+ }
+ final String detailColumn =
+ a.getString(R.styleable.ContactsDataKind_android_detailColumn);
+ if (detailColumn != null) {
+ // Inflate specific column as summary
+ kind.actionBody = new SimpleInflater(detailColumn);
+ }
+
+ a.recycle();
+
+ addKind(kind);
+ }
+ }
+ } catch (XmlPullParserException e) {
+ throw new DefinitionException("Problem reading XML", e);
+ } catch (IOException e) {
+ throw new DefinitionException("Problem reading XML", e);
+ }
+ }
+}
diff --git a/java/com/android/contacts/common/model/account/FallbackAccountType.java b/java/com/android/contacts/common/model/account/FallbackAccountType.java
new file mode 100644
index 000000000..976a7b892
--- /dev/null
+++ b/java/com/android/contacts/common/model/account/FallbackAccountType.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright (C) 2009 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.common.model.account;
+
+import android.content.Context;
+import android.util.Log;
+import com.android.contacts.common.R;
+import com.android.contacts.common.model.dataitem.DataKind;
+
+public class FallbackAccountType extends BaseAccountType {
+
+ private static final String TAG = "FallbackAccountType";
+
+ private FallbackAccountType(Context context, String resPackageName) {
+ this.accountType = null;
+ this.dataSet = null;
+ this.titleRes = R.string.account_phone;
+ this.iconRes = R.mipmap.ic_contacts_launcher;
+
+ // Note those are only set for unit tests.
+ this.resourcePackageName = resPackageName;
+ this.syncAdapterPackageName = resPackageName;
+
+ try {
+ addDataKindStructuredName(context);
+ addDataKindDisplayName(context);
+ addDataKindPhoneticName(context);
+ addDataKindNickname(context);
+ addDataKindPhone(context);
+ addDataKindEmail(context);
+ addDataKindStructuredPostal(context);
+ addDataKindIm(context);
+ addDataKindOrganization(context);
+ addDataKindPhoto(context);
+ addDataKindNote(context);
+ addDataKindWebsite(context);
+ addDataKindSipAddress(context);
+ addDataKindGroupMembership(context);
+
+ mIsInitialized = true;
+ } catch (DefinitionException e) {
+ Log.e(TAG, "Problem building account type", e);
+ }
+ }
+
+ public FallbackAccountType(Context context) {
+ this(context, null);
+ }
+
+ /**
+ * Used to compare with an {@link ExternalAccountType} built from a test contacts.xml. In order to
+ * build {@link DataKind}s with the same resource package name, {@code resPackageName} is
+ * injectable.
+ */
+ static AccountType createWithPackageNameForTest(Context context, String resPackageName) {
+ return new FallbackAccountType(context, resPackageName);
+ }
+
+ @Override
+ public boolean areContactsWritable() {
+ return true;
+ }
+}
diff --git a/java/com/android/contacts/common/model/account/GoogleAccountType.java b/java/com/android/contacts/common/model/account/GoogleAccountType.java
new file mode 100644
index 000000000..2f1fe0ed6
--- /dev/null
+++ b/java/com/android/contacts/common/model/account/GoogleAccountType.java
@@ -0,0 +1,206 @@
+/*
+ * Copyright (C) 2009 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.common.model.account;
+
+import android.content.ContentValues;
+import android.content.Context;
+import android.provider.ContactsContract.CommonDataKinds.Email;
+import android.provider.ContactsContract.CommonDataKinds.Event;
+import android.provider.ContactsContract.CommonDataKinds.Phone;
+import android.provider.ContactsContract.CommonDataKinds.Relation;
+import android.util.Log;
+import com.android.contacts.common.R;
+import com.android.contacts.common.model.dataitem.DataKind;
+import com.android.contacts.common.util.CommonDateUtils;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+public class GoogleAccountType extends BaseAccountType {
+
+ /**
+ * The package name that we should load contacts.xml from and rely on to handle G+ account
+ * actions. Even though this points to gms, in some cases gms will still hand off responsibility
+ * to the G+ app.
+ */
+ public static final String PLUS_EXTENSION_PACKAGE_NAME = "com.google.android.gms";
+
+ public static final String ACCOUNT_TYPE = "com.google";
+ private static final String TAG = "GoogleAccountType";
+ private static final List<String> mExtensionPackages =
+ new ArrayList<>(Collections.singletonList(PLUS_EXTENSION_PACKAGE_NAME));
+
+ public GoogleAccountType(Context context, String authenticatorPackageName) {
+ this.accountType = ACCOUNT_TYPE;
+ this.resourcePackageName = null;
+ this.syncAdapterPackageName = authenticatorPackageName;
+
+ try {
+ addDataKindStructuredName(context);
+ addDataKindDisplayName(context);
+ addDataKindPhoneticName(context);
+ addDataKindNickname(context);
+ addDataKindPhone(context);
+ addDataKindEmail(context);
+ addDataKindStructuredPostal(context);
+ addDataKindIm(context);
+ addDataKindOrganization(context);
+ addDataKindPhoto(context);
+ addDataKindNote(context);
+ addDataKindWebsite(context);
+ addDataKindSipAddress(context);
+ addDataKindGroupMembership(context);
+ addDataKindRelation(context);
+ addDataKindEvent(context);
+
+ mIsInitialized = true;
+ } catch (DefinitionException e) {
+ Log.e(TAG, "Problem building account type", e);
+ }
+ }
+
+ @Override
+ public List<String> getExtensionPackageNames() {
+ return mExtensionPackages;
+ }
+
+ @Override
+ protected DataKind addDataKindPhone(Context context) throws DefinitionException {
+ final DataKind kind = super.addDataKindPhone(context);
+
+ kind.typeColumn = Phone.TYPE;
+ kind.typeList = new ArrayList<>();
+ kind.typeList.add(buildPhoneType(Phone.TYPE_MOBILE));
+ kind.typeList.add(buildPhoneType(Phone.TYPE_WORK));
+ kind.typeList.add(buildPhoneType(Phone.TYPE_HOME));
+ kind.typeList.add(buildPhoneType(Phone.TYPE_MAIN));
+ kind.typeList.add(buildPhoneType(Phone.TYPE_FAX_WORK).setSecondary(true));
+ kind.typeList.add(buildPhoneType(Phone.TYPE_FAX_HOME).setSecondary(true));
+ kind.typeList.add(buildPhoneType(Phone.TYPE_PAGER).setSecondary(true));
+ kind.typeList.add(buildPhoneType(Phone.TYPE_OTHER));
+ kind.typeList.add(
+ buildPhoneType(Phone.TYPE_CUSTOM).setSecondary(true).setCustomColumn(Phone.LABEL));
+
+ kind.fieldList = new ArrayList<>();
+ kind.fieldList.add(new EditField(Phone.NUMBER, R.string.phoneLabelsGroup, FLAGS_PHONE));
+
+ return kind;
+ }
+
+ @Override
+ protected DataKind addDataKindEmail(Context context) throws DefinitionException {
+ final DataKind kind = super.addDataKindEmail(context);
+
+ kind.typeColumn = Email.TYPE;
+ kind.typeList = new ArrayList<>();
+ kind.typeList.add(buildEmailType(Email.TYPE_HOME));
+ kind.typeList.add(buildEmailType(Email.TYPE_WORK));
+ kind.typeList.add(buildEmailType(Email.TYPE_OTHER));
+ kind.typeList.add(
+ buildEmailType(Email.TYPE_CUSTOM).setSecondary(true).setCustomColumn(Email.LABEL));
+
+ kind.fieldList = new ArrayList<>();
+ kind.fieldList.add(new EditField(Email.DATA, R.string.emailLabelsGroup, FLAGS_EMAIL));
+
+ return kind;
+ }
+
+ private DataKind addDataKindRelation(Context context) throws DefinitionException {
+ DataKind kind =
+ addKind(
+ new DataKind(
+ Relation.CONTENT_ITEM_TYPE,
+ R.string.relationLabelsGroup,
+ Weight.RELATIONSHIP,
+ true));
+ kind.actionHeader = new RelationActionInflater();
+ kind.actionBody = new SimpleInflater(Relation.NAME);
+
+ kind.typeColumn = Relation.TYPE;
+ kind.typeList = new ArrayList<>();
+ kind.typeList.add(buildRelationType(Relation.TYPE_ASSISTANT));
+ kind.typeList.add(buildRelationType(Relation.TYPE_BROTHER));
+ kind.typeList.add(buildRelationType(Relation.TYPE_CHILD));
+ kind.typeList.add(buildRelationType(Relation.TYPE_DOMESTIC_PARTNER));
+ kind.typeList.add(buildRelationType(Relation.TYPE_FATHER));
+ kind.typeList.add(buildRelationType(Relation.TYPE_FRIEND));
+ kind.typeList.add(buildRelationType(Relation.TYPE_MANAGER));
+ kind.typeList.add(buildRelationType(Relation.TYPE_MOTHER));
+ kind.typeList.add(buildRelationType(Relation.TYPE_PARENT));
+ kind.typeList.add(buildRelationType(Relation.TYPE_PARTNER));
+ kind.typeList.add(buildRelationType(Relation.TYPE_REFERRED_BY));
+ kind.typeList.add(buildRelationType(Relation.TYPE_RELATIVE));
+ kind.typeList.add(buildRelationType(Relation.TYPE_SISTER));
+ kind.typeList.add(buildRelationType(Relation.TYPE_SPOUSE));
+ kind.typeList.add(
+ buildRelationType(Relation.TYPE_CUSTOM).setSecondary(true).setCustomColumn(Relation.LABEL));
+
+ kind.defaultValues = new ContentValues();
+ kind.defaultValues.put(Relation.TYPE, Relation.TYPE_SPOUSE);
+
+ kind.fieldList = new ArrayList<>();
+ kind.fieldList.add(new EditField(Relation.DATA, R.string.relationLabelsGroup, FLAGS_RELATION));
+
+ return kind;
+ }
+
+ private DataKind addDataKindEvent(Context context) throws DefinitionException {
+ DataKind kind =
+ addKind(
+ new DataKind(Event.CONTENT_ITEM_TYPE, R.string.eventLabelsGroup, Weight.EVENT, true));
+ kind.actionHeader = new EventActionInflater();
+ kind.actionBody = new SimpleInflater(Event.START_DATE);
+
+ kind.typeColumn = Event.TYPE;
+ kind.typeList = new ArrayList<>();
+ kind.dateFormatWithoutYear = CommonDateUtils.NO_YEAR_DATE_FORMAT;
+ kind.dateFormatWithYear = CommonDateUtils.FULL_DATE_FORMAT;
+ kind.typeList.add(buildEventType(Event.TYPE_BIRTHDAY, true).setSpecificMax(1));
+ kind.typeList.add(buildEventType(Event.TYPE_ANNIVERSARY, false));
+ kind.typeList.add(buildEventType(Event.TYPE_OTHER, false));
+ kind.typeList.add(
+ buildEventType(Event.TYPE_CUSTOM, false).setSecondary(true).setCustomColumn(Event.LABEL));
+
+ kind.defaultValues = new ContentValues();
+ kind.defaultValues.put(Event.TYPE, Event.TYPE_BIRTHDAY);
+
+ kind.fieldList = new ArrayList<>();
+ kind.fieldList.add(new EditField(Event.DATA, R.string.eventLabelsGroup, FLAGS_EVENT));
+
+ return kind;
+ }
+
+ @Override
+ public boolean isGroupMembershipEditable() {
+ return true;
+ }
+
+ @Override
+ public boolean areContactsWritable() {
+ return true;
+ }
+
+ @Override
+ public String getViewContactNotifyServiceClassName() {
+ return "com.google.android.syncadapters.contacts." + "SyncHighResPhotoIntentService";
+ }
+
+ @Override
+ public String getViewContactNotifyServicePackageName() {
+ return "com.google.android.syncadapters.contacts";
+ }
+}
diff --git a/java/com/android/contacts/common/model/account/SamsungAccountType.java b/java/com/android/contacts/common/model/account/SamsungAccountType.java
new file mode 100644
index 000000000..45406bc2b
--- /dev/null
+++ b/java/com/android/contacts/common/model/account/SamsungAccountType.java
@@ -0,0 +1,235 @@
+/*
+ * Copyright (C) 2015 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.common.model.account;
+
+import android.content.ContentValues;
+import android.content.Context;
+import android.provider.ContactsContract.CommonDataKinds.Email;
+import android.provider.ContactsContract.CommonDataKinds.Event;
+import android.provider.ContactsContract.CommonDataKinds.Phone;
+import android.provider.ContactsContract.CommonDataKinds.Relation;
+import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
+import android.util.Log;
+import com.android.contacts.common.R;
+import com.android.contacts.common.model.dataitem.DataKind;
+import com.android.contacts.common.util.CommonDateUtils;
+import java.util.ArrayList;
+import java.util.Locale;
+
+/**
+ * A writable account type that can be used to support samsung contacts. This may not perfectly
+ * match Samsung's latest intended account schema.
+ *
+ * <p>This is only used to partially support Samsung accounts. The DataKind labels & fields are
+ * setup to support the values used by Samsung. But, not everything in the Samsung account type is
+ * supported. The Samsung account type includes a "Message Type" mimetype that we have no intention
+ * of showing inside the Contact editor. Similarly, we don't handle the "Ringtone" mimetype here
+ * since managing ringtones is handled in a different flow.
+ */
+public class SamsungAccountType extends BaseAccountType {
+
+ private static final String TAG = "KnownExternalAccountType";
+ private static final String ACCOUNT_TYPE_SAMSUNG = "com.osp.app.signin";
+
+ public SamsungAccountType(Context context, String authenticatorPackageName, String type) {
+ this.accountType = type;
+ this.resourcePackageName = null;
+ this.syncAdapterPackageName = authenticatorPackageName;
+
+ try {
+ addDataKindStructuredName(context);
+ addDataKindDisplayName(context);
+ addDataKindPhoneticName(context);
+ addDataKindNickname(context);
+ addDataKindPhone(context);
+ addDataKindEmail(context);
+ addDataKindStructuredPostal(context);
+ addDataKindIm(context);
+ addDataKindOrganization(context);
+ addDataKindPhoto(context);
+ addDataKindNote(context);
+ addDataKindWebsite(context);
+ addDataKindGroupMembership(context);
+ addDataKindRelation(context);
+ addDataKindEvent(context);
+
+ mIsInitialized = true;
+ } catch (DefinitionException e) {
+ Log.e(TAG, "Problem building account type", e);
+ }
+ }
+
+ /**
+ * Returns {@code TRUE} if this is samsung's account type and Samsung hasn't bothered to define a
+ * contacts.xml to provide a more accurate definition than ours.
+ */
+ public static boolean isSamsungAccountType(Context context, String type, String packageName) {
+ return ACCOUNT_TYPE_SAMSUNG.equals(type)
+ && !ExternalAccountType.hasContactsXml(context, packageName);
+ }
+
+ @Override
+ protected DataKind addDataKindStructuredPostal(Context context) throws DefinitionException {
+ final DataKind kind = super.addDataKindStructuredPostal(context);
+
+ final boolean useJapaneseOrder =
+ Locale.JAPANESE.getLanguage().equals(Locale.getDefault().getLanguage());
+ kind.typeColumn = StructuredPostal.TYPE;
+ kind.typeList = new ArrayList<>();
+ kind.typeList.add(buildPostalType(StructuredPostal.TYPE_WORK).setSpecificMax(1));
+ kind.typeList.add(buildPostalType(StructuredPostal.TYPE_HOME).setSpecificMax(1));
+ kind.typeList.add(buildPostalType(StructuredPostal.TYPE_OTHER).setSpecificMax(1));
+
+ kind.fieldList = new ArrayList<>();
+ if (useJapaneseOrder) {
+ kind.fieldList.add(
+ new EditField(StructuredPostal.COUNTRY, R.string.postal_country, FLAGS_POSTAL)
+ .setOptional(true));
+ kind.fieldList.add(
+ new EditField(StructuredPostal.POSTCODE, R.string.postal_postcode, FLAGS_POSTAL));
+ kind.fieldList.add(
+ new EditField(StructuredPostal.REGION, R.string.postal_region, FLAGS_POSTAL));
+ kind.fieldList.add(new EditField(StructuredPostal.CITY, R.string.postal_city, FLAGS_POSTAL));
+ kind.fieldList.add(
+ new EditField(StructuredPostal.STREET, R.string.postal_street, FLAGS_POSTAL));
+ } else {
+ kind.fieldList.add(
+ new EditField(StructuredPostal.STREET, R.string.postal_street, FLAGS_POSTAL));
+ kind.fieldList.add(new EditField(StructuredPostal.CITY, R.string.postal_city, FLAGS_POSTAL));
+ kind.fieldList.add(
+ new EditField(StructuredPostal.REGION, R.string.postal_region, FLAGS_POSTAL));
+ kind.fieldList.add(
+ new EditField(StructuredPostal.POSTCODE, R.string.postal_postcode, FLAGS_POSTAL));
+ kind.fieldList.add(
+ new EditField(StructuredPostal.COUNTRY, R.string.postal_country, FLAGS_POSTAL)
+ .setOptional(true));
+ }
+
+ return kind;
+ }
+
+ @Override
+ protected DataKind addDataKindPhone(Context context) throws DefinitionException {
+ final DataKind kind = super.addDataKindPhone(context);
+
+ kind.typeColumn = Phone.TYPE;
+ kind.typeList = new ArrayList<>();
+ kind.typeList.add(buildPhoneType(Phone.TYPE_MOBILE));
+ kind.typeList.add(buildPhoneType(Phone.TYPE_HOME));
+ kind.typeList.add(buildPhoneType(Phone.TYPE_WORK));
+ kind.typeList.add(buildPhoneType(Phone.TYPE_MAIN));
+ kind.typeList.add(buildPhoneType(Phone.TYPE_FAX_WORK).setSecondary(true));
+ kind.typeList.add(buildPhoneType(Phone.TYPE_FAX_HOME).setSecondary(true));
+ kind.typeList.add(buildPhoneType(Phone.TYPE_PAGER).setSecondary(true));
+ kind.typeList.add(buildPhoneType(Phone.TYPE_RADIO).setSecondary(true));
+ kind.typeList.add(buildPhoneType(Phone.TYPE_OTHER));
+ kind.typeList.add(
+ buildPhoneType(Phone.TYPE_CUSTOM).setSecondary(true).setCustomColumn(Phone.LABEL));
+
+ kind.fieldList = new ArrayList<>();
+ kind.fieldList.add(new EditField(Phone.NUMBER, R.string.phoneLabelsGroup, FLAGS_PHONE));
+
+ return kind;
+ }
+
+ @Override
+ protected DataKind addDataKindEmail(Context context) throws DefinitionException {
+ final DataKind kind = super.addDataKindEmail(context);
+
+ kind.typeColumn = Email.TYPE;
+ kind.typeList = new ArrayList<>();
+ kind.typeList.add(buildEmailType(Email.TYPE_HOME));
+ kind.typeList.add(buildEmailType(Email.TYPE_WORK));
+ kind.typeList.add(buildEmailType(Email.TYPE_OTHER));
+ kind.typeList.add(
+ buildEmailType(Email.TYPE_CUSTOM).setSecondary(true).setCustomColumn(Email.LABEL));
+
+ kind.fieldList = new ArrayList<>();
+ kind.fieldList.add(new EditField(Email.DATA, R.string.emailLabelsGroup, FLAGS_EMAIL));
+
+ return kind;
+ }
+
+ private DataKind addDataKindRelation(Context context) throws DefinitionException {
+ DataKind kind =
+ addKind(new DataKind(Relation.CONTENT_ITEM_TYPE, R.string.relationLabelsGroup, 160, true));
+ kind.actionHeader = new RelationActionInflater();
+ kind.actionBody = new SimpleInflater(Relation.NAME);
+
+ kind.typeColumn = Relation.TYPE;
+ kind.typeList = new ArrayList<>();
+ kind.typeList.add(buildRelationType(Relation.TYPE_ASSISTANT));
+ kind.typeList.add(buildRelationType(Relation.TYPE_BROTHER));
+ kind.typeList.add(buildRelationType(Relation.TYPE_CHILD));
+ kind.typeList.add(buildRelationType(Relation.TYPE_DOMESTIC_PARTNER));
+ kind.typeList.add(buildRelationType(Relation.TYPE_FATHER));
+ kind.typeList.add(buildRelationType(Relation.TYPE_FRIEND));
+ kind.typeList.add(buildRelationType(Relation.TYPE_MANAGER));
+ kind.typeList.add(buildRelationType(Relation.TYPE_MOTHER));
+ kind.typeList.add(buildRelationType(Relation.TYPE_PARENT));
+ kind.typeList.add(buildRelationType(Relation.TYPE_PARTNER));
+ kind.typeList.add(buildRelationType(Relation.TYPE_REFERRED_BY));
+ kind.typeList.add(buildRelationType(Relation.TYPE_RELATIVE));
+ kind.typeList.add(buildRelationType(Relation.TYPE_SISTER));
+ kind.typeList.add(buildRelationType(Relation.TYPE_SPOUSE));
+ kind.typeList.add(
+ buildRelationType(Relation.TYPE_CUSTOM).setSecondary(true).setCustomColumn(Relation.LABEL));
+
+ kind.defaultValues = new ContentValues();
+ kind.defaultValues.put(Relation.TYPE, Relation.TYPE_SPOUSE);
+
+ kind.fieldList = new ArrayList<>();
+ kind.fieldList.add(new EditField(Relation.DATA, R.string.relationLabelsGroup, FLAGS_RELATION));
+
+ return kind;
+ }
+
+ private DataKind addDataKindEvent(Context context) throws DefinitionException {
+ DataKind kind =
+ addKind(new DataKind(Event.CONTENT_ITEM_TYPE, R.string.eventLabelsGroup, 150, true));
+ kind.actionHeader = new EventActionInflater();
+ kind.actionBody = new SimpleInflater(Event.START_DATE);
+
+ kind.typeColumn = Event.TYPE;
+ kind.typeList = new ArrayList<>();
+ kind.dateFormatWithoutYear = CommonDateUtils.NO_YEAR_DATE_FORMAT;
+ kind.dateFormatWithYear = CommonDateUtils.FULL_DATE_FORMAT;
+ kind.typeList.add(buildEventType(Event.TYPE_BIRTHDAY, true).setSpecificMax(1));
+ kind.typeList.add(buildEventType(Event.TYPE_ANNIVERSARY, false));
+ kind.typeList.add(buildEventType(Event.TYPE_OTHER, false));
+ kind.typeList.add(
+ buildEventType(Event.TYPE_CUSTOM, false).setSecondary(true).setCustomColumn(Event.LABEL));
+
+ kind.defaultValues = new ContentValues();
+ kind.defaultValues.put(Event.TYPE, Event.TYPE_BIRTHDAY);
+
+ kind.fieldList = new ArrayList<>();
+ kind.fieldList.add(new EditField(Event.DATA, R.string.eventLabelsGroup, FLAGS_EVENT));
+
+ return kind;
+ }
+
+ @Override
+ public boolean isGroupMembershipEditable() {
+ return true;
+ }
+
+ @Override
+ public boolean areContactsWritable() {
+ return true;
+ }
+}
diff --git a/java/com/android/contacts/common/model/dataitem/DataItem.java b/java/com/android/contacts/common/model/dataitem/DataItem.java
new file mode 100644
index 000000000..dc746055b
--- /dev/null
+++ b/java/com/android/contacts/common/model/dataitem/DataItem.java
@@ -0,0 +1,258 @@
+/*
+ * Copyright (C) 2012 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.common.model.dataitem;
+
+import android.content.ContentValues;
+import android.content.Context;
+import android.provider.ContactsContract.CommonDataKinds.Email;
+import android.provider.ContactsContract.CommonDataKinds.Event;
+import android.provider.ContactsContract.CommonDataKinds.GroupMembership;
+import android.provider.ContactsContract.CommonDataKinds.Identity;
+import android.provider.ContactsContract.CommonDataKinds.Im;
+import android.provider.ContactsContract.CommonDataKinds.Nickname;
+import android.provider.ContactsContract.CommonDataKinds.Note;
+import android.provider.ContactsContract.CommonDataKinds.Organization;
+import android.provider.ContactsContract.CommonDataKinds.Phone;
+import android.provider.ContactsContract.CommonDataKinds.Photo;
+import android.provider.ContactsContract.CommonDataKinds.Relation;
+import android.provider.ContactsContract.CommonDataKinds.SipAddress;
+import android.provider.ContactsContract.CommonDataKinds.StructuredName;
+import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
+import android.provider.ContactsContract.CommonDataKinds.Website;
+import android.provider.ContactsContract.Contacts.Data;
+import android.provider.ContactsContract.Contacts.Entity;
+import com.android.contacts.common.Collapser;
+import com.android.contacts.common.MoreContactUtils;
+import com.android.contacts.common.model.account.AccountType.EditType;
+
+/** This is the base class for data items, which represents a row from the Data table. */
+public class DataItem implements Collapser.Collapsible<DataItem> {
+
+ private final ContentValues mContentValues;
+ protected DataKind mKind;
+
+ protected DataItem(ContentValues values) {
+ mContentValues = values;
+ }
+
+ /**
+ * Factory for creating subclasses of DataItem objects based on the mimetype in the content
+ * values. Raw contact is the raw contact that this data item is associated with.
+ */
+ public static DataItem createFrom(ContentValues values) {
+ final String mimeType = values.getAsString(Data.MIMETYPE);
+ if (GroupMembership.CONTENT_ITEM_TYPE.equals(mimeType)) {
+ return new GroupMembershipDataItem(values);
+ } else if (StructuredName.CONTENT_ITEM_TYPE.equals(mimeType)) {
+ return new StructuredNameDataItem(values);
+ } else if (Phone.CONTENT_ITEM_TYPE.equals(mimeType)) {
+ return new PhoneDataItem(values);
+ } else if (Email.CONTENT_ITEM_TYPE.equals(mimeType)) {
+ return new EmailDataItem(values);
+ } else if (StructuredPostal.CONTENT_ITEM_TYPE.equals(mimeType)) {
+ return new StructuredPostalDataItem(values);
+ } else if (Im.CONTENT_ITEM_TYPE.equals(mimeType)) {
+ return new ImDataItem(values);
+ } else if (Organization.CONTENT_ITEM_TYPE.equals(mimeType)) {
+ return new OrganizationDataItem(values);
+ } else if (Nickname.CONTENT_ITEM_TYPE.equals(mimeType)) {
+ return new NicknameDataItem(values);
+ } else if (Note.CONTENT_ITEM_TYPE.equals(mimeType)) {
+ return new NoteDataItem(values);
+ } else if (Website.CONTENT_ITEM_TYPE.equals(mimeType)) {
+ return new WebsiteDataItem(values);
+ } else if (SipAddress.CONTENT_ITEM_TYPE.equals(mimeType)) {
+ return new SipAddressDataItem(values);
+ } else if (Event.CONTENT_ITEM_TYPE.equals(mimeType)) {
+ return new EventDataItem(values);
+ } else if (Relation.CONTENT_ITEM_TYPE.equals(mimeType)) {
+ return new RelationDataItem(values);
+ } else if (Identity.CONTENT_ITEM_TYPE.equals(mimeType)) {
+ return new IdentityDataItem(values);
+ } else if (Photo.CONTENT_ITEM_TYPE.equals(mimeType)) {
+ return new PhotoDataItem(values);
+ }
+
+ // generic
+ return new DataItem(values);
+ }
+
+ public ContentValues getContentValues() {
+ return mContentValues;
+ }
+
+ public Long getRawContactId() {
+ return mContentValues.getAsLong(Data.RAW_CONTACT_ID);
+ }
+
+ public void setRawContactId(long rawContactId) {
+ mContentValues.put(Data.RAW_CONTACT_ID, rawContactId);
+ }
+
+ /** Returns the data id. */
+ public long getId() {
+ return mContentValues.getAsLong(Data._ID);
+ }
+
+ /** Returns the mimetype of the data. */
+ public String getMimeType() {
+ return mContentValues.getAsString(Data.MIMETYPE);
+ }
+
+ public void setMimeType(String mimeType) {
+ mContentValues.put(Data.MIMETYPE, mimeType);
+ }
+
+ public boolean isPrimary() {
+ Integer primary = mContentValues.getAsInteger(Data.IS_PRIMARY);
+ return primary != null && primary != 0;
+ }
+
+ public boolean isSuperPrimary() {
+ Integer superPrimary = mContentValues.getAsInteger(Data.IS_SUPER_PRIMARY);
+ return superPrimary != null && superPrimary != 0;
+ }
+
+ public boolean hasKindTypeColumn(DataKind kind) {
+ final String key = kind.typeColumn;
+ return key != null
+ && mContentValues.containsKey(key)
+ && mContentValues.getAsInteger(key) != null;
+ }
+
+ public int getKindTypeColumn(DataKind kind) {
+ final String key = kind.typeColumn;
+ return mContentValues.getAsInteger(key);
+ }
+
+ /**
+ * Indicates the carrier presence value for the current {@link DataItem}.
+ *
+ * @return {@link Data#CARRIER_PRESENCE_VT_CAPABLE} if the {@link DataItem} supports carrier video
+ * calling, {@code 0} otherwise.
+ */
+ public int getCarrierPresence() {
+ return mContentValues.getAsInteger(Data.CARRIER_PRESENCE);
+ }
+
+ /**
+ * This builds the data string depending on the type of data item by using the generic DataKind
+ * object underneath.
+ */
+ public String buildDataString(Context context, DataKind kind) {
+ if (kind.actionBody == null) {
+ return null;
+ }
+ CharSequence actionBody = kind.actionBody.inflateUsing(context, mContentValues);
+ return actionBody == null ? null : actionBody.toString();
+ }
+
+ /**
+ * This builds the data string(intended for display) depending on the type of data item. It
+ * returns the same value as {@link #buildDataString} by default, but certain data items can
+ * override it to provide their version of formatted data strings.
+ *
+ * @return Data string representing the data item, possibly formatted for display
+ */
+ public String buildDataStringForDisplay(Context context, DataKind kind) {
+ return buildDataString(context, kind);
+ }
+
+ public DataKind getDataKind() {
+ return mKind;
+ }
+
+ public void setDataKind(DataKind kind) {
+ mKind = kind;
+ }
+
+ public Integer getTimesUsed() {
+ return mContentValues.getAsInteger(Entity.TIMES_USED);
+ }
+
+ public Long getLastTimeUsed() {
+ return mContentValues.getAsLong(Entity.LAST_TIME_USED);
+ }
+
+ @Override
+ public void collapseWith(DataItem that) {
+ DataKind thisKind = getDataKind();
+ DataKind thatKind = that.getDataKind();
+ // If this does not have a type and that does, or if that's type is higher precedence,
+ // use that's type
+ if ((!hasKindTypeColumn(thisKind) && that.hasKindTypeColumn(thatKind))
+ || (that.hasKindTypeColumn(thatKind)
+ && getTypePrecedence(thisKind, getKindTypeColumn(thisKind))
+ > getTypePrecedence(thatKind, that.getKindTypeColumn(thatKind)))) {
+ mContentValues.put(thatKind.typeColumn, that.getKindTypeColumn(thatKind));
+ mKind = thatKind;
+ }
+
+ // Choose the max of the maxLines and maxLabelLines values.
+ mKind.maxLinesForDisplay = Math.max(thisKind.maxLinesForDisplay, thatKind.maxLinesForDisplay);
+
+ // If any of the collapsed entries are super primary make the whole thing super primary.
+ if (isSuperPrimary() || that.isSuperPrimary()) {
+ mContentValues.put(Data.IS_SUPER_PRIMARY, 1);
+ mContentValues.put(Data.IS_PRIMARY, 1);
+ }
+
+ // If any of the collapsed entries are primary make the whole thing primary.
+ if (isPrimary() || that.isPrimary()) {
+ mContentValues.put(Data.IS_PRIMARY, 1);
+ }
+
+ // Add up the times used
+ mContentValues.put(
+ Entity.TIMES_USED,
+ (getTimesUsed() == null ? 0 : getTimesUsed())
+ + (that.getTimesUsed() == null ? 0 : that.getTimesUsed()));
+
+ // Use the most recent time
+ mContentValues.put(
+ Entity.LAST_TIME_USED,
+ Math.max(
+ getLastTimeUsed() == null ? 0 : getLastTimeUsed(),
+ that.getLastTimeUsed() == null ? 0 : that.getLastTimeUsed()));
+ }
+
+ @Override
+ public boolean shouldCollapseWith(DataItem t, Context context) {
+ if (mKind == null || t.getDataKind() == null) {
+ return false;
+ }
+ return MoreContactUtils.shouldCollapse(
+ getMimeType(),
+ buildDataString(context, mKind),
+ t.getMimeType(),
+ t.buildDataString(context, t.getDataKind()));
+ }
+
+ /**
+ * Return the precedence for the the given {@link EditType#rawValue}, where lower numbers are
+ * higher precedence.
+ */
+ private static int getTypePrecedence(DataKind kind, int rawValue) {
+ for (int i = 0; i < kind.typeList.size(); i++) {
+ final EditType type = kind.typeList.get(i);
+ if (type.rawValue == rawValue) {
+ return i;
+ }
+ }
+ return Integer.MAX_VALUE;
+ }
+}
diff --git a/java/com/android/contacts/common/model/dataitem/DataKind.java b/java/com/android/contacts/common/model/dataitem/DataKind.java
new file mode 100644
index 000000000..3b470a2ae
--- /dev/null
+++ b/java/com/android/contacts/common/model/dataitem/DataKind.java
@@ -0,0 +1,132 @@
+/*
+ * 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.common.model.dataitem;
+
+import android.content.ContentValues;
+import android.content.Context;
+import android.provider.ContactsContract.Data;
+import com.android.contacts.common.model.account.AccountType.EditField;
+import com.android.contacts.common.model.account.AccountType.EditType;
+import com.android.contacts.common.model.account.AccountType.StringInflater;
+import com.google.common.collect.Iterators;
+import java.text.SimpleDateFormat;
+import java.util.List;
+
+/**
+ * Description of a specific data type, usually marked by a unique {@link Data#MIMETYPE}. Includes
+ * details about how to view and edit {@link Data} rows of this kind, including the possible {@link
+ * EditType} labels and editable {@link EditField}.
+ */
+public final class DataKind {
+
+ public static final String PSEUDO_MIME_TYPE_DISPLAY_NAME = "#displayName";
+ public static final String PSEUDO_MIME_TYPE_PHONETIC_NAME = "#phoneticName";
+ public static final String PSEUDO_COLUMN_PHONETIC_NAME = "#phoneticName";
+
+ public String resourcePackageName;
+ public String mimeType;
+ public int titleRes;
+ public int iconAltRes;
+ public int iconAltDescriptionRes;
+ public int weight;
+ public boolean editable;
+
+ public StringInflater actionHeader;
+ public StringInflater actionAltHeader;
+ public StringInflater actionBody;
+
+ public String typeColumn;
+
+ /** Maximum number of values allowed in the list. -1 represents infinity. */
+ public int typeOverallMax;
+
+ public List<EditType> typeList;
+ public List<EditField> fieldList;
+
+ public ContentValues defaultValues;
+
+ /**
+ * If this is a date field, this specifies the format of the date when saving. The date includes
+ * year, month and day. If this is not a date field or the date field is not editable, this value
+ * should be ignored.
+ */
+ public SimpleDateFormat dateFormatWithoutYear;
+
+ /**
+ * If this is a date field, this specifies the format of the date when saving. The date includes
+ * month and day. If this is not a date field, the field is not editable or dates without year are
+ * not supported, this value should be ignored.
+ */
+ public SimpleDateFormat dateFormatWithYear;
+
+ /** The number of lines available for displaying this kind of data. Defaults to 1. */
+ public int maxLinesForDisplay;
+
+ public DataKind() {
+ maxLinesForDisplay = 1;
+ }
+
+ public DataKind(String mimeType, int titleRes, int weight, boolean editable) {
+ this.mimeType = mimeType;
+ this.titleRes = titleRes;
+ this.weight = weight;
+ this.editable = editable;
+ this.typeOverallMax = -1;
+ maxLinesForDisplay = 1;
+ }
+
+ public static String toString(SimpleDateFormat format) {
+ return format == null ? "(null)" : format.toPattern();
+ }
+
+ public static String toString(Iterable<?> list) {
+ if (list == null) {
+ return "(null)";
+ } else {
+ return Iterators.toString(list.iterator());
+ }
+ }
+
+ public String getKindString(Context context) {
+ return (titleRes == -1 || titleRes == 0) ? "" : context.getString(titleRes);
+ }
+
+ @Override
+ public String toString() {
+ final StringBuilder sb = new StringBuilder();
+ sb.append("DataKind:");
+ sb.append(" resPackageName=").append(resourcePackageName);
+ sb.append(" mimeType=").append(mimeType);
+ sb.append(" titleRes=").append(titleRes);
+ sb.append(" iconAltRes=").append(iconAltRes);
+ sb.append(" iconAltDescriptionRes=").append(iconAltDescriptionRes);
+ sb.append(" weight=").append(weight);
+ sb.append(" editable=").append(editable);
+ sb.append(" actionHeader=").append(actionHeader);
+ sb.append(" actionAltHeader=").append(actionAltHeader);
+ sb.append(" actionBody=").append(actionBody);
+ sb.append(" typeColumn=").append(typeColumn);
+ sb.append(" typeOverallMax=").append(typeOverallMax);
+ sb.append(" typeList=").append(toString(typeList));
+ sb.append(" fieldList=").append(toString(fieldList));
+ sb.append(" defaultValues=").append(defaultValues);
+ sb.append(" dateFormatWithoutYear=").append(toString(dateFormatWithoutYear));
+ sb.append(" dateFormatWithYear=").append(toString(dateFormatWithYear));
+
+ return sb.toString();
+ }
+}
diff --git a/java/com/android/contacts/common/model/dataitem/EmailDataItem.java b/java/com/android/contacts/common/model/dataitem/EmailDataItem.java
new file mode 100644
index 000000000..2fe297816
--- /dev/null
+++ b/java/com/android/contacts/common/model/dataitem/EmailDataItem.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2012 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.common.model.dataitem;
+
+import android.content.ContentValues;
+import android.provider.ContactsContract.CommonDataKinds.Email;
+
+/**
+ * Represents an email data item, wrapping the columns in {@link
+ * ContactsContract.CommonDataKinds.Email}.
+ */
+public class EmailDataItem extends DataItem {
+
+ /* package */ EmailDataItem(ContentValues values) {
+ super(values);
+ }
+
+ public String getAddress() {
+ return getContentValues().getAsString(Email.ADDRESS);
+ }
+
+ public String getDisplayName() {
+ return getContentValues().getAsString(Email.DISPLAY_NAME);
+ }
+
+ public String getData() {
+ return getContentValues().getAsString(Email.DATA);
+ }
+
+ public String getLabel() {
+ return getContentValues().getAsString(Email.LABEL);
+ }
+}
diff --git a/java/com/android/contacts/common/model/dataitem/EventDataItem.java b/java/com/android/contacts/common/model/dataitem/EventDataItem.java
new file mode 100644
index 000000000..15d9880b1
--- /dev/null
+++ b/java/com/android/contacts/common/model/dataitem/EventDataItem.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2012 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.common.model.dataitem;
+
+import android.content.ContentValues;
+import android.content.Context;
+import android.provider.ContactsContract.CommonDataKinds.Event;
+import android.text.TextUtils;
+
+/**
+ * Represents an event data item, wrapping the columns in {@link
+ * ContactsContract.CommonDataKinds.Event}.
+ */
+public class EventDataItem extends DataItem {
+
+ /* package */ EventDataItem(ContentValues values) {
+ super(values);
+ }
+
+ public String getStartDate() {
+ return getContentValues().getAsString(Event.START_DATE);
+ }
+
+ public String getLabel() {
+ return getContentValues().getAsString(Event.LABEL);
+ }
+
+ @Override
+ public boolean shouldCollapseWith(DataItem t, Context context) {
+ if (!(t instanceof EventDataItem) || mKind == null || t.getDataKind() == null) {
+ return false;
+ }
+ final EventDataItem that = (EventDataItem) t;
+ // Events can be different (anniversary, birthday) but have the same start date
+ if (!TextUtils.equals(getStartDate(), that.getStartDate())) {
+ return false;
+ } else if (!hasKindTypeColumn(mKind) || !that.hasKindTypeColumn(that.getDataKind())) {
+ return hasKindTypeColumn(mKind) == that.hasKindTypeColumn(that.getDataKind());
+ } else if (getKindTypeColumn(mKind) != that.getKindTypeColumn(that.getDataKind())) {
+ return false;
+ } else if (getKindTypeColumn(mKind) == Event.TYPE_CUSTOM
+ && !TextUtils.equals(getLabel(), that.getLabel())) {
+ // Check if custom types are not the same
+ return false;
+ }
+ return true;
+ }
+}
diff --git a/java/com/android/contacts/common/model/dataitem/GroupMembershipDataItem.java b/java/com/android/contacts/common/model/dataitem/GroupMembershipDataItem.java
new file mode 100644
index 000000000..f921b3c9d
--- /dev/null
+++ b/java/com/android/contacts/common/model/dataitem/GroupMembershipDataItem.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2012 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.common.model.dataitem;
+
+import android.content.ContentValues;
+import android.provider.ContactsContract;
+import android.provider.ContactsContract.CommonDataKinds.GroupMembership;
+
+/**
+ * Represents a group memebership data item, wrapping the columns in {@link
+ * ContactsContract.CommonDataKinds.GroupMembership}.
+ */
+public class GroupMembershipDataItem extends DataItem {
+
+ /* package */ GroupMembershipDataItem(ContentValues values) {
+ super(values);
+ }
+
+ public Long getGroupRowId() {
+ return getContentValues().getAsLong(GroupMembership.GROUP_ROW_ID);
+ }
+
+ public String getGroupSourceId() {
+ return getContentValues().getAsString(GroupMembership.GROUP_SOURCE_ID);
+ }
+}
diff --git a/java/com/android/contacts/common/model/dataitem/IdentityDataItem.java b/java/com/android/contacts/common/model/dataitem/IdentityDataItem.java
new file mode 100644
index 000000000..2badf92f7
--- /dev/null
+++ b/java/com/android/contacts/common/model/dataitem/IdentityDataItem.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2012 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.common.model.dataitem;
+
+import android.content.ContentValues;
+import android.provider.ContactsContract.CommonDataKinds.Identity;
+
+/**
+ * Represents an identity data item, wrapping the columns in {@link
+ * ContactsContract.CommonDataKinds.Identity}.
+ */
+public class IdentityDataItem extends DataItem {
+
+ /* package */ IdentityDataItem(ContentValues values) {
+ super(values);
+ }
+
+ public String getIdentity() {
+ return getContentValues().getAsString(Identity.IDENTITY);
+ }
+
+ public String getNamespace() {
+ return getContentValues().getAsString(Identity.NAMESPACE);
+ }
+}
diff --git a/java/com/android/contacts/common/model/dataitem/ImDataItem.java b/java/com/android/contacts/common/model/dataitem/ImDataItem.java
new file mode 100644
index 000000000..16b9fd094
--- /dev/null
+++ b/java/com/android/contacts/common/model/dataitem/ImDataItem.java
@@ -0,0 +1,109 @@
+/*
+ * Copyright (C) 2012 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.common.model.dataitem;
+
+import android.content.ContentValues;
+import android.content.Context;
+import android.provider.ContactsContract.CommonDataKinds.Email;
+import android.provider.ContactsContract.CommonDataKinds.Im;
+import android.text.TextUtils;
+
+/**
+ * Represents an IM data item, wrapping the columns in {@link ContactsContract.CommonDataKinds.Im}.
+ */
+public class ImDataItem extends DataItem {
+
+ private final boolean mCreatedFromEmail;
+
+ /* package */ ImDataItem(ContentValues values) {
+ super(values);
+ mCreatedFromEmail = false;
+ }
+
+ private ImDataItem(ContentValues values, boolean createdFromEmail) {
+ super(values);
+ mCreatedFromEmail = createdFromEmail;
+ }
+
+ public static ImDataItem createFromEmail(EmailDataItem item) {
+ final ImDataItem im = new ImDataItem(new ContentValues(item.getContentValues()), true);
+ im.setMimeType(Im.CONTENT_ITEM_TYPE);
+ return im;
+ }
+
+ public String getData() {
+ if (mCreatedFromEmail) {
+ return getContentValues().getAsString(Email.DATA);
+ } else {
+ return getContentValues().getAsString(Im.DATA);
+ }
+ }
+
+ public String getLabel() {
+ return getContentValues().getAsString(Im.LABEL);
+ }
+
+ /** Values are one of Im.PROTOCOL_ */
+ public Integer getProtocol() {
+ return getContentValues().getAsInteger(Im.PROTOCOL);
+ }
+
+ public boolean isProtocolValid() {
+ return getProtocol() != null;
+ }
+
+ public String getCustomProtocol() {
+ return getContentValues().getAsString(Im.CUSTOM_PROTOCOL);
+ }
+
+ public int getChatCapability() {
+ Integer result = getContentValues().getAsInteger(Im.CHAT_CAPABILITY);
+ return result == null ? 0 : result;
+ }
+
+ public boolean isCreatedFromEmail() {
+ return mCreatedFromEmail;
+ }
+
+ @Override
+ public boolean shouldCollapseWith(DataItem t, Context context) {
+ if (!(t instanceof ImDataItem) || mKind == null || t.getDataKind() == null) {
+ return false;
+ }
+ final ImDataItem that = (ImDataItem) t;
+ // IM can have the same data put different protocol. These should not collapse.
+ if (!getData().equals(that.getData())) {
+ return false;
+ } else if (!isProtocolValid() || !that.isProtocolValid()) {
+ // Deal with invalid protocol as if it was custom. If either has a non valid
+ // protocol, check to see if the other has a valid that is not custom
+ if (isProtocolValid()) {
+ return getProtocol() == Im.PROTOCOL_CUSTOM;
+ } else if (that.isProtocolValid()) {
+ return that.getProtocol() == Im.PROTOCOL_CUSTOM;
+ }
+ return true;
+ } else if (getProtocol() != that.getProtocol()) {
+ return false;
+ } else if (getProtocol() == Im.PROTOCOL_CUSTOM
+ && !TextUtils.equals(getCustomProtocol(), that.getCustomProtocol())) {
+ // Check if custom protocols are not the same
+ return false;
+ }
+ return true;
+ }
+}
diff --git a/java/com/android/contacts/common/model/dataitem/NicknameDataItem.java b/java/com/android/contacts/common/model/dataitem/NicknameDataItem.java
new file mode 100644
index 000000000..a448be786
--- /dev/null
+++ b/java/com/android/contacts/common/model/dataitem/NicknameDataItem.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2012 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.common.model.dataitem;
+
+import android.content.ContentValues;
+import android.provider.ContactsContract.CommonDataKinds.Nickname;
+
+/**
+ * Represents a nickname data item, wrapping the columns in {@link
+ * ContactsContract.CommonDataKinds.Nickname}.
+ */
+public class NicknameDataItem extends DataItem {
+
+ public NicknameDataItem(ContentValues values) {
+ super(values);
+ }
+
+ public String getName() {
+ return getContentValues().getAsString(Nickname.NAME);
+ }
+
+ public String getLabel() {
+ return getContentValues().getAsString(Nickname.LABEL);
+ }
+}
diff --git a/java/com/android/contacts/common/model/dataitem/NoteDataItem.java b/java/com/android/contacts/common/model/dataitem/NoteDataItem.java
new file mode 100644
index 000000000..b55ecc3e5
--- /dev/null
+++ b/java/com/android/contacts/common/model/dataitem/NoteDataItem.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2012 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.common.model.dataitem;
+
+import android.content.ContentValues;
+import android.provider.ContactsContract.CommonDataKinds.Note;
+
+/**
+ * Represents a note data item, wrapping the columns in {@link
+ * ContactsContract.CommonDataKinds.Note}.
+ */
+public class NoteDataItem extends DataItem {
+
+ /* package */ NoteDataItem(ContentValues values) {
+ super(values);
+ }
+
+ public String getNote() {
+ return getContentValues().getAsString(Note.NOTE);
+ }
+}
diff --git a/java/com/android/contacts/common/model/dataitem/OrganizationDataItem.java b/java/com/android/contacts/common/model/dataitem/OrganizationDataItem.java
new file mode 100644
index 000000000..b33124838
--- /dev/null
+++ b/java/com/android/contacts/common/model/dataitem/OrganizationDataItem.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright (C) 2012 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.common.model.dataitem;
+
+import android.content.ContentValues;
+import android.provider.ContactsContract;
+import android.provider.ContactsContract.CommonDataKinds.Organization;
+
+/**
+ * Represents an organization data item, wrapping the columns in {@link
+ * ContactsContract.CommonDataKinds.Organization}.
+ */
+public class OrganizationDataItem extends DataItem {
+
+ /* package */ OrganizationDataItem(ContentValues values) {
+ super(values);
+ }
+
+ public String getCompany() {
+ return getContentValues().getAsString(Organization.COMPANY);
+ }
+
+ public String getLabel() {
+ return getContentValues().getAsString(Organization.LABEL);
+ }
+
+ public String getTitle() {
+ return getContentValues().getAsString(Organization.TITLE);
+ }
+
+ public String getDepartment() {
+ return getContentValues().getAsString(Organization.DEPARTMENT);
+ }
+
+ public String getJobDescription() {
+ return getContentValues().getAsString(Organization.JOB_DESCRIPTION);
+ }
+
+ public String getSymbol() {
+ return getContentValues().getAsString(Organization.SYMBOL);
+ }
+
+ public String getPhoneticName() {
+ return getContentValues().getAsString(Organization.PHONETIC_NAME);
+ }
+
+ public String getOfficeLocation() {
+ return getContentValues().getAsString(Organization.OFFICE_LOCATION);
+ }
+}
diff --git a/java/com/android/contacts/common/model/dataitem/PhoneDataItem.java b/java/com/android/contacts/common/model/dataitem/PhoneDataItem.java
new file mode 100644
index 000000000..e1f56456a
--- /dev/null
+++ b/java/com/android/contacts/common/model/dataitem/PhoneDataItem.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright (C) 2012 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.common.model.dataitem;
+
+import android.content.ContentValues;
+import android.content.Context;
+import android.provider.ContactsContract.CommonDataKinds.Phone;
+import com.android.contacts.common.compat.PhoneNumberUtilsCompat;
+
+/**
+ * Represents a phone data item, wrapping the columns in {@link
+ * ContactsContract.CommonDataKinds.Phone}.
+ */
+public class PhoneDataItem extends DataItem {
+
+ public static final String KEY_FORMATTED_PHONE_NUMBER = "formattedPhoneNumber";
+
+ /* package */ PhoneDataItem(ContentValues values) {
+ super(values);
+ }
+
+ public String getNumber() {
+ return getContentValues().getAsString(Phone.NUMBER);
+ }
+
+ /** Returns the normalized phone number in E164 format. */
+ public String getNormalizedNumber() {
+ return getContentValues().getAsString(Phone.NORMALIZED_NUMBER);
+ }
+
+ public String getFormattedPhoneNumber() {
+ return getContentValues().getAsString(KEY_FORMATTED_PHONE_NUMBER);
+ }
+
+ public String getLabel() {
+ return getContentValues().getAsString(Phone.LABEL);
+ }
+
+ public void computeFormattedPhoneNumber(String defaultCountryIso) {
+ final String phoneNumber = getNumber();
+ if (phoneNumber != null) {
+ final String formattedPhoneNumber =
+ PhoneNumberUtilsCompat.formatNumber(
+ phoneNumber, getNormalizedNumber(), defaultCountryIso);
+ getContentValues().put(KEY_FORMATTED_PHONE_NUMBER, formattedPhoneNumber);
+ }
+ }
+
+ /**
+ * Returns the formatted phone number (if already computed using {@link
+ * #computeFormattedPhoneNumber}). Otherwise this method returns the unformatted phone number.
+ */
+ @Override
+ public String buildDataStringForDisplay(Context context, DataKind kind) {
+ final String formatted = getFormattedPhoneNumber();
+ if (formatted != null) {
+ return formatted;
+ } else {
+ return getNumber();
+ }
+ }
+}
diff --git a/java/com/android/contacts/common/model/dataitem/PhotoDataItem.java b/java/com/android/contacts/common/model/dataitem/PhotoDataItem.java
new file mode 100644
index 000000000..0bf7a318b
--- /dev/null
+++ b/java/com/android/contacts/common/model/dataitem/PhotoDataItem.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2012 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.common.model.dataitem;
+
+import android.content.ContentValues;
+import android.provider.ContactsContract;
+import android.provider.ContactsContract.Contacts.Photo;
+
+/**
+ * Represents a photo data item, wrapping the columns in {@link ContactsContract.Contacts.Photo}.
+ */
+public class PhotoDataItem extends DataItem {
+
+ /* package */ PhotoDataItem(ContentValues values) {
+ super(values);
+ }
+
+ public Long getPhotoFileId() {
+ return getContentValues().getAsLong(Photo.PHOTO_FILE_ID);
+ }
+
+ public byte[] getPhoto() {
+ return getContentValues().getAsByteArray(Photo.PHOTO);
+ }
+}
diff --git a/java/com/android/contacts/common/model/dataitem/RelationDataItem.java b/java/com/android/contacts/common/model/dataitem/RelationDataItem.java
new file mode 100644
index 000000000..fdbcbb313
--- /dev/null
+++ b/java/com/android/contacts/common/model/dataitem/RelationDataItem.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2012 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.common.model.dataitem;
+
+import android.content.ContentValues;
+import android.content.Context;
+import android.provider.ContactsContract.CommonDataKinds.Relation;
+import android.text.TextUtils;
+
+/**
+ * Represents a relation data item, wrapping the columns in {@link
+ * ContactsContract.CommonDataKinds.Relation}.
+ */
+public class RelationDataItem extends DataItem {
+
+ /* package */ RelationDataItem(ContentValues values) {
+ super(values);
+ }
+
+ public String getName() {
+ return getContentValues().getAsString(Relation.NAME);
+ }
+
+ public String getLabel() {
+ return getContentValues().getAsString(Relation.LABEL);
+ }
+
+ @Override
+ public boolean shouldCollapseWith(DataItem t, Context context) {
+ if (!(t instanceof RelationDataItem) || mKind == null || t.getDataKind() == null) {
+ return false;
+ }
+ final RelationDataItem that = (RelationDataItem) t;
+ // Relations can have different types (assistant, father) but have the same name
+ if (!TextUtils.equals(getName(), that.getName())) {
+ return false;
+ } else if (!hasKindTypeColumn(mKind) || !that.hasKindTypeColumn(that.getDataKind())) {
+ return hasKindTypeColumn(mKind) == that.hasKindTypeColumn(that.getDataKind());
+ } else if (getKindTypeColumn(mKind) != that.getKindTypeColumn(that.getDataKind())) {
+ return false;
+ } else if (getKindTypeColumn(mKind) == Relation.TYPE_CUSTOM
+ && !TextUtils.equals(getLabel(), that.getLabel())) {
+ // Check if custom types are not the same
+ return false;
+ }
+ return true;
+ }
+}
diff --git a/java/com/android/contacts/common/model/dataitem/SipAddressDataItem.java b/java/com/android/contacts/common/model/dataitem/SipAddressDataItem.java
new file mode 100644
index 000000000..0ca9eae6d
--- /dev/null
+++ b/java/com/android/contacts/common/model/dataitem/SipAddressDataItem.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2012 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.common.model.dataitem;
+
+import android.content.ContentValues;
+import android.provider.ContactsContract;
+import android.provider.ContactsContract.CommonDataKinds.SipAddress;
+
+/**
+ * Represents a sip address data item, wrapping the columns in {@link
+ * ContactsContract.CommonDataKinds.SipAddress}.
+ */
+public class SipAddressDataItem extends DataItem {
+
+ /* package */ SipAddressDataItem(ContentValues values) {
+ super(values);
+ }
+
+ public String getSipAddress() {
+ return getContentValues().getAsString(SipAddress.SIP_ADDRESS);
+ }
+
+ public String getLabel() {
+ return getContentValues().getAsString(SipAddress.LABEL);
+ }
+}
diff --git a/java/com/android/contacts/common/model/dataitem/StructuredNameDataItem.java b/java/com/android/contacts/common/model/dataitem/StructuredNameDataItem.java
new file mode 100644
index 000000000..22bf037f1
--- /dev/null
+++ b/java/com/android/contacts/common/model/dataitem/StructuredNameDataItem.java
@@ -0,0 +1,100 @@
+/*
+ * Copyright (C) 2012 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.common.model.dataitem;
+
+import android.content.ContentValues;
+import android.provider.ContactsContract.CommonDataKinds.StructuredName;
+import android.provider.ContactsContract.Contacts.Data;
+
+/**
+ * Represents a structured name data item, wrapping the columns in {@link
+ * ContactsContract.CommonDataKinds.StructuredName}.
+ */
+public class StructuredNameDataItem extends DataItem {
+
+ public StructuredNameDataItem() {
+ super(new ContentValues());
+ getContentValues().put(Data.MIMETYPE, StructuredName.CONTENT_ITEM_TYPE);
+ }
+
+ /* package */ StructuredNameDataItem(ContentValues values) {
+ super(values);
+ }
+
+ public String getDisplayName() {
+ return getContentValues().getAsString(StructuredName.DISPLAY_NAME);
+ }
+
+ public void setDisplayName(String name) {
+ getContentValues().put(StructuredName.DISPLAY_NAME, name);
+ }
+
+ public String getGivenName() {
+ return getContentValues().getAsString(StructuredName.GIVEN_NAME);
+ }
+
+ public String getFamilyName() {
+ return getContentValues().getAsString(StructuredName.FAMILY_NAME);
+ }
+
+ public String getPrefix() {
+ return getContentValues().getAsString(StructuredName.PREFIX);
+ }
+
+ public String getMiddleName() {
+ return getContentValues().getAsString(StructuredName.MIDDLE_NAME);
+ }
+
+ public String getSuffix() {
+ return getContentValues().getAsString(StructuredName.SUFFIX);
+ }
+
+ public String getPhoneticGivenName() {
+ return getContentValues().getAsString(StructuredName.PHONETIC_GIVEN_NAME);
+ }
+
+ public void setPhoneticGivenName(String name) {
+ getContentValues().put(StructuredName.PHONETIC_GIVEN_NAME, name);
+ }
+
+ public String getPhoneticMiddleName() {
+ return getContentValues().getAsString(StructuredName.PHONETIC_MIDDLE_NAME);
+ }
+
+ public void setPhoneticMiddleName(String name) {
+ getContentValues().put(StructuredName.PHONETIC_MIDDLE_NAME, name);
+ }
+
+ public String getPhoneticFamilyName() {
+ return getContentValues().getAsString(StructuredName.PHONETIC_FAMILY_NAME);
+ }
+
+ public void setPhoneticFamilyName(String name) {
+ getContentValues().put(StructuredName.PHONETIC_FAMILY_NAME, name);
+ }
+
+ public String getFullNameStyle() {
+ return getContentValues().getAsString(StructuredName.FULL_NAME_STYLE);
+ }
+
+ public boolean isSuperPrimary() {
+ final ContentValues contentValues = getContentValues();
+ return contentValues == null || !contentValues.containsKey(StructuredName.IS_SUPER_PRIMARY)
+ ? false
+ : contentValues.getAsBoolean(StructuredName.IS_SUPER_PRIMARY);
+ }
+}
diff --git a/java/com/android/contacts/common/model/dataitem/StructuredPostalDataItem.java b/java/com/android/contacts/common/model/dataitem/StructuredPostalDataItem.java
new file mode 100644
index 000000000..18aae282c
--- /dev/null
+++ b/java/com/android/contacts/common/model/dataitem/StructuredPostalDataItem.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright (C) 2012 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.common.model.dataitem;
+
+import android.content.ContentValues;
+import android.provider.ContactsContract;
+import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
+
+/**
+ * Represents a structured postal data item, wrapping the columns in {@link
+ * ContactsContract.CommonDataKinds.StructuredPostal}.
+ */
+public class StructuredPostalDataItem extends DataItem {
+
+ /* package */ StructuredPostalDataItem(ContentValues values) {
+ super(values);
+ }
+
+ public String getFormattedAddress() {
+ return getContentValues().getAsString(StructuredPostal.FORMATTED_ADDRESS);
+ }
+
+ public String getLabel() {
+ return getContentValues().getAsString(StructuredPostal.LABEL);
+ }
+
+ public String getStreet() {
+ return getContentValues().getAsString(StructuredPostal.STREET);
+ }
+
+ public String getPOBox() {
+ return getContentValues().getAsString(StructuredPostal.POBOX);
+ }
+
+ public String getNeighborhood() {
+ return getContentValues().getAsString(StructuredPostal.NEIGHBORHOOD);
+ }
+
+ public String getCity() {
+ return getContentValues().getAsString(StructuredPostal.CITY);
+ }
+
+ public String getRegion() {
+ return getContentValues().getAsString(StructuredPostal.REGION);
+ }
+
+ public String getPostcode() {
+ return getContentValues().getAsString(StructuredPostal.POSTCODE);
+ }
+
+ public String getCountry() {
+ return getContentValues().getAsString(StructuredPostal.COUNTRY);
+ }
+}
diff --git a/java/com/android/contacts/common/model/dataitem/WebsiteDataItem.java b/java/com/android/contacts/common/model/dataitem/WebsiteDataItem.java
new file mode 100644
index 000000000..b8400ecd1
--- /dev/null
+++ b/java/com/android/contacts/common/model/dataitem/WebsiteDataItem.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2012 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.common.model.dataitem;
+
+import android.content.ContentValues;
+import android.provider.ContactsContract.CommonDataKinds.Website;
+
+/**
+ * Represents a website data item, wrapping the columns in {@link
+ * ContactsContract.CommonDataKinds.Website}.
+ */
+public class WebsiteDataItem extends DataItem {
+
+ /* package */ WebsiteDataItem(ContentValues values) {
+ super(values);
+ }
+
+ public String getUrl() {
+ return getContentValues().getAsString(Website.URL);
+ }
+
+ public String getLabel() {
+ return getContentValues().getAsString(Website.LABEL);
+ }
+}