diff options
author | Steve Kondik <steve@cyngn.com> | 2015-03-24 13:36:45 -0700 |
---|---|---|
committer | Steve Kondik <steve@cyngn.com> | 2015-03-25 03:25:09 -0700 |
commit | 90b89eda4c38deeb39a0c961b8149a04d553051b (patch) | |
tree | 2988137a54c868d49cae08a99a86bbf36912ba88 /src/com/android/contacts | |
parent | 516b8b57f2a445afe1b1ef212e9abfa4549a13c7 (diff) | |
parent | 7c27248f86795222adc8b87543eda5c6148610db (diff) | |
download | android_packages_apps_ContactsCommon-90b89eda4c38deeb39a0c961b8149a04d553051b.tar.gz android_packages_apps_ContactsCommon-90b89eda4c38deeb39a0c961b8149a04d553051b.tar.bz2 android_packages_apps_ContactsCommon-90b89eda4c38deeb39a0c961b8149a04d553051b.zip |
Merge branch 'lollipop-mr1-release' of https://android.googlesource.com/platform/packages/apps/ContactsCommon into cm-12.1
Change-Id: I271972eb6db4d6c5cc620a1adb26b1f06beaf1b4
Diffstat (limited to 'src/com/android/contacts')
32 files changed, 722 insertions, 202 deletions
diff --git a/src/com/android/contacts/common/CallUtil.java b/src/com/android/contacts/common/CallUtil.java index 7c491664..3d5f7eab 100644 --- a/src/com/android/contacts/common/CallUtil.java +++ b/src/com/android/contacts/common/CallUtil.java @@ -141,6 +141,13 @@ public class CallUtil { } /** + * A variant of {@link #getCallIntent(android.net.Uri)} for calling Voicemail. + */ + public static Intent getVoicemailIntent() { + return getCallIntent(Uri.fromParts(PhoneAccount.SCHEME_VOICEMAIL, "", null)); + } + + /** * A variant of {@link #getCallIntent(android.net.Uri)} but also accept a call * origin and {@code Account} and {@code VideoCallProfile} state. * For more information about call origin, see comments in Phone package (PhoneApp). diff --git a/src/com/android/contacts/common/ContactsUtils.java b/src/com/android/contacts/common/ContactsUtils.java index 857450d9..a6e0e0e8 100644 --- a/src/com/android/contacts/common/ContactsUtils.java +++ b/src/com/android/contacts/common/ContactsUtils.java @@ -41,6 +41,8 @@ public class ContactsUtils { public static final String SCHEME_MAILTO = "mailto"; public static final String SCHEME_SMSTO = "smsto"; + private static final int DEFAULT_THUMBNAIL_SIZE = 96; + private static int sThumbnailSize = -1; // TODO find a proper place for the canonical version of these @@ -139,14 +141,17 @@ public class ContactsUtils { final Cursor c = context.getContentResolver().query( DisplayPhoto.CONTENT_MAX_DIMENSIONS_URI, new String[] { DisplayPhoto.THUMBNAIL_MAX_DIM }, null, null, null); - try { - c.moveToFirst(); - sThumbnailSize = c.getInt(0); - } finally { - c.close(); + if (c != null) { + try { + if (c.moveToFirst()) { + sThumbnailSize = c.getInt(0); + } + } finally { + c.close(); + } } } - return sThumbnailSize; + return sThumbnailSize != -1 ? sThumbnailSize : DEFAULT_THUMBNAIL_SIZE; } private static Intent getCustomImIntent(ImDataItem im, int protocol) { diff --git a/src/com/android/contacts/common/interactions/ImportExportDialogFragment.java b/src/com/android/contacts/common/interactions/ImportExportDialogFragment.java index 6908b09d..68fb7df4 100644 --- a/src/com/android/contacts/common/interactions/ImportExportDialogFragment.java +++ b/src/com/android/contacts/common/interactions/ImportExportDialogFragment.java @@ -22,6 +22,7 @@ import android.accounts.Account; import android.app.Activity; import android.app.AlertDialog; import android.app.Dialog; +import android.app.DialogFragment; import android.app.FragmentManager; import android.app.ProgressDialog; import android.content.BroadcastReceiver; @@ -52,8 +53,11 @@ import android.provider.ContactsContract.CommonDataKinds.Email; import android.provider.ContactsContract.Data; import android.provider.ContactsContract.CommonDataKinds.StructuredName; import android.provider.Settings; +import android.telephony.PhoneNumberUtils; import android.telephony.TelephonyManager; +import android.telephony.SubscriptionInfo; import android.telephony.SubscriptionManager; +import android.text.TextUtils; import android.util.Log; import android.view.LayoutInflater; import android.view.View; @@ -75,8 +79,9 @@ import com.android.contacts.common.util.AccountSelectionUtil; import com.android.contacts.common.util.AccountsListAdapter.AccountListFilter; import com.android.contacts.common.vcard.ExportVCardActivity; import com.android.contacts.common.vcard.VCardCommonArguments; -import com.android.dialerbind.analytics.AnalyticsDialogFragment; +import com.android.contacts.commonbind.analytics.AnalyticsUtil; +import java.util.Collections; import java.util.List; import java.util.ArrayList; import java.util.Iterator; @@ -84,13 +89,12 @@ import java.util.Iterator; /** * An dialog invoked to import/export contacts. */ -public class ImportExportDialogFragment extends AnalyticsDialogFragment +public class ImportExportDialogFragment extends DialogFragment implements SelectAccountDialogFragment.Listener { public static final String TAG = "ImportExportDialogFragment"; - private static final String SIM_INDEX = "sim_index"; - private static final String KEY_RES_ID = "resourceId"; + private static final String KEY_SUBSCRIPTION_ID = "subscriptionId"; private static final String ARG_CONTACTS_ARE_AVAILABLE = "CONTACTS_ARE_AVAILABLE"; private static int SIM_ID_INVALID = -1; private static int mSelectedSim = SIM_ID_INVALID; @@ -155,6 +159,9 @@ public class ImportExportDialogFragment extends AnalyticsDialogFragment public void showExportToSIMProgressDialog(Activity activity){ mExportThread.showExportProgressDialog(activity); } + + private SubscriptionManager mSubscriptionManager; + /** Preferred way to show this dialog */ public static void show(FragmentManager fragmentManager, boolean contactsAreAvailable, Class callingActivity) { @@ -169,7 +176,7 @@ public class ImportExportDialogFragment extends AnalyticsDialogFragment @Override public void onAttach(Activity activity) { super.onAttach(activity); - sendScreenView(); + AnalyticsUtil.sendScreenView(this); } @Override @@ -184,35 +191,70 @@ public class ImportExportDialogFragment extends AnalyticsDialogFragment VCardCommonArguments.ARG_CALLING_ACTIVITY); // Adapter that shows a list of string resources - mAdapter = new ArrayAdapter<Integer>(getActivity(), + final ArrayAdapter<AdapterEntry> adapter = new ArrayAdapter<AdapterEntry>(getActivity(), R.layout.select_dialog_item) { @Override public View getView(int position, View convertView, ViewGroup parent) { final TextView result = (TextView)(convertView != null ? convertView : dialogInflater.inflate(R.layout.select_dialog_item, parent, false)); - final int resId = getItem(position); - result.setText(resId); + result.setText(getItem(position).mLabel); return result; } }; // Manually call notifyDataSetChanged() to refresh the list. - mAdapter.setNotifyOnChange(false); + adapter.setNotifyOnChange(false); loadData(contactsAreAvailable); + final TelephonyManager manager = + (TelephonyManager) getActivity().getSystemService(Context.TELEPHONY_SERVICE); + + mSubscriptionManager = SubscriptionManager.from(getActivity()); + + if (res.getBoolean(R.bool.config_allow_import_from_sdcard)) { + adapter.add(new AdapterEntry(getString(R.string.import_from_sdcard), + R.string.import_from_sdcard)); + } + if (manager != null && res.getBoolean(R.bool.config_allow_sim_import)) { + final List<SubscriptionInfo> subInfoRecords = + mSubscriptionManager.getActiveSubscriptionInfoList(); + if (subInfoRecords != null) { + if (subInfoRecords.size() == 1) { + adapter.add(new AdapterEntry(getString(R.string.import_from_sim), + R.string.import_from_sim, subInfoRecords.get(0).getSubscriptionId())); + } else { + for (SubscriptionInfo record : subInfoRecords) { + adapter.add(new AdapterEntry(getSubDescription(record), + R.string.import_from_sim, record.getSubscriptionId())); + } + } + } + } + if (res.getBoolean(R.bool.config_allow_export_to_sdcard)) { + if (contactsAreAvailable) { + adapter.add(new AdapterEntry(getString(R.string.export_to_sdcard), + R.string.export_to_sdcard)); + } + } + if (res.getBoolean(R.bool.config_allow_share_visible_contacts)) { + if (contactsAreAvailable) { + adapter.add(new AdapterEntry(getString(R.string.share_visible_contacts), + R.string.share_visible_contacts)); + } + } + final DialogInterface.OnClickListener clickListener = new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { - final int resId = mAdapter.getItem(which); + boolean dismissDialog; + final int resId = adapter.getItem(which).mChoiceResourceId; switch (resId) { - case R.string.import_from_sim: { - handleImportFromSimRequest(resId); - break; - } + case R.string.import_from_sim: case R.string.import_from_sdcard: { - handleImportRequest(resId); + dismissDialog = handleImportRequest(resId, + adapter.getItem(which).mSubscriptionId); break; } case R.string.export_to_sim: { @@ -308,9 +350,10 @@ public class ImportExportDialogFragment extends AnalyticsDialogFragment * * @return {@code true} if the dialog show be closed. {@code false} otherwise. */ - private boolean handleImportRequest(int resId) { - // There are two possibilities: - // - one or more than one accounts -> ask the user (user can select phone-local also) + private boolean handleImportRequest(int resId, int subscriptionId) { + // There are three possibilities: + // - more than one accounts -> ask the user + // - just one account -> use the account without asking the user // - no account -> use phone-local storage without asking the user final AccountTypeManager accountTypes = AccountTypeManager.getInstance(mActivity); final List<AccountWithDataSet> accountList = accountTypes.getAccounts(true); @@ -319,6 +362,7 @@ public class ImportExportDialogFragment extends AnalyticsDialogFragment // Send over to the account selector final Bundle args = new Bundle(); args.putInt(KEY_RES_ID, resId); + args.putInt(KEY_SUBSCRIPTION_ID, subscriptionId); SelectAccountDialogFragment.show( mActivity.getFragmentManager(), this, R.string.dialog_new_contact_account, @@ -330,7 +374,8 @@ public class ImportExportDialogFragment extends AnalyticsDialogFragment return false; } - AccountSelectionUtil.doImport(mActivity, resId, null); + AccountSelectionUtil.doImport(getActivity(), resId, + (size == 1 ? accountList.get(0) : null), subscriptionId); return true; // Close the dialog. } @@ -339,7 +384,8 @@ public class ImportExportDialogFragment extends AnalyticsDialogFragment */ @Override public void onAccountChosen(AccountWithDataSet account, Bundle extraArgs) { - AccountSelectionUtil.doImport(mActivity, extraArgs.getInt(KEY_RES_ID), account); + AccountSelectionUtil.doImport(getActivity(), extraArgs.getInt(KEY_RES_ID), + account, extraArgs.getInt(KEY_SUBSCRIPTION_ID)); // At this point the dialog is still showing (which is why we can use getActivity() above) // So close it. @@ -375,7 +421,7 @@ public class ImportExportDialogFragment extends AnalyticsDialogFragment if (which >= 0) { AccountSelectionUtil.setImportSubscription(which); } else if (which == DialogInterface.BUTTON_POSITIVE) { - handleImportRequest(R.string.import_from_sim); + handleImportRequest(R.string.import_from_sim, which); } } } @@ -750,20 +796,6 @@ public class ImportExportDialogFragment extends AnalyticsDialogFragment .setSingleChoiceItems(items, 0, listener).create().show(); } - private void handleImportFromSimRequest(int Id) { - if (TelephonyManager.getDefault().isMultiSimEnabled()) { - if (MoreContactUtils.getEnabledSimCount() > 1) { - displayImportExportDialog(R.string.import_from_sim_select - ,null); - } else { - AccountSelectionUtil.setImportSubscription(getEnabledIccCard()); - handleImportRequest(Id); - } - } else { - handleImportRequest(Id); - } - } - private void handleExportToSimRequest(int Id) { if (MoreContactUtils.getEnabledSimCount() >1) { //has two enalbed sim cards, prompt dialog to select one @@ -796,4 +828,32 @@ public class ImportExportDialogFragment extends AnalyticsDialogFragment } return SimContactsConstants.SUB_1; } + + private CharSequence getSubDescription(SubscriptionInfo record) { + CharSequence name = record.getDisplayName(); + if (TextUtils.isEmpty(record.getNumber())) { + // Don't include the phone number in the description, since we don't know the number. + return getString(R.string.import_from_sim_summary_no_number, name); + } + return TextUtils.expandTemplate( + getString(R.string.import_from_sim_summary), + name, + PhoneNumberUtils.ttsSpanAsPhoneNumber(record.getNumber())); + } + + private static class AdapterEntry { + public final CharSequence mLabel; + public final int mChoiceResourceId; + public final int mSubscriptionId; + + public AdapterEntry(CharSequence label, int resId, int subId) { + mLabel = label; + mChoiceResourceId = resId; + mSubscriptionId = subId; + } + + public AdapterEntry(String label, int resId) { + this(label, resId, SubscriptionManager.INVALID_SUBSCRIPTION_ID); + } + } } diff --git a/src/com/android/contacts/common/list/ContactEntryListFragment.java b/src/com/android/contacts/common/list/ContactEntryListFragment.java index 1819c0c8..c7204931 100755 --- a/src/com/android/contacts/common/list/ContactEntryListFragment.java +++ b/src/com/android/contacts/common/list/ContactEntryListFragment.java @@ -17,6 +17,7 @@ package com.android.contacts.common.list; import android.app.Activity; +import android.app.Fragment; import android.app.LoaderManager; import android.app.LoaderManager.LoaderCallbacks; import android.content.BroadcastReceiver; @@ -25,7 +26,6 @@ import android.content.CursorLoader; import android.content.Intent; import android.content.IntentFilter; import android.content.Loader; -import android.content.res.Resources; import android.database.Cursor; import android.os.Bundle; import android.os.Handler; @@ -47,12 +47,10 @@ import android.widget.AdapterView.OnItemClickListener; import android.widget.ListView; import com.android.common.widget.CompositeCursorAdapter.Partition; -import com.android.contacts.common.R; import com.android.contacts.common.ContactPhotoManager; import com.android.contacts.common.preference.ContactsPreferences; import com.android.contacts.common.util.ContactListViewUtils; import com.android.contacts.common.util.SchedulingUtils; -import com.android.dialerbind.analytics.AnalyticsFragment; import com.android.internal.telephony.TelephonyIntents; import java.util.Locale; @@ -61,7 +59,7 @@ import java.util.Locale; * Common base class for various contact-related list fragments. */ public abstract class ContactEntryListFragment<T extends ContactEntryListAdapter> - extends AnalyticsFragment + extends Fragment implements OnItemClickListener, OnScrollListener, OnFocusChangeListener, OnTouchListener, LoaderCallbacks<Cursor> { private static final String TAG = "ContactEntryListFragment"; diff --git a/src/com/android/contacts/common/list/ContactListAdapter.java b/src/com/android/contacts/common/list/ContactListAdapter.java index b987e4d8..ea2d8539 100755 --- a/src/com/android/contacts/common/list/ContactListAdapter.java +++ b/src/com/android/contacts/common/list/ContactListAdapter.java @@ -407,8 +407,7 @@ public abstract class ContactListAdapter extends ContactEntryListAdapter { super.changeCursor(partitionIndex, cursor); // Check if a profile exists - if (cursor != null && cursor.getCount() > 0) { - cursor.moveToFirst(); + if (cursor != null && cursor.moveToFirst()) { setProfileExists(cursor.getInt(ContactQuery.CONTACT_IS_USER_PROFILE) == 1); } } diff --git a/src/com/android/contacts/common/list/ContactListItemView.java b/src/com/android/contacts/common/list/ContactListItemView.java index e6369977..dd869b0c 100755 --- a/src/com/android/contacts/common/list/ContactListItemView.java +++ b/src/com/android/contacts/common/list/ContactListItemView.java @@ -50,6 +50,7 @@ import com.android.contacts.common.ContactPresenceIconUtil; import com.android.contacts.common.ContactStatusUtil; import com.android.contacts.common.R; import com.android.contacts.common.format.TextHighlighter; +import com.android.contacts.common.util.ContactDisplayUtils; import com.android.contacts.common.util.SearchUtil; import com.android.contacts.common.util.ViewUtil; import com.android.contacts.common.widget.CheckableImageView; @@ -1031,6 +1032,7 @@ public class ContactListItemView extends ViewGroup mPhoneticNameTextView.setSingleLine(true); mPhoneticNameTextView.setEllipsize(getTextEllipsis()); mPhoneticNameTextView.setTextAppearance(getContext(), android.R.style.TextAppearance_Small); + mPhoneticNameTextView.setTextAlignment(View.TEXT_ALIGNMENT_VIEW_START); mPhoneticNameTextView.setTypeface(mPhoneticNameTextView.getTypeface(), Typeface.BOLD); mPhoneticNameTextView.setActivated(isActivated()); mPhoneticNameTextView.setId(R.id.cliv_phoneticname_textview); @@ -1156,6 +1158,7 @@ public class ContactListItemView extends ViewGroup mDataView.setSingleLine(true); mDataView.setEllipsize(getTextEllipsis()); mDataView.setTextAppearance(getContext(), R.style.TextAppearanceSmall); + mDataView.setTextAlignment(View.TEXT_ALIGNMENT_VIEW_START); mDataView.setActivated(isActivated()); mDataView.setId(R.id.cliv_data_view); mDataView.setElegantTextHeight(false); @@ -1175,6 +1178,13 @@ public class ContactListItemView extends ViewGroup } else { mTextHighlighter.setPrefixText(getSnippetView(), text, mHighlightedPrefix); mSnippetView.setVisibility(VISIBLE); + if (ContactDisplayUtils.isPossiblePhoneNumber(text)) { + // Give the text-to-speech engine a hint that it's a phone number + mSnippetView.setContentDescription( + ContactDisplayUtils.getTelephoneTtsSpannable(text)); + } else { + mSnippetView.setContentDescription(null); + } } } @@ -1187,6 +1197,7 @@ public class ContactListItemView extends ViewGroup mSnippetView.setSingleLine(true); mSnippetView.setEllipsize(getTextEllipsis()); mSnippetView.setTextAppearance(getContext(), android.R.style.TextAppearance_Small); + mSnippetView.setTextAlignment(View.TEXT_ALIGNMENT_VIEW_START); mSnippetView.setActivated(isActivated()); addView(mSnippetView); } @@ -1300,6 +1311,14 @@ public class ContactListItemView extends ViewGroup name = mUnknownNameText; } setMarqueeText(getNameTextView(), name); + + if (ContactDisplayUtils.isPossiblePhoneNumber(name)) { + // Give the text-to-speech engine a hint that it's a phone number + mNameTextView.setContentDescription( + ContactDisplayUtils.getTelephoneTtsSpannable(name.toString())); + } else { + mNameTextView.setContentDescription(null); + } } public void hideDisplayName() { diff --git a/src/com/android/contacts/common/list/ContactTileAdapter.java b/src/com/android/contacts/common/list/ContactTileAdapter.java index 70bdebf2..35f9f185 100644 --- a/src/com/android/contacts/common/list/ContactTileAdapter.java +++ b/src/com/android/contacts/common/list/ContactTileAdapter.java @@ -583,7 +583,8 @@ public class ContactTileAdapter extends BaseAdapter { // Just line up children horizontally. for (int i = 0; i < count; i++) { - final View child = getChildAt(i); + final int rtlAdjustedIndex = isLayoutRtl() ? count - i - 1 : i; + final View child = getChildAt(rtlAdjustedIndex); // Note MeasuredWidth includes the padding. final int childWidth = child.getMeasuredWidth(); diff --git a/src/com/android/contacts/common/list/CustomContactListFilterActivity.java b/src/com/android/contacts/common/list/CustomContactListFilterActivity.java index b0bf9c3d..5066b294 100755 --- a/src/com/android/contacts/common/list/CustomContactListFilterActivity.java +++ b/src/com/android/contacts/common/list/CustomContactListFilterActivity.java @@ -148,9 +148,12 @@ public class CustomContactListFilterActivity extends Activity if (account.dataSet != null) { groupsUri.appendQueryParameter(Groups.DATA_SET, account.dataSet).build(); } + final Cursor cursor = resolver.query(groupsUri.build(), null, null, null, null); + if (cursor == null) { + continue; + } android.content.EntityIterator iterator = - ContactsContract.Groups.newEntityIterator(resolver.query( - groupsUri.build(), null, null, null, null)); + ContactsContract.Groups.newEntityIterator(cursor); try { boolean hasGroups = false; diff --git a/src/com/android/contacts/common/list/PhoneNumberPickerFragment.java b/src/com/android/contacts/common/list/PhoneNumberPickerFragment.java index 5f850933..06a4f50c 100644 --- a/src/com/android/contacts/common/list/PhoneNumberPickerFragment.java +++ b/src/com/android/contacts/common/list/PhoneNumberPickerFragment.java @@ -15,7 +15,6 @@ */ package com.android.contacts.common.list; -import android.content.CursorLoader; import android.content.Intent; import android.content.Loader; import android.database.Cursor; diff --git a/src/com/android/contacts/common/list/ProfileAndContactsLoader.java b/src/com/android/contacts/common/list/ProfileAndContactsLoader.java index c19737d9..698ef96f 100644 --- a/src/com/android/contacts/common/list/ProfileAndContactsLoader.java +++ b/src/com/android/contacts/common/list/ProfileAndContactsLoader.java @@ -61,8 +61,8 @@ public class ProfileAndContactsLoader extends CursorLoader { Cursor cursor = null; try { cursor = super.loadInBackground(); - } catch (NullPointerException e) { - // Ignore NPEs thrown by providers + } catch (NullPointerException | SecurityException e) { + // Ignore NPEs and SecurityExceptions thrown by providers } final Cursor contactsCursor = cursor; cursors.add(contactsCursor); diff --git a/src/com/android/contacts/common/list/ViewPagerTabs.java b/src/com/android/contacts/common/list/ViewPagerTabs.java index ec95de6f..006d6321 100644 --- a/src/com/android/contacts/common/list/ViewPagerTabs.java +++ b/src/com/android/contacts/common/list/ViewPagerTabs.java @@ -198,7 +198,12 @@ public class ViewPagerTabs extends HorizontalScrollView implements ViewPager.OnP @Override public void onPageSelected(int position) { position = getRtlPosition(position); - if (mPrevSelected >= 0) { + int tabStripChildCount = mTabStrip.getChildCount(); + if ((tabStripChildCount == 0) || (position < 0) || (position >= tabStripChildCount)) { + return; + } + + if (mPrevSelected >= 0 && mPrevSelected < tabStripChildCount) { mTabStrip.getChildAt(mPrevSelected).setSelected(false); } final View selectedChild = mTabStrip.getChildAt(position); diff --git a/src/com/android/contacts/common/location/UpdateCountryService.java b/src/com/android/contacts/common/location/UpdateCountryService.java index e339306f..9403187e 100644 --- a/src/com/android/contacts/common/location/UpdateCountryService.java +++ b/src/com/android/contacts/common/location/UpdateCountryService.java @@ -38,6 +38,10 @@ public class UpdateCountryService extends IntentService { @Override protected void onHandleIntent(Intent intent) { + if (intent == null) { + Log.d(TAG, "onHandleIntent: could not handle null intent"); + return; + } if (ACTION_UPDATE_COUNTRY.equals(intent.getAction())) { final Location location = (Location) intent.getParcelableExtra(KEY_INTENT_LOCATION); final String country = getCountryFromLocation(getApplicationContext(), location); diff --git a/src/com/android/contacts/common/model/ContactLoader.java b/src/com/android/contacts/common/model/ContactLoader.java index bea72107..59ab292e 100644 --- a/src/com/android/contacts/common/model/ContactLoader.java +++ b/src/com/android/contacts/common/model/ContactLoader.java @@ -63,9 +63,11 @@ 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; /** @@ -773,6 +775,34 @@ public class ContactLoader extends AsyncTaskLoader<Contact> { } } + static private 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); + } + } + /** * Loads groups meta-data for all groups associated with all constituent raw contacts' * accounts. @@ -780,11 +810,15 @@ public class ContactLoader extends AsyncTaskLoader<Contact> { 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(); - if (accountName != null && accountType != null) { + 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 "); } diff --git a/src/com/android/contacts/common/model/account/BaseAccountType.java b/src/com/android/contacts/common/model/account/BaseAccountType.java index 8b4c955c..1ead103f 100644 --- a/src/com/android/contacts/common/model/account/BaseAccountType.java +++ b/src/com/android/contacts/common/model/account/BaseAccountType.java @@ -99,20 +99,20 @@ public abstract class BaseAccountType extends AccountType { static final String TYPE = "type"; } - private interface Weight { + protected interface Weight { static final int NONE = -1; - static final int ORGANIZATION = 5; static final int PHONE = 10; static final int EMAIL = 15; - static final int IM = 20; static final int STRUCTURED_POSTAL = 25; - static final int NOTE = 110; - static final int NICKNAME = 115; - static final int WEBSITE = 120; - static final int SIP_ADDRESS = 130; - static final int EVENT = 150; - static final int RELATIONSHIP = 160; - static final int GROUP_MEMBERSHIP = 999; + static final int NICKNAME = 111; + static final int EVENT = 120; + static final int ORGANIZATION = 125; + static final int NOTE = 130; + static final int IM = 140; + static final int SIP_ADDRESS = 145; + static final int GROUP_MEMBERSHIP = 150; + static final int WEBSITE = 160; + static final int RELATIONSHIP = 999; } public BaseAccountType() { @@ -148,7 +148,7 @@ public abstract class BaseAccountType extends AccountType { protected DataKind addDataKindStructuredName(Context context) throws DefinitionException { DataKind kind = addKind(new DataKind(StructuredName.CONTENT_ITEM_TYPE, - R.string.nameLabelsGroup, -1, true)); + R.string.nameLabelsGroup, Weight.NONE, true)); kind.actionHeader = new SimpleInflater(R.string.nameLabelsGroup); kind.actionBody = new SimpleInflater(Nickname.NAME); kind.typeOverallMax = 1; @@ -178,7 +178,7 @@ public abstract class BaseAccountType extends AccountType { protected DataKind addDataKindDisplayName(Context context) throws DefinitionException { DataKind kind = addKind(new DataKind(DataKind.PSEUDO_MIME_TYPE_DISPLAY_NAME, - R.string.nameLabelsGroup, -1, true)); + R.string.nameLabelsGroup, Weight.NONE, true)); kind.actionHeader = new SimpleInflater(R.string.nameLabelsGroup); kind.actionBody = new SimpleInflater(Nickname.NAME); kind.typeOverallMax = 1; @@ -219,7 +219,7 @@ public abstract class BaseAccountType extends AccountType { protected DataKind addDataKindPhoneticName(Context context) throws DefinitionException { DataKind kind = addKind(new DataKind(DataKind.PSEUDO_MIME_TYPE_PHONETIC_NAME, - R.string.name_phonetic, -1, true)); + R.string.name_phonetic, Weight.NONE, true)); kind.actionHeader = new SimpleInflater(R.string.nameLabelsGroup); kind.actionBody = new SimpleInflater(Nickname.NAME); kind.typeOverallMax = 1; @@ -239,7 +239,7 @@ public abstract class BaseAccountType extends AccountType { protected DataKind addDataKindNickname(Context context) throws DefinitionException { DataKind kind = addKind(new DataKind(Nickname.CONTENT_ITEM_TYPE, - R.string.nicknameLabelsGroup, 115, true)); + R.string.nicknameLabelsGroup, Weight.NICKNAME, true)); kind.typeOverallMax = 1; kind.actionHeader = new SimpleInflater(R.string.nicknameLabelsGroup); kind.actionBody = new SimpleInflater(Nickname.NAME); @@ -255,7 +255,7 @@ public abstract class BaseAccountType extends AccountType { protected DataKind addDataKindPhone(Context context) throws DefinitionException { DataKind kind = addKind(new DataKind(Phone.CONTENT_ITEM_TYPE, R.string.phoneLabelsGroup, - 10, true)); + Weight.PHONE, true)); kind.iconAltRes = R.drawable.ic_text_holo_light; kind.iconAltDescriptionRes = R.string.sms; kind.actionHeader = new PhoneActionInflater(); @@ -294,7 +294,7 @@ public abstract class BaseAccountType extends AccountType { protected DataKind addDataKindEmail(Context context) throws DefinitionException { DataKind kind = addKind(new DataKind(Email.CONTENT_ITEM_TYPE, R.string.emailLabelsGroup, - 15, true)); + Weight.EMAIL, true)); kind.actionHeader = new EmailActionInflater(); kind.actionBody = new SimpleInflater(Email.DATA); kind.typeColumn = Email.TYPE; @@ -314,7 +314,7 @@ public abstract class BaseAccountType extends AccountType { protected DataKind addDataKindStructuredPostal(Context context) throws DefinitionException { DataKind kind = addKind(new DataKind(StructuredPostal.CONTENT_ITEM_TYPE, - R.string.postalLabelsGroup, 25, true)); + R.string.postalLabelsGroup, Weight.STRUCTURED_POSTAL, true)); kind.actionHeader = new PostalActionInflater(); kind.actionBody = new SimpleInflater(StructuredPostal.FORMATTED_ADDRESS); kind.typeColumn = StructuredPostal.TYPE; @@ -336,8 +336,8 @@ public abstract class BaseAccountType extends AccountType { } protected DataKind addDataKindIm(Context context) throws DefinitionException { - DataKind kind = addKind(new DataKind(Im.CONTENT_ITEM_TYPE, R.string.imLabelsGroup, 20, - true)); + 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); @@ -368,7 +368,7 @@ public abstract class BaseAccountType extends AccountType { protected DataKind addDataKindOrganization(Context context) throws DefinitionException { DataKind kind = addKind(new DataKind(Organization.CONTENT_ITEM_TYPE, - R.string.organizationLabelsGroup, 5, true)); + R.string.organizationLabelsGroup, Weight.ORGANIZATION, true)); kind.actionHeader = new SimpleInflater(R.string.organizationLabelsGroup); kind.actionBody = ORGANIZATION_BODY_INFLATER; kind.typeOverallMax = 1; @@ -383,7 +383,7 @@ public abstract class BaseAccountType extends AccountType { } protected DataKind addDataKindPhoto(Context context) throws DefinitionException { - DataKind kind = addKind(new DataKind(Photo.CONTENT_ITEM_TYPE, -1, -1, true)); + DataKind kind = addKind(new DataKind(Photo.CONTENT_ITEM_TYPE, -1, Weight.NONE, true)); kind.typeOverallMax = 1; kind.fieldList = Lists.newArrayList(); kind.fieldList.add(new EditField(Photo.PHOTO, -1, -1)); @@ -391,8 +391,8 @@ public abstract class BaseAccountType extends AccountType { } protected DataKind addDataKindNote(Context context) throws DefinitionException { - DataKind kind = addKind(new DataKind(Note.CONTENT_ITEM_TYPE, R.string.label_notes, 110, - true)); + 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); @@ -406,7 +406,7 @@ public abstract class BaseAccountType extends AccountType { protected DataKind addDataKindWebsite(Context context) throws DefinitionException { DataKind kind = addKind(new DataKind(Website.CONTENT_ITEM_TYPE, - R.string.websiteLabelsGroup, 120, true)); + R.string.websiteLabelsGroup, Weight.WEBSITE, true)); kind.actionHeader = new SimpleInflater(R.string.websiteLabelsGroup); kind.actionBody = new SimpleInflater(Website.URL); kind.defaultValues = new ContentValues(); @@ -420,7 +420,7 @@ public abstract class BaseAccountType extends AccountType { protected DataKind addDataKindSipAddress(Context context) throws DefinitionException { DataKind kind = addKind(new DataKind(SipAddress.CONTENT_ITEM_TYPE, - R.string.label_sip_address, 130, true)); + R.string.label_sip_address, Weight.SIP_ADDRESS, true)); kind.typeOverallMax = 1; kind.actionHeader = new SimpleInflater(R.string.label_sip_address); @@ -434,7 +434,7 @@ public abstract class BaseAccountType extends AccountType { protected DataKind addDataKindGroupMembership(Context context) throws DefinitionException { DataKind kind = addKind(new DataKind(GroupMembership.CONTENT_ITEM_TYPE, - R.string.groupsLabel, 999, true)); + R.string.groupsLabel, Weight.GROUP_MEMBERSHIP, true)); kind.actionHeader = new SimpleInflater(R.string.label_groups); kind.actionBody = new SimpleInflater(GroupMembership.GROUP_ROW_ID); diff --git a/src/com/android/contacts/common/model/account/ExchangeAccountType.java b/src/com/android/contacts/common/model/account/ExchangeAccountType.java index 04b5263a..7020836d 100644 --- a/src/com/android/contacts/common/model/account/ExchangeAccountType.java +++ b/src/com/android/contacts/common/model/account/ExchangeAccountType.java @@ -80,7 +80,7 @@ public class ExchangeAccountType extends BaseAccountType { @Override protected DataKind addDataKindStructuredName(Context context) throws DefinitionException { DataKind kind = addKind(new DataKind(StructuredName.CONTENT_ITEM_TYPE, - R.string.nameLabelsGroup, -1, true)); + R.string.nameLabelsGroup, Weight.NONE, true)); kind.actionHeader = new SimpleInflater(R.string.nameLabelsGroup); kind.actionBody = new SimpleInflater(Nickname.NAME); @@ -109,7 +109,7 @@ public class ExchangeAccountType extends BaseAccountType { @Override protected DataKind addDataKindDisplayName(Context context) throws DefinitionException { DataKind kind = addKind(new DataKind(DataKind.PSEUDO_MIME_TYPE_DISPLAY_NAME, - R.string.nameLabelsGroup, -1, true)); + R.string.nameLabelsGroup, Weight.NONE, true)); boolean displayOrderPrimary = context.getResources().getBoolean(R.bool.config_editor_field_order_primary); @@ -142,7 +142,7 @@ public class ExchangeAccountType extends BaseAccountType { @Override protected DataKind addDataKindPhoneticName(Context context) throws DefinitionException { DataKind kind = addKind(new DataKind(DataKind.PSEUDO_MIME_TYPE_PHONETIC_NAME, - R.string.name_phonetic, -1, true)); + R.string.name_phonetic, Weight.NONE, true)); kind.actionHeader = new SimpleInflater(R.string.nameLabelsGroup); kind.actionBody = new SimpleInflater(Nickname.NAME); @@ -307,7 +307,7 @@ public class ExchangeAccountType extends BaseAccountType { protected DataKind addDataKindEvent(Context context) throws DefinitionException { DataKind kind = addKind(new DataKind(Event.CONTENT_ITEM_TYPE, R.string.eventLabelsGroup, - 150, true)); + Weight.EVENT, true)); kind.actionHeader = new EventActionInflater(); kind.actionBody = new SimpleInflater(Event.START_DATE); diff --git a/src/com/android/contacts/common/model/account/ExternalAccountType.java b/src/com/android/contacts/common/model/account/ExternalAccountType.java index e4cef522..53089b84 100644 --- a/src/com/android/contacts/common/model/account/ExternalAccountType.java +++ b/src/com/android/contacts/common/model/account/ExternalAccountType.java @@ -17,9 +17,10 @@ package com.android.contacts.common.model.account; import android.content.Context; -import android.content.pm.PackageInfo; +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; @@ -48,12 +49,15 @@ import java.util.List; 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". * * 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. + * 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", @@ -114,15 +118,9 @@ public class ExternalAccountType extends BaseAccountType { this.resourcePackageName = packageName; this.syncAdapterPackageName = packageName; - final PackageManager pm = context.getPackageManager(); final XmlResourceParser parser; if (injectedMetadata == null) { - try { - parser = loadContactsXml(context, packageName); - } catch (NameNotFoundException e1) { - // If the package name is not found, we can't initialize this account type. - return; - } + parser = loadContactsXml(context, packageName); } else { parser = injectedMetadata; } @@ -181,35 +179,41 @@ public class ExternalAccountType extends BaseAccountType { /** * Returns the CONTACTS_STRUCTURE metadata (aka "contacts.xml") in the given apk package. * - * Unfortunately, there's no public way to determine which service defines a sync service for - * which account type, so this method looks through all services in the package, and just - * returns the first CONTACTS_STRUCTURE metadata defined in any of them. + * 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. * * Returns {@code null} if the package has no CONTACTS_STRUCTURE metadata. In this case * the account type *will* be initialized with minimal configuration. - * - * On the other hand, if the package is not found, it throws a {@link NameNotFoundException}, - * in which case the account type will *not* be initialized. */ - private XmlResourceParser loadContactsXml(Context context, String resPackageName) - throws NameNotFoundException { + public static XmlResourceParser loadContactsXml(Context context, String resPackageName) { final PackageManager pm = context.getPackageManager(); - PackageInfo packageInfo = pm.getPackageInfo(resPackageName, - PackageManager.GET_SERVICES|PackageManager.GET_META_DATA); - for (ServiceInfo serviceInfo : packageInfo.services) { - 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)); + 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; } - return parser; } } } + // Package was found, but that doesn't contain the CONTACTS_STRUCTURE metadata. return null; } diff --git a/src/com/android/contacts/common/model/account/GoogleAccountType.java b/src/com/android/contacts/common/model/account/GoogleAccountType.java index 8705ae34..68771874 100644 --- a/src/com/android/contacts/common/model/account/GoogleAccountType.java +++ b/src/com/android/contacts/common/model/account/GoogleAccountType.java @@ -122,7 +122,7 @@ public class GoogleAccountType extends BaseAccountType { private DataKind addDataKindRelation(Context context) throws DefinitionException { DataKind kind = addKind(new DataKind(Relation.CONTENT_ITEM_TYPE, - R.string.relationLabelsGroup, 160, true)); + R.string.relationLabelsGroup, Weight.RELATIONSHIP, true)); kind.actionHeader = new RelationActionInflater(); kind.actionBody = new SimpleInflater(Relation.NAME); @@ -157,7 +157,7 @@ public class GoogleAccountType extends BaseAccountType { private DataKind addDataKindEvent(Context context) throws DefinitionException { DataKind kind = addKind(new DataKind(Event.CONTENT_ITEM_TYPE, - R.string.eventLabelsGroup, 150, true)); + R.string.eventLabelsGroup, Weight.EVENT, true)); kind.actionHeader = new EventActionInflater(); kind.actionBody = new SimpleInflater(Event.START_DATE); diff --git a/src/com/android/contacts/common/model/dataitem/EventDataItem.java b/src/com/android/contacts/common/model/dataitem/EventDataItem.java index aae00e97..5096fea2 100644 --- a/src/com/android/contacts/common/model/dataitem/EventDataItem.java +++ b/src/com/android/contacts/common/model/dataitem/EventDataItem.java @@ -20,6 +20,7 @@ import android.content.ContentValues; import android.content.Context; import android.provider.ContactsContract; import android.provider.ContactsContract.CommonDataKinds.Event; +import android.text.TextUtils; /** * Represents an event data item, wrapping the columns in @@ -46,12 +47,14 @@ public class EventDataItem extends DataItem { } final EventDataItem that = (EventDataItem) t; // Events can be different (anniversary, birthday) but have the same start date - if (!getStartDate().equals(that.getStartDate())) { + 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 && - !getLabel().equals(that.getLabel())) { + !TextUtils.equals(getLabel(), that.getLabel())) { // Check if custom types are not the same return false; } diff --git a/src/com/android/contacts/common/model/dataitem/ImDataItem.java b/src/com/android/contacts/common/model/dataitem/ImDataItem.java index d1af2462..f89e5c6a 100644 --- a/src/com/android/contacts/common/model/dataitem/ImDataItem.java +++ b/src/com/android/contacts/common/model/dataitem/ImDataItem.java @@ -21,6 +21,7 @@ import android.content.Context; import android.provider.ContactsContract; 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 @@ -91,21 +92,21 @@ public class ImDataItem extends DataItem { // 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() && - getProtocol() != that.getProtocol()) { + } 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 (isProtocolValid() && that.isProtocolValid() && - getProtocol() == Im.PROTOCOL_CUSTOM && - !getCustomProtocol().equals(that.getCustomProtocol())) { + } else if (getProtocol() == Im.PROTOCOL_CUSTOM && + !TextUtils.equals(getCustomProtocol(), that.getCustomProtocol())) { // Check if custom protocols are not the same return false; - } else if ((isProtocolValid() && !that.isProtocolValid() && - getProtocol() != Im.PROTOCOL_CUSTOM) || - (that.isProtocolValid() && !isProtocolValid() && - that.getProtocol() != Im.PROTOCOL_CUSTOM)) { - // 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 - return false; } return true; } diff --git a/src/com/android/contacts/common/model/dataitem/RelationDataItem.java b/src/com/android/contacts/common/model/dataitem/RelationDataItem.java index 1ba3fcfb..9e883fef 100644 --- a/src/com/android/contacts/common/model/dataitem/RelationDataItem.java +++ b/src/com/android/contacts/common/model/dataitem/RelationDataItem.java @@ -20,6 +20,7 @@ import android.content.ContentValues; import android.content.Context; import android.provider.ContactsContract; import android.provider.ContactsContract.CommonDataKinds.Relation; +import android.text.TextUtils; /** * Represents a relation data item, wrapping the columns in @@ -46,12 +47,14 @@ public class RelationDataItem extends DataItem { } final RelationDataItem that = (RelationDataItem) t; // Relations can have different types (assistant, father) but have the same name - if (!getName().equals(that.getName())) { + 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 && - !getLabel().equals(that.getLabel())) { + !TextUtils.equals(getLabel(), that.getLabel())) { // Check if custom types are not the same return false; } diff --git a/src/com/android/contacts/common/util/AccountSelectionUtil.java b/src/com/android/contacts/common/util/AccountSelectionUtil.java index 2ea53f11..ed77da22 100644 --- a/src/com/android/contacts/common/util/AccountSelectionUtil.java +++ b/src/com/android/contacts/common/util/AccountSelectionUtil.java @@ -24,6 +24,7 @@ import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.net.Uri; +import android.telephony.SubscriptionManager; import android.telephony.TelephonyManager; import android.util.Log; import android.view.ContextThemeWrapper; @@ -53,7 +54,6 @@ public class AccountSelectionUtil { public static boolean mVCardShare = false; private static int SIM_ID_INVALID = -1; private static int mSelectedSim = SIM_ID_INVALID; - private static final String SIM_INDEX = "sim_index"; // Constant value to know option is import from all SIM's private static int IMPORT_FROM_ALL = 8; @@ -66,22 +66,31 @@ public class AccountSelectionUtil { final private Context mContext; final private int mResId; + final private int mSubscriptionId; protected List<AccountWithDataSet> mAccountList; public AccountSelectedListener(Context context, List<AccountWithDataSet> accountList, - int resId) { + int resId, int subscriptionId) { if (accountList == null || accountList.size() == 0) { Log.e(LOG_TAG, "The size of Account list is 0."); } mContext = context; mAccountList = accountList; mResId = resId; + mSubscriptionId = subscriptionId; + } + + public AccountSelectedListener(Context context, List<AccountWithDataSet> accountList, + int resId) { + // Subscription id is only needed for importing from SIM card. We can safely ignore + // its value for SD card importing. + this(context, accountList, resId, SubscriptionManager.INVALID_SUBSCRIPTION_ID); } public void onClick(DialogInterface dialog, int which) { dialog.dismiss(); - doImport(mContext, mResId, mAccountList.get(which)); + doImport(mContext, mResId, mAccountList.get(which), mSubscriptionId); } /** * Reset the account list for this listener, to make sure the selected @@ -195,10 +204,11 @@ public class AccountSelectionUtil { .create(); } - public static void doImport(Context context, int resId, AccountWithDataSet account) { + public static void doImport(Context context, int resId, AccountWithDataSet account, + int subscriptionId) { switch (resId) { case R.string.import_from_sim: { - doImportFromSim(context, account); + doImportFromSim(context, account, subscriptionId); break; } case R.string.import_from_sdcard: { @@ -208,23 +218,8 @@ public class AccountSelectionUtil { } } - public static void doImportFromSim(Context context, AccountWithDataSet account) { - Intent importIntent = new Intent(SimContactsConstants.ACTION_MULTI_PICK_SIM); - if (account != null) { - importIntent.putExtra(SimContactsConstants.ACCOUNT_NAME, account.name); - importIntent.putExtra(SimContactsConstants.ACCOUNT_TYPE, account.type); - importIntent.putExtra(SimContactsConstants.ACCOUNT_DATA, account.dataSet); - } - if (TelephonyManager.getDefault().isMultiSimEnabled()) { - importIntent.putExtra(SimContactsConstants.SUB, mImportSub); - } else { - importIntent.putExtra(SimContactsConstants.SUB,SimContactsConstants.SUB_1); - } - context.startActivity(importIntent); - } - - public static void doImportFromMultiSim(Context context, AccountWithDataSet account, - int selectedSim) { + public static void doImportFromSim(Context context, AccountWithDataSet account, + int subscriptionId) { Intent importIntent = new Intent(Intent.ACTION_VIEW); importIntent.setType("vnd.android.cursor.item/sim-contact"); if (account != null) { @@ -232,8 +227,8 @@ public class AccountSelectionUtil { importIntent.putExtra("account_type", account.type); importIntent.putExtra("data_set", account.dataSet); } + importIntent.putExtra("subscription_id", (Integer) subscriptionId); importIntent.setClassName("com.android.phone", "com.android.phone.SimContacts"); - importIntent.putExtra(SIM_INDEX, selectedSim); context.startActivity(importIntent); } @@ -268,7 +263,7 @@ public class AccountSelectionUtil { public void onClick(DialogInterface dialog, int which) { Log.d(LOG_TAG, "onClick OK: mSelectedSim = " + mSelectedSim); if (mSelectedSim != SIM_ID_INVALID) { - doImportFromMultiSim(mContext, mAccount, mSelectedSim); + doImportFromSim(mContext, mAccount, mSelectedSim); } } } diff --git a/src/com/android/contacts/common/util/BitmapUtil.java b/src/com/android/contacts/common/util/BitmapUtil.java index a70831ec..66ab00f5 100644 --- a/src/com/android/contacts/common/util/BitmapUtil.java +++ b/src/com/android/contacts/common/util/BitmapUtil.java @@ -19,6 +19,11 @@ package com.android.contacts.common.util; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.PorterDuff.Mode; +import android.graphics.PorterDuffXfermode; +import android.graphics.Rect; +import android.graphics.RectF; import android.graphics.drawable.Drawable; import android.graphics.drawable.BitmapDrawable; @@ -107,4 +112,51 @@ public class BitmapUtil { return new BitmapDrawable(resources,rotated); } + + /** + * Given an input bitmap, scales it to the given width/height and makes it round. + * + * @param input {@link Bitmap} to scale and crop + * @param targetWidth desired output width + * @param targetHeight desired output height + * @return output bitmap scaled to the target width/height and cropped to an oval. The + * cropping algorithm will try to fit as much of the input into the output as possible, + * while preserving the target width/height ratio. + */ + public static Bitmap getRoundedBitmap(Bitmap input, int targetWidth, int targetHeight) { + if (input == null) { + return null; + } + final Bitmap result = Bitmap.createBitmap(targetWidth, targetHeight, input.getConfig()); + final Canvas canvas = new Canvas(result); + final Paint paint = new Paint(); + canvas.drawARGB(0, 0, 0, 0); + paint.setAntiAlias(true); + canvas.drawOval(0, 0, targetWidth, targetHeight, paint); + + // Specifies that only pixels present in the destination (i.e. the drawn oval) should + // be overwritten with pixels from the input bitmap. + paint.setXfermode(new PorterDuffXfermode(Mode.SRC_IN)); + + final int inputWidth = input.getWidth(); + final int inputHeight = input.getHeight(); + + // Choose the largest scale factor that will fit inside the dimensions of the + // input bitmap. + final float scaleBy = Math.min((float) inputWidth / targetWidth, + (float) inputHeight / targetHeight); + + final int xCropAmountHalved = (int) (scaleBy * targetWidth / 2); + final int yCropAmountHalved = (int) (scaleBy * targetHeight / 2); + + final Rect src = new Rect( + inputWidth / 2 - xCropAmountHalved, + inputHeight / 2 - yCropAmountHalved, + inputWidth / 2 + xCropAmountHalved, + inputHeight / 2 + yCropAmountHalved); + + final RectF dst = new RectF(0, 0, targetWidth, targetHeight); + canvas.drawBitmap(input, src, dst, paint); + return result; + } } diff --git a/src/com/android/contacts/common/util/ContactDisplayUtils.java b/src/com/android/contacts/common/util/ContactDisplayUtils.java index 7ec751a2..bb91b531 100644 --- a/src/com/android/contacts/common/util/ContactDisplayUtils.java +++ b/src/com/android/contacts/common/util/ContactDisplayUtils.java @@ -19,10 +19,18 @@ package com.android.contacts.common.util; import static android.provider.ContactsContract.CommonDataKinds.Phone; import android.content.Context; +import android.telephony.PhoneNumberUtils; +import android.text.Spannable; +import android.text.SpannableString; +import android.text.style.TtsSpan; import android.util.Log; +import android.util.Patterns; import com.android.contacts.common.R; +import com.android.i18n.phonenumbers.NumberParseException; +import com.android.i18n.phonenumbers.PhoneNumberUtil; +import com.android.i18n.phonenumbers.Phonenumber.PhoneNumber; import com.google.common.base.Preconditions; /** @@ -187,4 +195,80 @@ public class ContactDisplayUtils { } } + /** + * Whether the given text could be a phone number. + * + * Note this will miss many things that are legitimate phone numbers, for example, + * phone numbers with letters. + */ + public static boolean isPossiblePhoneNumber(CharSequence text) { + return text == null ? false : Patterns.PHONE.matcher(text.toString()).matches(); + } + + /** + * Returns a Spannable for the given phone number with a telephone {@link TtsSpan} set over + * the entire length of the given phone number. + */ + public static Spannable getTelephoneTtsSpannable(String phoneNumber) { + if (phoneNumber == null) { + return null; + } + final Spannable spannable = new SpannableString(phoneNumber); + final TtsSpan ttsSpan = getTelephoneTtsSpan(phoneNumber); + spannable.setSpan(ttsSpan, 0, phoneNumber.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + return spannable; + } + + /** + * Returns a Spannable for the given message with a telephone {@link TtsSpan} set for + * the given phone number text wherever it is found within the message. + */ + public static Spannable getTelephoneTtsSpannable(String message, String phoneNumber) { + if (message == null) { + return null; + } + final Spannable spannable = new SpannableString(message); + int start = phoneNumber == null ? -1 : message.indexOf(phoneNumber); + while (start >= 0) { + final int end = start + phoneNumber.length(); + final TtsSpan ttsSpan = getTelephoneTtsSpan(phoneNumber); + spannable.setSpan(ttsSpan, start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + start = message.indexOf(phoneNumber, end); + } + return spannable; + } + + /** + * Returns a telephone {@link TtsSpan} for the given phone number. + */ + public static TtsSpan getTelephoneTtsSpan(String phoneNumberString) { + if (phoneNumberString == null) { + throw new NullPointerException(); + } + + // Parse the phone number + final PhoneNumberUtil phoneNumberUtil = PhoneNumberUtil.getInstance(); + PhoneNumber phoneNumber = null; + try { + // Don't supply a defaultRegion so this fails for non-international numbers because + // we don't want to TalkBalk to read a country code (e.g. +1) if it is not already + // present + phoneNumber = phoneNumberUtil.parse(phoneNumberString, /* defaultRegion */ null); + } catch (NumberParseException ignored) { + } + + // Build a telephone tts span + final TtsSpan.TelephoneBuilder builder = new TtsSpan.TelephoneBuilder(); + if (phoneNumber == null) { + // Strip separators otherwise TalkBack will be silent + // (this behavior was observed with TalkBalk 4.0.2 from their alpha channel) + builder.setNumberParts(PhoneNumberUtils.stripSeparators(phoneNumberString)); + } else { + if (phoneNumber.hasCountryCode()) { + builder.setCountryCode(Integer.toString(phoneNumber.getCountryCode())); + } + builder.setNumberParts(Long.toString(phoneNumber.getNationalNumber())); + } + return builder.build(); + } } diff --git a/src/com/android/contacts/common/util/LocalizedNameResolver.java b/src/com/android/contacts/common/util/LocalizedNameResolver.java index 3c21946a..92104c44 100644 --- a/src/com/android/contacts/common/util/LocalizedNameResolver.java +++ b/src/com/android/contacts/common/util/LocalizedNameResolver.java @@ -19,10 +19,8 @@ package com.android.contacts.common.util; import android.accounts.AccountManager; import android.accounts.AuthenticatorDescription; import android.content.Context; -import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.content.pm.PackageManager.NameNotFoundException; -import android.content.pm.ServiceInfo; import android.content.res.Resources; import android.content.res.Resources.NotFoundException; import android.content.res.TypedArray; @@ -32,6 +30,7 @@ import android.util.Log; import android.util.Xml; import com.android.contacts.common.R; +import com.android.contacts.common.model.account.ExternalAccountType; import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; @@ -45,11 +44,6 @@ import java.io.IOException; public class LocalizedNameResolver { private static final String TAG = "LocalizedNameResolver"; - /** - * Meta-data key for the contacts configuration associated with a sync service. - */ - private static final String METADATA_CONTACTS = "android.provider.CONTACTS_STRUCTURE"; - private static final String CONTACTS_DATA_KIND = "ContactsDataKind"; /** @@ -82,20 +76,9 @@ public class LocalizedNameResolver { * reads the picture priority from that file. */ private static String resolveAllContactsNameFromMetaData(Context context, String packageName) { - final PackageManager pm = context.getPackageManager(); - try { - PackageInfo pi = pm.getPackageInfo(packageName, PackageManager.GET_SERVICES - | PackageManager.GET_META_DATA); - if (pi != null && pi.services != null) { - for (ServiceInfo si : pi.services) { - final XmlResourceParser parser = si.loadXmlMetaData(pm, METADATA_CONTACTS); - if (parser != null) { - return loadAllContactsNameFromXml(context, parser, packageName); - } - } - } - } catch (NameNotFoundException e) { - Log.w(TAG, "Problem loading \"All Contacts\"-name: " + e.toString()); + final XmlResourceParser parser = ExternalAccountType.loadContactsXml(context, packageName); + if (parser != null) { + return loadAllContactsNameFromXml(context, parser, packageName); } return null; } diff --git a/src/com/android/contacts/common/util/MaterialColorMapUtils.java b/src/com/android/contacts/common/util/MaterialColorMapUtils.java index 7827ae36..a8fbf421 100644 --- a/src/com/android/contacts/common/util/MaterialColorMapUtils.java +++ b/src/com/android/contacts/common/util/MaterialColorMapUtils.java @@ -25,7 +25,6 @@ import android.os.Parcelable; import android.os.Trace; public class MaterialColorMapUtils { - private final TypedArray sPrimaryColors; private final TypedArray sSecondaryColors; diff --git a/src/com/android/contacts/common/vcard/ExportVCardActivity.java b/src/com/android/contacts/common/vcard/ExportVCardActivity.java index 625412e8..029561d0 100644 --- a/src/com/android/contacts/common/vcard/ExportVCardActivity.java +++ b/src/com/android/contacts/common/vcard/ExportVCardActivity.java @@ -30,6 +30,8 @@ import android.os.Handler; import android.os.IBinder; import android.os.Message; import android.os.Messenger; +import android.text.BidiFormatter; +import android.text.TextDirectionHeuristics; import android.text.TextUtils; import android.util.Log; @@ -113,6 +115,7 @@ public class ExportVCardActivity extends Activity implements ServiceConnection, private VCardService mService; private final Messenger mIncomingMessenger = new Messenger(new IncomingHandler()); + private static final BidiFormatter mBidiFormatter = BidiFormatter.getInstance(); // Used temporarily when asking users to confirm the file name private String mTargetFileName; @@ -268,13 +271,25 @@ public class ExportVCardActivity extends Activity implements ServiceConnection, } } + /** + * Returns the name of the target path with additional formatting characters to improve its + * appearance in bidirectional text. + */ + private String getTargetFileForDisplay() { + if (mTargetFileName == null) { + return null; + } + return mBidiFormatter.unicodeWrap(mTargetFileName, TextDirectionHeuristics.LTR); + } + @Override protected Dialog onCreateDialog(int id, Bundle bundle) { switch (id) { case R.id.dialog_export_confirmation: { return new AlertDialog.Builder(this) .setTitle(R.string.confirm_export_title) - .setMessage(getString(R.string.confirm_export_message, mTargetFileName)) + .setMessage(getString(R.string.confirm_export_message, + getTargetFileForDisplay())) .setPositiveButton(android.R.string.ok, new ExportConfirmationListener(mTargetFileName)) .setNegativeButton(android.R.string.cancel, this) @@ -318,7 +333,7 @@ public class ExportVCardActivity extends Activity implements ServiceConnection, ((AlertDialog)dialog).setMessage(mErrorReason); } else if (id == R.id.dialog_export_confirmation) { ((AlertDialog)dialog).setMessage( - getString(R.string.confirm_export_message, mTargetFileName)); + getString(R.string.confirm_export_message, getTargetFileForDisplay())); } else { super.onPrepareDialog(id, dialog, args); } diff --git a/src/com/android/contacts/common/vcard/ImportProcessor.java b/src/com/android/contacts/common/vcard/ImportProcessor.java index 37128755..219ec144 100644 --- a/src/com/android/contacts/common/vcard/ImportProcessor.java +++ b/src/com/android/contacts/common/vcard/ImportProcessor.java @@ -194,14 +194,14 @@ public class ImportProcessor extends ProcessorBase implements VCardEntryHandler Log.i(LOG_TAG, "Successfully finished importing one vCard file: " + uri); List<Uri> uris = committer.getCreatedUris(); if (mListener != null) { - if (uris != null && uris.size() > 0) { - // TODO: construct intent showing a list of imported contact list. + if (uris != null && uris.size() == 1) { mListener.onImportFinished(mImportRequest, mJobId, uris.get(0)); } else { - // Not critical, but suspicious. - Log.w(LOG_TAG, - "Created Uris is null or 0 length " + - "though the creation itself is successful."); + if (uris == null || uris.size() == 0) { + // Not critical, but suspicious. + Log.w(LOG_TAG, "Created Uris is null or 0 length " + + "though the creation itself is successful."); + } mListener.onImportFinished(mImportRequest, mJobId, null); } } diff --git a/src/com/android/contacts/common/vcard/ImportVCardActivity.java b/src/com/android/contacts/common/vcard/ImportVCardActivity.java index 24f24dd1..d36dcaf3 100644 --- a/src/com/android/contacts/common/vcard/ImportVCardActivity.java +++ b/src/com/android/contacts/common/vcard/ImportVCardActivity.java @@ -110,8 +110,6 @@ public class ImportVCardActivity extends Activity { final static String CACHED_URIS = "cached_uris"; - private AccountSelectionUtil.AccountSelectedListener mAccountSelectionListener; - private AccountWithDataSet mAccount; private ProgressDialog mProgressDialogForScanVCard; @@ -946,14 +944,6 @@ public class ImportVCardActivity extends Activity { @Override protected Dialog onCreateDialog(int resId, Bundle bundle) { switch (resId) { - case R.string.import_from_sdcard: { - if (mAccountSelectionListener == null) { - throw new NullPointerException( - "mAccountSelectionListener must not be null."); - } - return AccountSelectionUtil.getSelectAccountDialog(this, resId, - mAccountSelectionListener, mCancelListener); - } case R.id.dialog_searching_vcard: { if (mProgressDialogForScanVCard == null) { String message = getString(R.string.searching_vcard_message); diff --git a/src/com/android/contacts/common/vcard/NotificationImportExportListener.java b/src/com/android/contacts/common/vcard/NotificationImportExportListener.java index 7117f9f5..63420026 100644 --- a/src/com/android/contacts/common/vcard/NotificationImportExportListener.java +++ b/src/com/android/contacts/common/vcard/NotificationImportExportListener.java @@ -26,12 +26,15 @@ import android.content.Intent; import android.net.Uri; import android.os.Handler; import android.os.Message; +import android.provider.ContactsContract; import android.provider.ContactsContract.RawContacts; import android.widget.Toast; import com.android.contacts.common.R; import com.android.vcard.VCardEntry; +import java.text.NumberFormat; + public class NotificationImportExportListener implements VCardImportExportListener, Handler.Callback { /** The tag used by vCard-related notifications. */ @@ -123,7 +126,8 @@ public class NotificationImportExportListener implements VCardImportExportListen RawContacts.CONTENT_URI, rawContactId)); intent = new Intent(Intent.ACTION_VIEW, contactUri); } else { - intent = null; + intent = new Intent(Intent.ACTION_VIEW); + intent.setType(ContactsContract.Contacts.CONTENT_TYPE); } final Notification notification = NotificationImportExportListener.constructFinishNotification(mContext, @@ -218,13 +222,15 @@ public class NotificationImportExportListener implements VCardImportExportListen .setProgress(totalCount, currentCount, totalCount == - 1) .setTicker(tickerText) .setContentTitle(description) + .setColor(context.getResources().getColor(R.color.dialtacts_theme_color)) .setSmallIcon(type == VCardService.TYPE_IMPORT ? android.R.drawable.stat_sys_download : android.R.drawable.stat_sys_upload) .setContentIntent(PendingIntent.getActivity(context, 0, intent, 0)); if (totalCount > 0) { - builder.setContentText(context.getString(R.string.percentage, - String.valueOf(currentCount * 100 / totalCount))); + String percentage = + NumberFormat.getPercentInstance().format((double) currentCount / totalCount); + builder.setContentText(percentage); } return builder.getNotification(); } @@ -240,6 +246,7 @@ public class NotificationImportExportListener implements VCardImportExportListen return new Notification.Builder(context) .setAutoCancel(true) .setSmallIcon(android.R.drawable.stat_notify_error) + .setColor(context.getResources().getColor(R.color.dialtacts_theme_color)) .setContentTitle(description) .setContentText(description) .setContentIntent(PendingIntent.getActivity(context, 0, new Intent(), 0)) @@ -260,6 +267,7 @@ public class NotificationImportExportListener implements VCardImportExportListen .setSmallIcon(type == VCardService.TYPE_IMPORT ? android.R.drawable.stat_sys_download_done : android.R.drawable.stat_sys_upload_done) + .setColor(context.getResources().getColor(R.color.dialtacts_theme_color)) .setContentTitle(title) .setContentText(description) .setContentIntent(PendingIntent.getActivity(context, 0, @@ -277,6 +285,7 @@ public class NotificationImportExportListener implements VCardImportExportListen Context context, String reason) { return new Notification.Builder(context) .setAutoCancel(true) + .setColor(context.getResources().getColor(R.color.dialtacts_theme_color)) .setSmallIcon(android.R.drawable.stat_notify_error) .setContentTitle(context.getString(R.string.vcard_import_failed)) .setContentText(reason) diff --git a/src/com/android/contacts/common/vcard/VCardService.java b/src/com/android/contacts/common/vcard/VCardService.java index f11598a1..7b90eddb 100644 --- a/src/com/android/contacts/common/vcard/VCardService.java +++ b/src/com/android/contacts/common/vcard/VCardService.java @@ -38,6 +38,7 @@ import java.io.File; import java.util.ArrayList; import java.util.HashSet; import java.util.List; +import java.util.Locale; import java.util.Set; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; @@ -496,7 +497,8 @@ public class VCardService extends Service { * This method increments "index" part from 1 to maximum, and checks whether any file name * following naming rule is available. If there's no file named /mnt/sdcard/00001.vcf, the * name will be returned to a caller. If there are 00001.vcf 00002.vcf, 00003.vcf is - * returned. + * returned. We format these numbers in the US locale to ensure we they appear as + * english numerals. * * There may not be any appropriate file name. If there are 99999 vCard files in the * storage, for example, there's no appropriate name, so this method returns @@ -519,7 +521,7 @@ public class VCardService extends Service { if (!ALLOW_LONG_FILE_NAME) { final String possibleBody = - String.format(bodyFormat, mFileNamePrefix, 1, mFileNameSuffix); + String.format(Locale.US, bodyFormat, mFileNamePrefix, 1, mFileNameSuffix); if (possibleBody.length() > 8 || mFileNameExtension.length() > 3) { Log.e(LOG_TAG, "This code does not allow any long file name."); mErrorReason = getString(R.string.fail_reason_too_long_filename, @@ -531,7 +533,8 @@ public class VCardService extends Service { for (int i = mFileIndexMinimum; i <= mFileIndexMaximum; i++) { boolean numberIsAvailable = true; - final String body = String.format(bodyFormat, mFileNamePrefix, i, mFileNameSuffix); + final String body + = String.format(Locale.US, bodyFormat, mFileNamePrefix, i, mFileNameSuffix); // Make sure that none of the extensions of mExtensionsToConsider matches. If this // number is free, we'll go ahead with mFileNameExtension (which is included in // mExtensionsToConsider) diff --git a/src/com/android/contacts/common/widget/SelectPhoneAccountDialogFragment.java b/src/com/android/contacts/common/widget/SelectPhoneAccountDialogFragment.java new file mode 100644 index 00000000..08702543 --- /dev/null +++ b/src/com/android/contacts/common/widget/SelectPhoneAccountDialogFragment.java @@ -0,0 +1,220 @@ +/* + * Copyright (C) 2014 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.widget; + +import android.telecom.PhoneAccount; +import android.telecom.PhoneAccountHandle; +import android.app.AlertDialog; +import android.app.Dialog; +import android.app.DialogFragment; +import android.app.FragmentManager; +import android.content.Context; +import android.content.DialogInterface; +import android.os.Bundle; +import android.telecom.TelecomManager; +import android.telephony.PhoneNumberUtils; +import android.text.TextUtils; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ArrayAdapter; +import android.widget.CheckBox; +import android.widget.CompoundButton; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.ListAdapter; +import android.widget.TextView; + +import com.android.contacts.common.R; + +import java.util.List; + +/** + * Dialog that allows the user to select a phone accounts for a given action. Optionally provides + * the choice to set the phone account as default. + */ +public class SelectPhoneAccountDialogFragment extends DialogFragment { + private int mTitleResId; + private boolean mCanSetDefault; + private List<PhoneAccountHandle> mAccountHandles; + private boolean mIsSelected; + private boolean mIsDefaultChecked; + private TelecomManager mTelecomManager; + private SelectPhoneAccountListener mListener; + + /** + * Shows the account selection dialog. + * This is the preferred way to show this dialog. + * + * @param fragmentManager The fragment manager. + * @param accountHandles The {@code PhoneAccountHandle}s available to select from. + * @param listener The listener for the results of the account selection. + */ + public static void showAccountDialog(FragmentManager fragmentManager, + List<PhoneAccountHandle> accountHandles, SelectPhoneAccountListener listener) { + showAccountDialog(fragmentManager, R.string.select_account_dialog_title, false, + accountHandles, listener); + } + + /** + * Shows the account selection dialog. + * This is the preferred way to show this dialog. + * This method also allows specifying a custom title and "set default" checkbox. + * + * @param fragmentManager The fragment manager. + * @param titleResId The resource ID for the string to use in the title of the dialog. + * @param canSetDefault {@code true} if the dialog should include an option to set the selection + * as the default. False otherwise. + * @param accountHandles The {@code PhoneAccountHandle}s available to select from. + * @param listener The listener for the results of the account selection. + */ + public static void showAccountDialog(FragmentManager fragmentManager, int titleResId, + boolean canSetDefault, List<PhoneAccountHandle> accountHandles, + SelectPhoneAccountListener listener) { + SelectPhoneAccountDialogFragment fragment = + new SelectPhoneAccountDialogFragment( + titleResId, canSetDefault, accountHandles, listener); + fragment.show(fragmentManager, "selectAccount"); + } + + public SelectPhoneAccountDialogFragment(int titleResId, boolean canSetDefault, + List<PhoneAccountHandle> accountHandles, SelectPhoneAccountListener listener) { + super(); + mTitleResId = titleResId; + mCanSetDefault = canSetDefault; + mAccountHandles = accountHandles; + mListener = listener; + } + + public interface SelectPhoneAccountListener { + void onPhoneAccountSelected(PhoneAccountHandle selectedAccountHandle, boolean setDefault); + void onDialogDismissed(); + } + + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + mIsSelected = false; + mIsDefaultChecked = false; + mTelecomManager = + (TelecomManager) getActivity().getSystemService(Context.TELECOM_SERVICE); + + final DialogInterface.OnClickListener selectionListener = + new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + mIsSelected = true; + PhoneAccountHandle selectedAccountHandle = mAccountHandles.get(which); + mListener.onPhoneAccountSelected(selectedAccountHandle, mIsDefaultChecked); + } + }; + + final CompoundButton.OnCheckedChangeListener checkListener = + new CompoundButton.OnCheckedChangeListener() { + @Override + public void onCheckedChanged(CompoundButton check, boolean isChecked) { + mIsDefaultChecked = isChecked; + } + }; + + AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); + ListAdapter selectAccountListAdapter = new SelectAccountListAdapter( + builder.getContext(), + R.layout.select_account_list_item, + mAccountHandles); + + AlertDialog dialog = builder.setTitle(mTitleResId) + .setAdapter(selectAccountListAdapter, selectionListener) + .create(); + + if (mCanSetDefault) { + // Generate custom checkbox view + LinearLayout checkboxLayout = (LinearLayout) getActivity() + .getLayoutInflater() + .inflate(R.layout.default_account_checkbox, null); + + CheckBox cb = + (CheckBox) checkboxLayout.findViewById(R.id.default_account_checkbox_view); + cb.setOnCheckedChangeListener(checkListener); + + dialog.getListView().addFooterView(checkboxLayout); + } + + return dialog; + } + + private class SelectAccountListAdapter extends ArrayAdapter<PhoneAccountHandle> { + private int mResId; + + public SelectAccountListAdapter( + Context context, int resource, List<PhoneAccountHandle> accountHandles) { + super(context, resource, accountHandles); + mResId = resource; + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + LayoutInflater inflater = (LayoutInflater) + getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE); + + View rowView; + final ViewHolder holder; + + if (convertView == null) { + // Cache views for faster scrolling + rowView = inflater.inflate(mResId, null); + holder = new ViewHolder(); + holder.labelTextView = (TextView) rowView.findViewById(R.id.label); + holder.numberTextView = (TextView) rowView.findViewById(R.id.number); + holder.imageView = (ImageView) rowView.findViewById(R.id.icon); + rowView.setTag(holder); + } + else { + rowView = convertView; + holder = (ViewHolder) rowView.getTag(); + } + + PhoneAccountHandle accountHandle = getItem(position); + PhoneAccount account = mTelecomManager.getPhoneAccount(accountHandle); + holder.labelTextView.setText(account.getLabel()); + if (account.getAddress() == null || + TextUtils.isEmpty(account.getAddress().getSchemeSpecificPart())) { + holder.numberTextView.setVisibility(View.GONE); + } else { + holder.numberTextView.setVisibility(View.VISIBLE); + holder.numberTextView.setText( + PhoneNumberUtils.ttsSpanAsPhoneNumber( + account.getAddress().getSchemeSpecificPart())); + } + holder.imageView.setImageDrawable(account.createIconDrawable(getContext())); + return rowView; + } + + private class ViewHolder { + TextView labelTextView; + TextView numberTextView; + ImageView imageView; + } + } + + @Override + public void onPause() { + if (!mIsSelected) { + mListener.onDialogDismissed(); + } + super.onPause(); + } +} diff --git a/src/com/android/contacts/commonbind/analytics/AnalyticsUtil.java b/src/com/android/contacts/commonbind/analytics/AnalyticsUtil.java new file mode 100644 index 00000000..59650aa9 --- /dev/null +++ b/src/com/android/contacts/commonbind/analytics/AnalyticsUtil.java @@ -0,0 +1,25 @@ +package com.android.contacts.commonbind.analytics; + +import android.app.Activity; +import android.app.Application; +import android.app.Fragment; +import android.text.TextUtils; + +public class AnalyticsUtil { + + /** + * Initialize this class and setup automatic activity tracking. + */ + public static void initialize(Application application) { } + + /** + * Log a screen view for {@param fragment}. + */ + public static void sendScreenView(Fragment fragment) {} + + public static void sendScreenView(Fragment fragment, Activity activity) {} + + public static void sendScreenView(Fragment fragment, Activity activity, String tag) {} + + public static void sendScreenView(String fragmentName, Activity activity, String tag) {} +}
\ No newline at end of file |