Merge "Import translations. DO NOT MERGE" into ub-contactsdialer-h-dev
diff --git a/res/color/contact_list_name_text_color.xml b/res/color/contact_list_name_text_color.xml
new file mode 100644
index 0000000..2327e2a
--- /dev/null
+++ b/res/color/contact_list_name_text_color.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2016 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.
+-->
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:color="@color/disabled_contact_list_name_text" android:state_enabled="false" />
+ <item android:color="@color/primary_text_color" />
+</selector>
\ No newline at end of file
diff --git a/res/layout/fragment_sim_import.xml b/res/layout/fragment_sim_import.xml
index 6b8c3fe..95864c8 100644
--- a/res/layout/fragment_sim_import.xml
+++ b/res/layout/fragment_sim_import.xml
@@ -13,69 +13,76 @@
See the License for the specific language governing permissions and
limitations under the License.
-->
-<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+<android.support.design.widget.CoordinatorLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:orientation="vertical">
+ android:layout_height="match_parent">
- <android.support.v7.widget.Toolbar
- android:id="@+id/toolbar"
+ <LinearLayout
android:layout_width="match_parent"
- android:layout_height="?attr/actionBarSize"
- android:background="?attr/colorPrimary"
- android:elevation="4dp"
- android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
- app:navigationContentDescription="@string/sim_import_cancel_content_description"
- app:navigationIcon="@drawable/ic_close_dk"
- app:title="@string/sim_import_title_none_selected">
+ android:layout_height="match_parent"
+ android:orientation="vertical">
- <Button
- android:id="@+id/import_button"
- style="@style/Widget.AppCompat.Button.Borderless"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_gravity="right|center_vertical"
- android:text="@string/sim_import_button_text"
- />
- </android.support.v7.widget.Toolbar>
-
- <FrameLayout
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:layout_marginBottom="16dp"
- android:background="?android:colorBackground"
- android:elevation="4dp">
-
- <include layout="@layout/editor_account_header"/>
- </FrameLayout>
-
- <FrameLayout
- android:layout_width="match_parent"
- android:layout_height="match_parent">
-
- <ListView
- android:id="@+id/list"
+ <android.support.v7.widget.Toolbar
+ android:id="@+id/toolbar"
+ style="@style/ContactsToolbarStyle"
android:layout_width="match_parent"
- android:layout_height="match_parent"/>
+ android:layout_height="?attr/actionBarSize"
+ android:background="?attr/colorPrimary"
+ android:elevation="4dp"
+ android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
+ app:navigationContentDescription="@string/sim_import_cancel_content_description"
+ app:navigationIcon="@drawable/ic_close_dk"
+ app:title="@string/sim_import_title_none_selected">
- <android.support.v4.widget.ContentLoadingProgressBar
- android:id="@+id/loading_progress"
- style="@style/Widget.AppCompat.ProgressBar"
- android:layout_width="wrap_content"
+ <Button
+ android:id="@+id/import_button"
+ style="@style/Widget.AppCompat.Button.Borderless"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="right|center_vertical"
+ android:text="@string/sim_import_button_text"
+ />
+ </android.support.v7.widget.Toolbar>
+
+ <FrameLayout
+ android:layout_width="match_parent"
android:layout_height="wrap_content"
- android:layout_gravity="center"
- android:indeterminate="true"/>
+ android:layout_marginBottom="16dp"
+ android:background="?android:colorBackground"
+ android:elevation="4dp">
- <TextView
- android:id="@+id/empty_message"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_gravity="center"
- android:text="@string/sim_import_empty_message"
- android:textAppearance="?android:textAppearanceMedium"
- android:visibility="gone"/>
+ <include layout="@layout/editor_account_header"/>
+ </FrameLayout>
- </FrameLayout>
+ <FrameLayout
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
-</LinearLayout>
\ No newline at end of file
+ <ListView
+ android:id="@+id/list"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"/>
+
+ <android.support.v4.widget.ContentLoadingProgressBar
+ android:id="@+id/loading_progress"
+ style="@style/Widget.AppCompat.ProgressBar"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center"
+ android:indeterminate="true"/>
+
+ <TextView
+ android:id="@+id/empty_message"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center"
+ android:text="@string/sim_import_empty_message"
+ android:textAppearance="?android:textAppearanceMedium"
+ android:visibility="gone"/>
+
+ </FrameLayout>
+
+ </LinearLayout>
+</android.support.design.widget.CoordinatorLayout>
diff --git a/res/values/colors.xml b/res/values/colors.xml
index 6dbe010..03ca621 100644
--- a/res/values/colors.xml
+++ b/res/values/colors.xml
@@ -83,6 +83,9 @@
<!-- Color of background of disabled link contacts button, 15% black. -->
<color name="disabled_button_background">#26000000</color>
+ <!-- Color of text of people names in contact list when item is disabled. 30% black -->
+ <color name="disabled_contact_list_name_text">#4D000000</color>
+
<!-- Color of background of all empty states. -->
<color name="empty_state_background">#efefef</color>
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 015d612..36315a2 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -1818,6 +1818,10 @@
SIM card -->
<string name="sim_import_empty_message">No contacts on your SIM card</string>
+ <!-- Snackbar message shown on the SIM import screen when the user taps on a contact that
+ is disabled because an exact duplicate already exists in their contacts [CHAR LIMIT=60] -->
+ <string name="sim_import_contact_exists_toast">Contact already exists in your list</string>
+
<!-- Toast shown on settings screen when importing from SIM completes successfully -->
<plurals name="sim_import_success_toast_fmt">
<item quantity="one">1 SIM contact imported</item>
@@ -1857,4 +1861,7 @@
<!-- No network connection error message [CHAR LIMIT=50] -->
<string name="connection_error_message">No connection</string>
+
+ <!-- Generic label for the SIM card when there is only a single SIM in the device [CHAR LIMIT=10]-->
+ <string name="single_sim_display_label">SIM</string>
</resources>
diff --git a/src/com/android/contacts/SimImportFragment.java b/src/com/android/contacts/SimImportFragment.java
index ed043bf..bc20d62 100644
--- a/src/com/android/contacts/SimImportFragment.java
+++ b/src/com/android/contacts/SimImportFragment.java
@@ -25,6 +25,7 @@
import android.database.Cursor;
import android.os.Bundle;
import android.support.annotation.Nullable;
+import android.support.design.widget.Snackbar;
import android.support.v4.widget.ContentLoadingProgressBar;
import android.support.v7.widget.Toolbar;
import android.view.LayoutInflater;
@@ -132,7 +133,12 @@
mListView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
- mAdapter.toggleSelectionOfContactId(id);
+ if (mAdapter.existsInContacts(position)) {
+ Snackbar.make(getView(), R.string.sim_import_contact_exists_toast,
+ Snackbar.LENGTH_LONG).show();
+ } else {
+ mAdapter.toggleSelectionOfContactId(id);
+ }
}
});
mImportButton = view.findViewById(R.id.import_button);
@@ -185,14 +191,17 @@
@Override
public void onLoadFinished(Loader<ArrayList<SimContact>> loader,
ArrayList<SimContact> data) {
+ mLoadingIndicator.hide();
mListView.setEmptyView(getView().findViewById(R.id.empty_message));
+ if (data == null) {
+ return;
+ }
mAdapter.setContacts(data);
if (mSelectedContacts != null) {
mAdapter.select(mSelectedContacts);
} else {
mAdapter.selectAll();
}
- mLoadingIndicator.hide();
}
@Override
@@ -258,6 +267,7 @@
private static class SimContactAdapter extends ContactListAdapter {
private ArrayList<SimContact> mContacts;
+ private static float DISABLED_AVATAR_ALPHA = 0.38f;
public SimContactAdapter(Context context) {
super(context);
@@ -278,6 +288,7 @@
// clickable
contactView.getCheckBox().setFocusable(false);
contactView.getCheckBox().setClickable(false);
+ setViewEnabled(contactView, !mContacts.get(cursor.getPosition()).existsInContacts());
}
public void setContacts(ArrayList<SimContact> contacts) {
@@ -304,7 +315,9 @@
final TreeSet<Long> selected = new TreeSet<>();
for (SimContact contact : mContacts) {
- selected.add(contact.getId());
+ if (!contact.existsInContacts()) {
+ selected.add(contact.getId());
+ }
}
setSelectedContactIds(selected);
}
@@ -316,6 +329,16 @@
}
setSelectedContactIds(selected);
}
+
+ public boolean existsInContacts(int position) {
+ return mContacts.get(position).existsInContacts();
+ }
+
+ private void setViewEnabled(ContactListItemView itemView, boolean enabled) {
+ itemView.getCheckBox().setEnabled(enabled);
+ itemView.getPhotoView().setAlpha(enabled ? 1f : DISABLED_AVATAR_ALPHA);
+ itemView.getNameTextView().setEnabled(enabled);
+ }
}
public static class SimContactLoader extends AsyncTaskLoader<ArrayList<SimContact>> {
@@ -346,11 +369,11 @@
@Override
public ArrayList<SimContact> loadInBackground() {
- if (mSubscriptionId != SimCard.NO_SUBSCRIPTION_ID) {
- return mDao.loadSimContacts(mSubscriptionId);
- } else {
- return mDao.loadSimContacts();
+ final SimCard sim = mDao.getSimBySubscriptionId(mSubscriptionId);
+ if (sim == null) {
+ return new ArrayList<>();
}
+ return mDao.loadSimContactsWithExistingContactIds(sim);
}
@Override
diff --git a/src/com/android/contacts/activities/PeopleActivity.java b/src/com/android/contacts/activities/PeopleActivity.java
index 513109c..fd6ee89 100644
--- a/src/com/android/contacts/activities/PeopleActivity.java
+++ b/src/com/android/contacts/activities/PeopleActivity.java
@@ -272,7 +272,8 @@
@Override
protected void onNewIntent(Intent intent) {
- if (GroupUtil.ACTION_CREATE_GROUP.equals(intent.getAction())) {
+ final String action = intent.getAction();
+ if (GroupUtil.ACTION_CREATE_GROUP.equals(action)) {
mGroupUri = intent.getData();
if (mGroupUri == null) {
toast(R.string.groupSavedErrorToast);
@@ -280,19 +281,19 @@
}
if (Log.isLoggable(TAG, Log.VERBOSE)) Log.v(TAG, "Received group URI " + mGroupUri);
switchView(ContactsView.GROUP_VIEW);
- mMembersFragment.toastForSaveAction(intent.getAction());
+ mMembersFragment.toastForSaveAction(action);
return;
}
- if (isGroupDeleteAction(intent.getAction())) {
+ if (isGroupDeleteAction(action)) {
popSecondLevel();
- mMembersFragment.toastForSaveAction(intent.getAction());
+ mMembersFragment.toastForSaveAction(action);
mCurrentView = ContactsView.ALL_CONTACTS;
showFabWithAnimation(/* showFab */ true);
return;
}
- if (isGroupSaveAction(intent.getAction())) {
+ if (isGroupSaveAction(action)) {
mGroupUri = intent.getData();
if (mGroupUri == null) {
popSecondLevel();
@@ -300,8 +301,15 @@
return;
}
if (Log.isLoggable(TAG, Log.VERBOSE)) Log.v(TAG, "Received group URI " + mGroupUri);
- switchView(ContactsView.GROUP_VIEW);
- mMembersFragment.toastForSaveAction(intent.getAction());
+ // ACTION_REMOVE_FROM_GROUP doesn't reload data, so it shouldn't cause b/32223934
+ // but it's necessary to use the previous fragment since
+ // GroupMembersFragment#mIsEditMode needs to be persisted between remove actions.
+ if (GroupUtil.ACTION_REMOVE_FROM_GROUP.equals(action)) {
+ switchToOrUpdateGroupView(action);
+ } else {
+ switchView(ContactsView.GROUP_VIEW);
+ }
+ mMembersFragment.toastForSaveAction(action);
}
setIntent(intent);
@@ -747,7 +755,7 @@
mGroupUri = savedInstanceState.getParcelable(KEY_GROUP_URI);
}
- private void onGroupDeleted(Intent intent) {
+ private void onGroupDeleted(final Intent intent) {
if (!ContactSaveService.canUndo(intent)) return;
Snackbar.make(mLayoutRoot, getString(R.string.groupDeletedToast), Snackbar.LENGTH_LONG)
@@ -822,7 +830,7 @@
mMembersFragment = GroupMembersFragment.newInstance(mGroupUri);
transaction.replace(
R.id.contacts_list_container, mMembersFragment, TAG_GROUP_VIEW);
- } else if(isAssistantView()) {
+ } else if (isAssistantView()) {
String fragmentTag;
if (Flags.getInstance().getBoolean(Experiments.ASSISTANT)) {
fragmentTag = TAG_ASSISTANT;
diff --git a/src/com/android/contacts/common/database/SimContactDao.java b/src/com/android/contacts/common/database/SimContactDao.java
index f5880a8..cab2906 100644
--- a/src/com/android/contacts/common/database/SimContactDao.java
+++ b/src/com/android/contacts/common/database/SimContactDao.java
@@ -29,13 +29,14 @@
import android.os.RemoteException;
import android.provider.BaseColumns;
import android.provider.ContactsContract;
-import android.support.annotation.NonNull;
+import android.provider.ContactsContract.CommonDataKinds.Phone;
import android.support.annotation.VisibleForTesting;
import android.telephony.SubscriptionInfo;
import android.telephony.SubscriptionManager;
import android.telephony.TelephonyManager;
import android.util.SparseArray;
+import com.android.contacts.R;
import com.android.contacts.common.Experiments;
import com.android.contacts.common.compat.CompatUtils;
import com.android.contacts.common.model.SimCard;
@@ -44,8 +45,10 @@
import com.android.contacts.common.util.PermissionsUtil;
import com.android.contacts.util.SharedPreferenceUtil;
import com.android.contactsbind.experiments.Flags;
+import com.google.common.base.Joiner;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.Collections;
import java.util.List;
@@ -56,6 +59,12 @@
public class SimContactDao {
private static final String TAG = "SimContactDao";
+ // Maximum number of SIM contacts to import in a single ContentResolver.applyBatch call.
+ // This is necessary to avoid TransactionTooLargeException when there are a large number of
+ // contacts. This has been tested on Nexus 6 NME70B and is probably be conservative enough
+ // to work on any phone.
+ private static final int IMPORT_MAX_BATCH_SIZE = 300;
+
// Set to true for manual testing on an emulator or phone without a SIM card
// DO NOT SUBMIT if set to true
private static final boolean USE_FAKE_INSTANCE = false;
@@ -72,7 +81,7 @@
private final ContentResolver mResolver;
private final TelephonyManager mTelephonyManager;
- public SimContactDao(Context context) {
+ private SimContactDao(Context context) {
mContext = context;
mResolver = context.getContentResolver();
mTelephonyManager = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
@@ -86,20 +95,21 @@
new AsyncTask<Void, Void, Void>() {
@Override
protected Void doInBackground(Void... params) {
- for (SimCard card : getSimCards()) {
- // We don't actually have to do any caching ourselves. Some other layer must do
- // caching of the data (OS or framework) because subsequent queries are very
- // fast.
- card.loadContacts(SimContactDao.this);
- }
+ getSimCardsWithContacts();
return null;
}
}.execute();
}
+ public Context getContext() {
+ return mContext;
+ }
+
public boolean canReadSimContacts() {
+ // Require SIM_STATE_READY because the TelephonyManager methods related to SIM require
+ // this state
return hasTelephony() && hasPermissions() &&
- mTelephonyManager.getSimState() != TelephonyManager.SIM_STATE_ABSENT;
+ mTelephonyManager.getSimState() == TelephonyManager.SIM_STATE_READY;
}
public List<SimCard> getSimCards() {
@@ -108,24 +118,26 @@
}
final List<SimCard> sims = CompatUtils.isMSIMCompatible() ?
getSimCardsFromSubscriptions() :
- Collections.singletonList(SimCard.create(mTelephonyManager));
+ Collections.singletonList(SimCard.create(mTelephonyManager,
+ mContext.getString(R.string.single_sim_display_label)));
return SharedPreferenceUtil.restoreSimStates(mContext, sims);
}
- @NonNull
- @TargetApi(Build.VERSION_CODES.LOLLIPOP_MR1)
- private List<SimCard> getSimCardsFromSubscriptions() {
- final SubscriptionManager subscriptionManager = (SubscriptionManager)
- mContext.getSystemService(Context.TELEPHONY_SUBSCRIPTION_SERVICE);
- final List<SubscriptionInfo> subscriptions = subscriptionManager
- .getActiveSubscriptionInfoList();
- final ArrayList<SimCard> result = new ArrayList<>();
- for (SubscriptionInfo subscriptionInfo : subscriptions) {
- result.add(SimCard.create(subscriptionInfo));
+ public List<SimCard> getSimCardsWithContacts() {
+ final List<SimCard> result = new ArrayList<>();
+ for (SimCard sim : getSimCards()) {
+ result.add(sim.withContacts(loadContactsForSim(sim)));
}
return result;
}
+ public ArrayList<SimContact> loadContactsForSim(SimCard sim) {
+ if (sim.hasValidSubscriptionId()) {
+ return loadSimContacts(sim.getSubscriptionId());
+ }
+ return loadSimContacts();
+ }
+
public ArrayList<SimContact> loadSimContacts(int subscriptionId) {
return loadFrom(ICC_CONTENT_URI.buildUpon()
.appendPath("subId")
@@ -137,8 +149,77 @@
return loadFrom(ICC_CONTENT_URI);
}
- public Context getContext() {
- return mContext;
+ public ArrayList<SimContact> loadSimContactsWithExistingContactIds(SimCard sim) {
+ return getSimContactsWithRawContacts(sim);
+ }
+
+ public ContentProviderResult[] importContacts(List<SimContact> contacts,
+ AccountWithDataSet targetAccount)
+ throws RemoteException, OperationApplicationException {
+ if (contacts.size() < IMPORT_MAX_BATCH_SIZE) {
+ return importBatch(contacts, targetAccount);
+ }
+ final List<ContentProviderResult> results = new ArrayList<>();
+ for (int i = 0; i < contacts.size(); i += IMPORT_MAX_BATCH_SIZE) {
+ results.addAll(Arrays.asList(importBatch(
+ contacts.subList(i, Math.min(contacts.size(), i + IMPORT_MAX_BATCH_SIZE)),
+ targetAccount)));
+ }
+ return results.toArray(new ContentProviderResult[results.size()]);
+ }
+
+ public void persistSimState(SimCard sim) {
+ SharedPreferenceUtil.persistSimStates(mContext, Collections.singletonList(sim));
+ }
+
+ public void persistSimStates(List<SimCard> simCards) {
+ SharedPreferenceUtil.persistSimStates(mContext, simCards);
+ }
+
+ public SimCard getFirstSimCard() {
+ return getSimBySubscriptionId(SimCard.NO_SUBSCRIPTION_ID);
+ }
+
+ public SimCard getSimBySubscriptionId(int subscriptionId) {
+ final List<SimCard> sims = getSimCards();
+ if (subscriptionId == SimCard.NO_SUBSCRIPTION_ID && !sims.isEmpty()) {
+ return sims.get(0);
+ }
+ for (SimCard sim : getSimCards()) {
+ if (sim.getSubscriptionId() == subscriptionId) {
+ return sim;
+ }
+ }
+ return null;
+ }
+
+ private ContentProviderResult[] importBatch(List<SimContact> contacts,
+ AccountWithDataSet targetAccount)
+ throws RemoteException, OperationApplicationException {
+ final ArrayList<ContentProviderOperation> ops =
+ createImportOperations(contacts, targetAccount);
+ return mResolver.applyBatch(ContactsContract.AUTHORITY, ops);
+ }
+
+ @TargetApi(Build.VERSION_CODES.LOLLIPOP_MR1)
+ private List<SimCard> getSimCardsFromSubscriptions() {
+ final SubscriptionManager subscriptionManager = (SubscriptionManager)
+ mContext.getSystemService(Context.TELEPHONY_SUBSCRIPTION_SERVICE);
+ final List<SubscriptionInfo> subscriptions = subscriptionManager
+ .getActiveSubscriptionInfoList();
+ if (subscriptions == null) {
+ return Collections.emptyList();
+ }
+ final ArrayList<SimCard> result = new ArrayList<>();
+ for (SubscriptionInfo subscriptionInfo : subscriptions) {
+ result.add(SimCard.create(subscriptionInfo));
+ }
+ return result;
+ }
+
+ private List<SimContact> getContactsForSim(SimCard sim) {
+ final List<SimContact> contacts = sim.getContacts();
+ return contacts != null ? contacts : loadContactsForSim(sim);
}
private ArrayList<SimContact> loadFrom(Uri uri) {
@@ -171,37 +252,82 @@
return result;
}
- public ContentProviderResult[] importContacts(List<SimContact> contacts,
- AccountWithDataSet targetAccount)
- throws RemoteException, OperationApplicationException {
- final ArrayList<ContentProviderOperation> ops =
- createImportOperations(contacts, targetAccount);
- return mResolver.applyBatch(ContactsContract.AUTHORITY, ops);
- }
-
- public void persistSimState(SimCard sim) {
- SharedPreferenceUtil.persistSimStates(mContext, Collections.singletonList(sim));
- }
-
- public void persistSimStates(List<SimCard> simCards) {
- SharedPreferenceUtil.persistSimStates(mContext, simCards);
- }
-
- public SimCard getFirstSimCard() {
- return getSimBySubscriptionId(SimCard.NO_SUBSCRIPTION_ID);
- }
-
- public SimCard getSimBySubscriptionId(int subscriptionId) {
- final List<SimCard> sims = getSimCards();
- if (subscriptionId == SimCard.NO_SUBSCRIPTION_ID && !sims.isEmpty()) {
- return sims.get(0);
+ private ArrayList<SimContact> getSimContactsWithRawContacts(SimCard sim) {
+ final ArrayList<SimContact> contacts = new ArrayList<>(getContactsForSim(sim));
+ for (int i = 0; i < contacts.size(); i += DataQuery.MAX_BATCH_SIZE) {
+ final List<SimContact> batch =
+ contacts.subList(i, Math.min(i + DataQuery.MAX_BATCH_SIZE, contacts.size()));
+ setRawContactsForSimContacts(batch);
}
- for (SimCard sim : getSimCards()) {
- if (sim.getSubscriptionId() == subscriptionId) {
- return sim;
+ // Restore default sort order
+ Collections.sort(contacts, SimContact.compareById());
+ return contacts;
+ }
+
+ private void setRawContactsForSimContacts(List<SimContact> contacts) {
+ final StringBuilder selectionBuilder = new StringBuilder();
+
+ int phoneCount = 0;
+ for (SimContact contact : contacts) {
+ if (contact.hasPhone()) {
+ phoneCount++;
}
}
- return null;
+ List<String> selectionArgs = new ArrayList<>(phoneCount + 1);
+
+ selectionBuilder.append(ContactsContract.Data.MIMETYPE).append("=? AND ");
+ selectionArgs.add(Phone.CONTENT_ITEM_TYPE);
+
+ selectionBuilder.append(Phone.NUMBER).append(" IN (")
+ .append(Joiner.on(',').join(Collections.nCopies(phoneCount, '?')))
+ .append(")");
+ for (SimContact contact : contacts) {
+ if (contact.hasPhone()) {
+ selectionArgs.add(contact.getPhone());
+ }
+ }
+
+ final Cursor cursor = mResolver.query(ContactsContract.Data.CONTENT_URI.buildUpon()
+ .appendQueryParameter(ContactsContract.Data.VISIBLE_CONTACTS_ONLY, "true")
+ .build(),
+ DataQuery.PROJECTION,
+ selectionBuilder.toString(),
+ selectionArgs.toArray(new String[selectionArgs.size()]),
+ ContactsContract.Data.RAW_CONTACT_ID + " ASC");
+
+ if (cursor == null) {
+ initializeRawContactIds(contacts);
+ return;
+ }
+
+ try {
+ setRawContactsForSimContacts(contacts, cursor);
+ } finally {
+ cursor.close();
+ }
+ }
+
+ private void initializeRawContactIds(List<SimContact> contacts) {
+ for (int i = 0; i < contacts.size(); i++) {
+ contacts.set(i, contacts.get(i).withRawContactId(SimContact.NO_EXISTING_CONTACT));
+ }
+ }
+
+ private void setRawContactsForSimContacts(List<SimContact> contacts, Cursor cursor) {
+ initializeRawContactIds(contacts);
+ Collections.sort(contacts, SimContact.compareByPhoneThenName());
+
+ while (cursor.moveToNext()) {
+ final String number = DataQuery.getPhoneNumber(cursor);
+ final String name = DataQuery.getDisplayName(cursor);
+
+ int index = SimContact.findByPhoneAndName(contacts, number, name);
+ if (index < 0) {
+ continue;
+ }
+ final SimContact contact = contacts.get(index);
+ contacts.set(index, contact.withRawContactId(DataQuery.getRawContactId(cursor)));
+ }
}
private ArrayList<ContentProviderOperation> createImportOperations(List<SimContact> contacts,
@@ -242,10 +368,10 @@
new SimContact(1, "John Sim", "15095550121", null),
new SimContact(2, "Bob Sim", "15095550122", null),
new SimContact(3, "Mary Sim", "15095550123", null),
- new SimContact(4, "Alice Sim", "15095550124", null)
+ new SimContact(4, "Alice Sim", "15095550124", null),
+ new SimContact(5, "Sim Duplicate", "15095550121", null)
));
}
-
return new SimContactDao(context);
}
@@ -290,4 +416,31 @@
return true;
}
}
+
+ // Query used for detecting existing contacts that may match a SimContact.
+ private static final class DataQuery {
+ // How many SIM contacts to consider in a single query. This prevents hitting the SQLite
+ // query parameter limit.
+ static final int MAX_BATCH_SIZE = 100;
+
+ public static final String[] PROJECTION = new String[] {
+ ContactsContract.Data.RAW_CONTACT_ID, Phone.NUMBER, Phone.DISPLAY_NAME
+ };
+
+ public static final int RAW_CONTACT_ID = 0;
+ public static final int PHONE_NUMBER = 1;
+ public static final int DISPLAY_NAME = 2;
+
+ public static long getRawContactId(Cursor cursor) {
+ return cursor.getLong(RAW_CONTACT_ID);
+ }
+
+ public static String getPhoneNumber(Cursor cursor) {
+ return cursor.getString(PHONE_NUMBER);
+ }
+
+ public static String getDisplayName(Cursor cursor) {
+ return cursor.getString(DISPLAY_NAME);
+ }
+ }
}
diff --git a/src/com/android/contacts/common/list/ContactListItemView.java b/src/com/android/contacts/common/list/ContactListItemView.java
index f74c671..6e742a3 100644
--- a/src/com/android/contacts/common/list/ContactListItemView.java
+++ b/src/com/android/contacts/common/list/ContactListItemView.java
@@ -31,6 +31,7 @@
import android.provider.ContactsContract.Contacts;
import android.provider.ContactsContract.SearchSnippets;
import android.support.v4.content.ContextCompat;
+import android.support.v4.content.res.ResourcesCompat;
import android.support.v4.graphics.drawable.DrawableCompat;
import android.support.v7.widget.AppCompatCheckBox;
import android.support.v7.widget.AppCompatImageButton;
@@ -1156,7 +1157,8 @@
mNameTextView = new TextView(getContext());
mNameTextView.setSingleLine(true);
mNameTextView.setEllipsize(getTextEllipsis());
- mNameTextView.setTextColor(mNameTextViewTextColor);
+ mNameTextView.setTextColor(ResourcesCompat.getColorStateList(getResources(),
+ R.color.contact_list_name_text_color, getContext().getTheme()));
mNameTextView.setTextSize(TypedValue.COMPLEX_UNIT_PX, mNameTextViewTextSize);
// Manually call setActivated() since this view may be added after the first
// setActivated() call toward this whole item view.
diff --git a/src/com/android/contacts/common/model/SimCard.java b/src/com/android/contacts/common/model/SimCard.java
index 9b5486b..3c27cfe 100644
--- a/src/com/android/contacts/common/model/SimCard.java
+++ b/src/com/android/contacts/common/model/SimCard.java
@@ -22,10 +22,8 @@
import android.telephony.TelephonyManager;
import android.util.Log;
-import com.android.contacts.common.database.SimContactDao;
-
+import java.util.ArrayList;
import java.util.Arrays;
-import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
@@ -65,7 +63,9 @@
mCountryCode = other.mCountryCode;
mDismissed = other.mDismissed;
mImported = other.mImported;
- mContacts = other.mContacts;
+ if (other.mContacts != null) {
+ mContacts = new ArrayList<>(other.mContacts);
+ }
}
public SimCard(String simId, int subscriptionId, CharSequence carrierName,
@@ -91,6 +91,10 @@
return mSubscriptionId;
}
+ public boolean hasValidSubscriptionId() {
+ return mSubscriptionId != NO_SUBSCRIPTION_ID;
+ }
+
public CharSequence getDisplayName() {
return mDisplayName;
}
@@ -106,6 +110,10 @@
return PhoneNumberUtils.formatNumber(mPhoneNumber, mCountryCode);
}
+ public String getCountryCode() {
+ return mCountryCode;
+ }
+
public boolean hasContacts() {
if (mContacts == null) {
throw new IllegalStateException("Contacts not loaded.");
@@ -137,24 +145,9 @@
}
public List<SimContact> getContacts() {
- if (mContacts == null) {
- throw new IllegalStateException("Contacts not loaded.");
- }
return mContacts;
}
- public synchronized void loadContacts(SimContactDao dao) {
- if (mSubscriptionId == NO_SUBSCRIPTION_ID) {
- // Load from the default SIM card.
- mContacts = dao.loadSimContacts();
- } else {
- mContacts = dao.loadSimContacts(mSubscriptionId);
- }
- if (mContacts == null) {
- mContacts = Collections.emptyList();
- }
- }
-
public SimCard withImportAndDismissStates(boolean imported, boolean dismissed) {
SimCard copy = new SimCard(this);
copy.mImported = imported;
@@ -170,6 +163,12 @@
return withImportAndDismissStates(dismissed, mImported);
}
+ public SimCard withContacts(List<SimContact> contacts) {
+ final SimCard copy = new SimCard(this);
+ copy.mContacts = contacts;
+ return copy;
+ }
+
public SimCard withContacts(SimContact... contacts) {
final SimCard copy = new SimCard(this);
copy.mContacts = Arrays.asList(contacts);
@@ -206,10 +205,15 @@
info.getCountryIso());
}
- public static SimCard create(TelephonyManager telephony) {
- SimCard simCard = new SimCard(telephony.getSimSerialNumber(),
- telephony.getNetworkOperatorName(), "SIM", telephony.getLine1Number(),
- telephony.getNetworkCountryIso());
- return simCard;
+ public static SimCard create(TelephonyManager telephony, String displayLabel) {
+ if (telephony.getSimState() == TelephonyManager.SIM_STATE_READY) {
+ return new SimCard(telephony.getSimSerialNumber(),
+ telephony.getSimOperatorName(), displayLabel, telephony.getLine1Number(),
+ telephony.getSimCountryIso());
+ } else {
+ // This should never happen but in case it does just fallback to an "empty" instance
+ return new SimCard(/* SIM id */ "", /* operator name */ null, displayLabel,
+ /* phone number */ "", /* Country code */ null);
+ }
}
}
diff --git a/src/com/android/contacts/common/model/SimContact.java b/src/com/android/contacts/common/model/SimContact.java
index a13ef67..3768503 100644
--- a/src/com/android/contacts/common/model/SimContact.java
+++ b/src/com/android/contacts/common/model/SimContact.java
@@ -26,27 +26,41 @@
import android.provider.ContactsContract.CommonDataKinds.StructuredName;
import com.android.contacts.common.model.account.AccountWithDataSet;
+import com.google.common.collect.ComparisonChain;
+import com.google.common.collect.Ordering;
import java.util.Arrays;
import java.util.Collection;
+import java.util.Collections;
+import java.util.Comparator;
import java.util.List;
+import java.util.Objects;
/**
* Holds data for contacts loaded from the SIM card.
*/
public class SimContact implements Parcelable {
+ public static final long EXISTING_CONTACT_UNINITIALIZED = -2;
+ public static final long NO_EXISTING_CONTACT = -1;
+
private final long mId;
private final String mName;
private final String mPhone;
private final String[] mEmails;
+ private final long mRawContactId;
+
public SimContact(long id, String name, String phone, String[] emails) {
- this.mId = id;
- this.mName = name;
- this.mPhone = phone;
- this.mEmails = emails;
+ this(id, name, phone, emails, EXISTING_CONTACT_UNINITIALIZED);
}
+ public SimContact(long id, String name, String phone, String[] emails, long rawContactId) {
+ mId = id;
+ mName = name;
+ mPhone = phone;
+ mEmails = emails;
+ mRawContactId = rawContactId;
+ }
public long getId() {
return mId;
}
@@ -63,6 +77,13 @@
return mEmails;
}
+ public boolean existsInContacts() {
+ if (mRawContactId == EXISTING_CONTACT_UNINITIALIZED) {
+ throw new IllegalStateException("Raw contact ID is uninitialized");
+ }
+ return mRawContactId > 0;
+ }
+
public void appendCreateContactOperations(List<ContentProviderOperation> ops,
AccountWithDataSet targetAccount) {
// nothing to save.
@@ -106,6 +127,22 @@
.add(ContactsContract.Contacts.LOOKUP_KEY, getLookupKey());
}
+ public boolean hasName() {
+ return mName != null;
+ }
+
+ public boolean hasPhone() {
+ return mPhone != null;
+ }
+
+ public boolean hasEmails() {
+ return mEmails != null && mEmails.length > 0;
+ }
+
+ public SimContact withRawContactId(long id) {
+ return new SimContact(mId, mName, mPhone, mEmails, id);
+ }
+
/**
* Generate a "fake" lookup key. This is needed because
* {@link com.android.contacts.common.ContactPhotoManager} will only generate a letter avatar
@@ -128,6 +165,7 @@
", mName='" + mName + '\'' +
", mPhone='" + mPhone + '\'' +
", mEmails=" + Arrays.toString(mEmails) +
+ ", mRawContactId=" + mRawContactId +
'}';
}
@@ -138,10 +176,9 @@
final SimContact that = (SimContact) o;
- if (mId != that.mId) return false;
- if (mName != null ? !mName.equals(that.mName) : that.mName != null) return false;
- if (mPhone != null ? !mPhone.equals(that.mPhone) : that.mPhone != null) return false;
- return Arrays.equals(mEmails, that.mEmails);
+ return mId == that.mId && mRawContactId == that.mRawContactId
+ && Objects.equals(mName, that.mName) && Objects.equals(mPhone, that.mPhone)
+ && Arrays.equals(mEmails, that.mEmails);
}
@Override
@@ -150,6 +187,7 @@
result = 31 * result + (mName != null ? mName.hashCode() : 0);
result = 31 * result + (mPhone != null ? mPhone.hashCode() : 0);
result = 31 * result + Arrays.hashCode(mEmails);
+ result = 31 * result + (int) (mRawContactId ^ (mRawContactId >>> 32));
return result;
}
@@ -164,6 +202,7 @@
dest.writeString(mName);
dest.writeString(mPhone);
dest.writeStringArray(mEmails);
+ dest.writeLong(mRawContactId);
}
/**
@@ -185,11 +224,12 @@
public static final Creator<SimContact> CREATOR = new Creator<SimContact>() {
@Override
public SimContact createFromParcel(Parcel source) {
- long id = source.readLong();
- String name = source.readString();
- String phone = source.readString();
- String[] emails = source.createStringArray();
- return new SimContact(id, name, phone, emails);
+ final long id = source.readLong();
+ final String name = source.readString();
+ final String phone = source.readString();
+ final String[] emails = source.createStringArray();
+ final long contactId = source.readLong();
+ return new SimContact(id, name, phone, emails, contactId);
}
@Override
@@ -197,4 +237,39 @@
return new SimContact[size];
}
};
+
+ /**
+ * Returns the index of a contact with a matching name and phone
+ * @param contacts list to search. Should be sorted using
+ * {@link SimContact#compareByPhoneThenName()}
+ * @param phone the phone to search for
+ * @param name the name to search for
+ */
+ public static int findByPhoneAndName(List<SimContact> contacts, String phone, String name) {
+ return Collections.binarySearch(contacts, new SimContact(-1, name, phone, null,
+ NO_EXISTING_CONTACT), compareByPhoneThenName());
+ }
+
+ public static final Comparator<SimContact> compareByPhoneThenName() {
+ return new Comparator<SimContact>() {
+ @Override
+ public int compare(SimContact lhs, SimContact rhs) {
+ return ComparisonChain.start()
+ .compare(lhs.mPhone, rhs.mPhone,
+ Ordering.<String>natural().nullsFirst())
+ .compare(lhs.mName, rhs.mName, Ordering.<String>natural().nullsFirst())
+ .result();
+ }
+ };
+ }
+
+ public static final Comparator<SimContact> compareById() {
+ return new Comparator<SimContact>() {
+ @Override
+ public int compare(SimContact lhs, SimContact rhs) {
+ // We assume ids are unique.
+ return Long.compare(lhs.mId, rhs.mId);
+ }
+ };
+ }
}
diff --git a/src/com/android/contacts/common/preference/DisplayOptionsPreferenceFragment.java b/src/com/android/contacts/common/preference/DisplayOptionsPreferenceFragment.java
index 1b9818f..901a15e 100644
--- a/src/com/android/contacts/common/preference/DisplayOptionsPreferenceFragment.java
+++ b/src/com/android/contacts/common/preference/DisplayOptionsPreferenceFragment.java
@@ -268,12 +268,13 @@
getPreferenceScreen().removePreference(findPreference(KEY_DISPLAY_ORDER));
}
- // Remove the "Default account" setting if there aren't any writable accounts
+ // Remove the default account and custom view settings there aren't any writable accounts
final AccountTypeManager accountTypeManager = AccountTypeManager.getInstance(getContext());
final List<AccountWithDataSet> accounts = accountTypeManager.getAccounts(
/* contactWritableOnly */ true);
if (accounts.isEmpty()) {
getPreferenceScreen().removePreference(findPreference(KEY_DEFAULT_ACCOUNT));
+ getPreferenceScreen().removePreference(findPreference(KEY_CUSTOM_CONTACTS_FILTER));
}
final boolean isPhone = TelephonyManagerCompat.isVoiceCapable(
diff --git a/src/com/android/contacts/group/GroupMembersFragment.java b/src/com/android/contacts/group/GroupMembersFragment.java
index 4f8f6a8..e71b3a4 100644
--- a/src/com/android/contacts/group/GroupMembersFragment.java
+++ b/src/com/android/contacts/group/GroupMembersFragment.java
@@ -398,7 +398,7 @@
}
private void sendToGroup(long[] ids, String sendScheme, String title) {
- if(ids == null || ids.length == 0) return;
+ if (ids == null || ids.length == 0) return;
// Get emails or phone numbers
// contactMap <contact_id, contact_data>
diff --git a/src/com/android/contacts/util/SharedPreferenceUtil.java b/src/com/android/contacts/util/SharedPreferenceUtil.java
index cee54b9..a1767fa 100644
--- a/src/com/android/contacts/util/SharedPreferenceUtil.java
+++ b/src/com/android/contacts/util/SharedPreferenceUtil.java
@@ -157,15 +157,19 @@
final Set<String> imported = new HashSet<>(getImportedSims(context));
final Set<String> dismissed = new HashSet<>(getDismissedSims(context));
for (SimCard sim : sims) {
+ final String id = sim.getSimId();
+ if (id == null) {
+ continue;
+ }
if (sim.isImported()) {
- imported.add(sim.getSimId());
+ imported.add(id);
} else {
- imported.remove(sim.getSimId());
+ imported.remove(id);
}
if (sim.isDismissed()) {
- dismissed.add(sim.getSimId());
+ dismissed.add(id);
} else {
- dismissed.remove(sim.getSimId());
+ dismissed.remove(id);
}
}
getSharedPreferences(context).edit()
diff --git a/tests/src/com/android/contacts/common/model/SimContactTests.java b/tests/src/com/android/contacts/common/model/SimContactTests.java
index de9ab5a..c37c270 100644
--- a/tests/src/com/android/contacts/common/model/SimContactTests.java
+++ b/tests/src/com/android/contacts/common/model/SimContactTests.java
@@ -21,7 +21,7 @@
public void parcelRoundtrip() {
assertParcelsCorrectly(new SimContact(1, "name1", "phone1",
new String[] { "email1a", "email1b" }));
- assertParcelsCorrectly(new SimContact(2, "name2", "phone2", null));
+ assertParcelsCorrectly(new SimContact(2, "name2", "phone2", null, 2));
assertParcelsCorrectly(new SimContact(3, "name3", null,
new String[] { "email3" }));
assertParcelsCorrectly(new SimContact(4, null, "phone4",
diff --git a/tests/src/com/android/contacts/tests/AdbHelpers.java b/tests/src/com/android/contacts/tests/AdbHelpers.java
index 0024dd1..163d7cc 100644
--- a/tests/src/com/android/contacts/tests/AdbHelpers.java
+++ b/tests/src/com/android/contacts/tests/AdbHelpers.java
@@ -16,10 +16,8 @@
package com.android.contacts.tests;
import android.content.Context;
-import android.content.SharedPreferences;
import android.os.Build;
import android.os.Bundle;
-import android.preference.PreferenceManager;
import android.support.annotation.RequiresApi;
import android.support.test.InstrumentationRegistry;
import android.util.Log;