diff options
Diffstat (limited to 'java/com/android/contacts/common/model')
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); + } +} |