Merge changes from topics 'sim-tests', 'import-notification' into ub-contactsdialer-i-dev
* changes:
Add additional tests of SIM contact importing
DO NOT MERGE Improve testability of SIM import code.
DO NOT MERGE Add notification for SIM import
diff --git a/AndroidManifest_common.xml b/AndroidManifest_common.xml
index b2a3c88..2a0e3a0 100644
--- a/AndroidManifest_common.xml
+++ b/AndroidManifest_common.xml
@@ -394,6 +394,11 @@
android:name=".ContactSaveService"
android:exported="false" />
+ <!-- Service to import contacts from the SIM card -->
+ <service
+ android:name=".SimImportService"
+ android:exported="false" />
+
<!-- Attaches a photo to a contact. Started from external applications -->
<activity android:name=".activities.AttachPhotoActivity"
android:label="@string/attach_photo_dialog_title"
diff --git a/res/values/strings.xml b/res/values/strings.xml
index f88be29..5a0610b 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -1904,4 +1904,13 @@
[CHAR LIMIT=NONE] -->
<string name="show_more_content_description">Show more</string>
+ <!-- The notification title shown when importing a SIM card finishes [CHAR LIMIT=40] -->
+ <string name="importing_sim_finished_title">Finished importing SIM card</string>
+
+ <!-- Notification title shown when importing SIM contacts failed [CHAR LIMIT=40] -->
+ <string name="importing_sim_failed_title">Couldn\'t import SIM card</string>
+
+ <!-- The notification title shown while SIM contacts are being imported [CHAR LIMIT=40] -->
+ <string name="importing_sim_in_progress_title">Importing SIM</string>
+
</resources>
diff --git a/src/com/android/contacts/ContactSaveService.java b/src/com/android/contacts/ContactSaveService.java
index 6c44a42..76a77c0 100755
--- a/src/com/android/contacts/ContactSaveService.java
+++ b/src/com/android/contacts/ContactSaveService.java
@@ -16,8 +16,6 @@
package com.android.contacts;
-import static android.Manifest.permission.WRITE_CONTACTS;
-
import android.app.Activity;
import android.app.IntentService;
import android.content.ContentProviderOperation;
@@ -49,7 +47,6 @@
import android.provider.ContactsContract.RawContactsEntity;
import android.support.v4.content.LocalBroadcastManager;
import android.support.v4.os.ResultReceiver;
-import android.telephony.SubscriptionInfo;
import android.text.TextUtils;
import android.util.Log;
import android.widget.Toast;
@@ -63,8 +60,6 @@
import com.android.contacts.common.model.RawContactDelta;
import com.android.contacts.common.model.RawContactDeltaList;
import com.android.contacts.common.model.RawContactModifier;
-import com.android.contacts.common.model.SimCard;
-import com.android.contacts.common.model.SimContact;
import com.android.contacts.common.model.account.AccountWithDataSet;
import com.android.contacts.common.preference.ContactsPreferences;
import com.android.contacts.common.util.ContactDisplayUtils;
@@ -72,7 +67,6 @@
import com.android.contacts.compat.PinnedPositionsCompat;
import com.android.contacts.util.ContactPhotoUtils;
import com.android.contactsbind.FeedbackHelper;
-
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
@@ -82,6 +76,8 @@
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
+import static android.Manifest.permission.WRITE_CONTACTS;
+
/**
* A service responsible for saving changes to the content provider.
*/
@@ -149,16 +145,11 @@
public static final String EXTRA_UNDO_ACTION = "undoAction";
public static final String EXTRA_UNDO_DATA = "undoData";
- public static final String ACTION_IMPORT_FROM_SIM = "importFromSim";
- public static final String EXTRA_SIM_CONTACTS = "simContacts";
- public static final String EXTRA_SIM_SUBSCRIPTION_ID = "simSubscriptionId";
-
// For debugging and testing what happens when requests are queued up.
public static final String ACTION_SLEEP = "sleep";
public static final String EXTRA_SLEEP_DURATION = "sleepDuration";
public static final String BROADCAST_GROUP_DELETED = "groupDeleted";
- public static final String BROADCAST_SIM_IMPORT_COMPLETE = "simImportComplete";
public static final String BROADCAST_LINK_COMPLETE = "linkComplete";
public static final String BROADCAST_UNLINK_COMPLETE = "unlinkComplete";
@@ -166,7 +157,6 @@
public static final String EXTRA_RESULT_CODE = "resultCode";
public static final String EXTRA_RESULT_COUNT = "count";
- public static final String EXTRA_OPERATION_REQUESTED_AT_TIME = "requestedTime";
public static final int CP2_ERROR = 0;
public static final int CONTACTS_LINKED = 1;
@@ -361,8 +351,6 @@
setRingtone(intent);
} else if (ACTION_UNDO.equals(action)) {
undo(intent);
- } else if (ACTION_IMPORT_FROM_SIM.equals(action)) {
- importFromSim(intent);
} else if (ACTION_SLEEP.equals(action)) {
sleepForDebugging(intent);
}
@@ -1752,58 +1740,6 @@
}
/**
- * Returns an intent that can be used to import the contacts into targetAccount.
- *
- * @param context context to use for creating the intent
- * @param subscriptionId the subscriptionId of the SIM card that is being imported. See
- * {@link SubscriptionInfo#getSubscriptionId()}. Upon completion the
- * SIM for that subscription ID will be marked as imported
- * @param contacts the contacts to import
- * @param targetAccount the account import the contacts into
- */
- public static Intent createImportFromSimIntent(Context context, int subscriptionId,
- ArrayList<SimContact> contacts, AccountWithDataSet targetAccount) {
- return new Intent(context, ContactSaveService.class)
- .setAction(ACTION_IMPORT_FROM_SIM)
- .putExtra(EXTRA_SIM_CONTACTS, contacts)
- .putExtra(EXTRA_SIM_SUBSCRIPTION_ID, subscriptionId)
- .putExtra(EXTRA_ACCOUNT, targetAccount);
- }
-
- private void importFromSim(Intent intent) {
- final Intent result = new Intent(BROADCAST_SIM_IMPORT_COMPLETE)
- .putExtra(EXTRA_OPERATION_REQUESTED_AT_TIME, System.currentTimeMillis());
- final int subscriptionId = intent.getIntExtra(EXTRA_SIM_SUBSCRIPTION_ID,
- SimCard.NO_SUBSCRIPTION_ID);
- try {
- final AccountWithDataSet targetAccount = intent.getParcelableExtra(EXTRA_ACCOUNT);
- final ArrayList<SimContact> contacts =
- intent.getParcelableArrayListExtra(EXTRA_SIM_CONTACTS);
- mSimContactDao.importContacts(contacts, targetAccount);
-
- // Update the imported state of the SIM card that was imported
- final SimCard sim = mSimContactDao.getSimBySubscriptionId(subscriptionId);
- if (sim != null) {
- mSimContactDao.persistSimState(sim.withImportedState(true));
- }
-
- // notify success
- LocalBroadcastManager.getInstance(this).sendBroadcast(result
- .putExtra(EXTRA_RESULT_COUNT, contacts.size())
- .putExtra(EXTRA_RESULT_CODE, RESULT_SUCCESS)
- .putExtra(EXTRA_SIM_SUBSCRIPTION_ID, subscriptionId));
- if (Log.isLoggable(TAG, Log.DEBUG)) {
- Log.d(TAG, "importFromSim completed successfully");
- }
- } catch (RemoteException|OperationApplicationException e) {
- FeedbackHelper.sendFeedback(this, TAG, "Failed to import contacts from SIM card", e);
- LocalBroadcastManager.getInstance(this).sendBroadcast(result
- .putExtra(EXTRA_RESULT_CODE, RESULT_FAILURE)
- .putExtra(EXTRA_SIM_SUBSCRIPTION_ID, subscriptionId));
- }
- }
-
- /**
* Returns an intent that can start this service and cause it to sleep for the specified time.
*
* This exists purely for debugging and manual testing. Since this service uses a single thread
diff --git a/src/com/android/contacts/SimImportFragment.java b/src/com/android/contacts/SimImportFragment.java
index b512599..dc4cabf 100644
--- a/src/com/android/contacts/SimImportFragment.java
+++ b/src/com/android/contacts/SimImportFragment.java
@@ -281,10 +281,8 @@
importableContacts.add(mAdapter.getItem(checked.keyAt(i)));
}
}
- ContactSaveService.startService(getContext(), ContactSaveService
- .createImportFromSimIntent(getContext(), mSubscriptionId,
- importableContacts,
- mAccountHeaderPresenter.getCurrentAccount()));
+ SimImportService.startImport(getContext(), mSubscriptionId, importableContacts,
+ mAccountHeaderPresenter.getCurrentAccount());
}
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
diff --git a/src/com/android/contacts/SimImportService.java b/src/com/android/contacts/SimImportService.java
new file mode 100644
index 0000000..4fa3695
--- /dev/null
+++ b/src/com/android/contacts/SimImportService.java
@@ -0,0 +1,341 @@
+/*
+ * 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.
+ */
+package com.android.contacts;
+
+import android.app.Notification;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.app.Service;
+import android.content.Context;
+import android.content.Intent;
+import android.content.OperationApplicationException;
+import android.os.AsyncTask;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.support.annotation.Nullable;
+import android.support.v4.app.NotificationCompat;
+import android.support.v4.content.LocalBroadcastManager;
+import android.util.TimingLogger;
+
+import com.android.contacts.activities.PeopleActivity;
+import com.android.contacts.common.database.SimContactDao;
+import com.android.contacts.common.database.SimContactDaoImpl;
+import com.android.contacts.common.model.SimCard;
+import com.android.contacts.common.model.SimContact;
+import com.android.contacts.common.model.account.AccountWithDataSet;
+import com.android.contactsbind.FeedbackHelper;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+/**
+ * Imports {@link SimContact}s from a background thread
+ */
+public class SimImportService extends Service {
+
+ private static final String TAG = "SimImportService";
+
+ /**
+ * Wrapper around the service state for testability
+ */
+ public interface StatusProvider {
+
+ /**
+ * Returns whether there is any imports still pending
+ *
+ * <p>This should be called from the UI thread</p>
+ */
+ boolean isRunning();
+
+ /**
+ * Returns whether an import for sim has been requested
+ *
+ * <p>This should be called from the UI thread</p>
+ */
+ boolean isImporting(SimCard sim);
+ }
+
+ public static final String EXTRA_ACCOUNT = "account";
+ public static final String EXTRA_SIM_CONTACTS = "simContacts";
+ public static final String EXTRA_SIM_SUBSCRIPTION_ID = "simSubscriptionId";
+ public static final String EXTRA_RESULT_CODE = "resultCode";
+ public static final String EXTRA_RESULT_COUNT = "count";
+ public static final String EXTRA_OPERATION_REQUESTED_AT_TIME = "requestedTime";
+
+ public static final String BROADCAST_SERVICE_STATE_CHANGED =
+ SimImportService.class.getName() + "#serviceStateChanged";
+ public static final String BROADCAST_SIM_IMPORT_COMPLETE =
+ SimImportService.class.getName() + "#simImportComplete";
+
+ public static final int RESULT_UNKNOWN = 0;
+ public static final int RESULT_SUCCESS = 1;
+ public static final int RESULT_FAILURE = 2;
+
+ // VCardService uses jobIds for it's notifications which count up from 0 so we just use a
+ // bigger number to prevent overlap.
+ private static final int NOTIFICATION_ID = 100;
+
+ private ExecutorService mExecutor = Executors.newSingleThreadExecutor();
+
+ // Keeps track of current tasks. This is only modified from the UI thread.
+ private static List<ImportTask> sPending = new ArrayList<>();
+
+ private static StatusProvider sStatusProvider = new StatusProvider() {
+ @Override
+ public boolean isRunning() {
+ return !sPending.isEmpty();
+ }
+
+ @Override
+ public boolean isImporting(SimCard sim) {
+ return SimImportService.isImporting(sim);
+ }
+ };
+
+ /**
+ * Returns whether an import for sim has been requested
+ *
+ * <p>This should be called from the UI thread</p>
+ */
+ private static boolean isImporting(SimCard sim) {
+ for (ImportTask task : sPending) {
+ if (task.getSim().equals(sim)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ public static StatusProvider getStatusProvider() {
+ return sStatusProvider;
+ }
+
+ /**
+ * Starts an import of the contacts from the sim into the target account
+ *
+ * @param context context to use for starting the service
+ * @param subscriptionId the subscriptionId of the SIM card that is being imported. See
+ * {@link android.telephony.SubscriptionInfo#getSubscriptionId()}.
+ * Upon completion the SIM for that subscription ID will be marked as
+ * imported
+ * @param contacts the contacts to import
+ * @param targetAccount the account import the contacts into
+ */
+ public static void startImport(Context context, int subscriptionId,
+ ArrayList<SimContact> contacts, AccountWithDataSet targetAccount) {
+ context.startService(new Intent(context, SimImportService.class)
+ .putExtra(EXTRA_SIM_CONTACTS, contacts)
+ .putExtra(EXTRA_SIM_SUBSCRIPTION_ID, subscriptionId)
+ .putExtra(EXTRA_ACCOUNT, targetAccount));
+ }
+
+
+ @Nullable
+ @Override
+ public IBinder onBind(Intent intent) {
+ return null;
+ }
+
+ @Override
+ public int onStartCommand(Intent intent, int flags, final int startId) {
+ final ImportTask task = createTaskForIntent(intent, startId);
+ if (task == null) {
+ new StopTask(this, startId).executeOnExecutor(mExecutor);
+ return START_NOT_STICKY;
+ }
+ sPending.add(task);
+ task.executeOnExecutor(mExecutor);
+ notifyStateChanged();
+ return START_REDELIVER_INTENT;
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ mExecutor.shutdown();
+ }
+
+ private ImportTask createTaskForIntent(Intent intent, int startId) {
+ final AccountWithDataSet targetAccount = intent.getParcelableExtra(EXTRA_ACCOUNT);
+ final ArrayList<SimContact> contacts =
+ intent.getParcelableArrayListExtra(EXTRA_SIM_CONTACTS);
+
+ final int subscriptionId = intent.getIntExtra(EXTRA_SIM_SUBSCRIPTION_ID,
+ SimCard.NO_SUBSCRIPTION_ID);
+ final SimContactDao dao = SimContactDao.create(this);
+ final SimCard sim = dao.getSimBySubscriptionId(subscriptionId);
+ if (sim != null) {
+ return new ImportTask(sim, contacts, targetAccount, dao, startId);
+ } else {
+ return null;
+ }
+ }
+
+ private Notification getCompletedNotification() {
+ final Intent intent = new Intent(this, PeopleActivity.class);
+ final NotificationCompat.Builder builder = new NotificationCompat.Builder(this);
+ builder.setOngoing(false)
+ .setAutoCancel(true)
+ .setContentTitle(this.getString(R.string.importing_sim_finished_title))
+ .setColor(this.getResources().getColor(R.color.dialtacts_theme_color))
+ .setSmallIcon(R.drawable.ic_check_mark)
+ .setContentIntent(PendingIntent.getActivity(this, 0, intent, 0));
+ return builder.build();
+ }
+
+ private Notification getFailedNotification() {
+ final Intent intent = new Intent(this, PeopleActivity.class);
+ final NotificationCompat.Builder builder = new NotificationCompat.Builder(this);
+ builder.setOngoing(false)
+ .setAutoCancel(true)
+ .setContentTitle(this.getString(R.string.importing_sim_failed_title))
+ .setColor(this.getResources().getColor(R.color.dialtacts_theme_color))
+ .setSmallIcon(R.drawable.ic_check_mark)
+ .setContentIntent(PendingIntent.getActivity(this, 0, intent, 0));
+ return builder.build();
+ }
+
+ private Notification getImportingNotification() {
+ final NotificationCompat.Builder builder = new NotificationCompat.Builder(this);
+ final String description = getString(R.string.importing_sim_in_progress_title);
+ builder.setOngoing(true)
+ .setProgress(/* current */ 0, /* max */ 100, /* indeterminate */ true)
+ .setContentTitle(description)
+ .setColor(this.getResources().getColor(R.color.dialtacts_theme_color))
+ .setSmallIcon(android.R.drawable.stat_sys_download);
+ return builder.build();
+ }
+
+ private void notifyStateChanged() {
+ LocalBroadcastManager.getInstance(this).sendBroadcast(
+ new Intent(BROADCAST_SERVICE_STATE_CHANGED));
+ }
+
+ // Schedule a task that calls stopSelf when it completes. This is used to ensure that the
+ // calls to stopSelf occur in the correct order (because this service uses a single thread
+ // executor this won't run until all work that was requested before it has finished)
+ private static class StopTask extends AsyncTask<Void, Void, Void> {
+ private Service mHost;
+ private final int mStartId;
+
+ private StopTask(Service host, int startId) {
+ mHost = host;
+ mStartId = startId;
+ }
+
+ @Override
+ protected Void doInBackground(Void... params) {
+ return null;
+ }
+
+ @Override
+ protected void onPostExecute(Void aVoid) {
+ super.onPostExecute(aVoid);
+ mHost.stopSelf(mStartId);
+ }
+ }
+
+ private class ImportTask extends AsyncTask<Void, Void, Boolean> {
+ private final SimCard mSim;
+ private final List<SimContact> mContacts;
+ private final AccountWithDataSet mTargetAccount;
+ private final SimContactDao mDao;
+ private final NotificationManager mNotificationManager;
+ private final int mStartId;
+ private final long mStartTime;
+
+ public ImportTask(SimCard sim, List<SimContact> contacts, AccountWithDataSet targetAccount,
+ SimContactDao dao, int startId) {
+ mSim = sim;
+ mContacts = contacts;
+ mTargetAccount = targetAccount;
+ mDao = dao;
+ mNotificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
+ mStartId = startId;
+ mStartTime = System.currentTimeMillis();
+ }
+
+ @Override
+ protected void onPreExecute() {
+ super.onPreExecute();
+ startForeground(NOTIFICATION_ID, getImportingNotification());
+ }
+
+ @Override
+ protected Boolean doInBackground(Void... params) {
+ final TimingLogger timer = new TimingLogger(TAG, "import");
+ try {
+ // Just import them all at once.
+ // Experimented with using smaller batches (e.g. 25 and 50) so that percentage
+ // progress could be displayed however this slowed down the import by over a factor
+ // of 2. If the batch size is over a 100 then most cases will only require a single
+ // batch so we don't even worry about displaying accurate progress
+ mDao.importContacts(mContacts, mTargetAccount);
+ mDao.persistSimState(mSim.withImportedState(true));
+ timer.addSplit("done");
+ timer.dumpToLog();
+ } catch (RemoteException|OperationApplicationException e) {
+ FeedbackHelper.sendFeedback(SimImportService.this, TAG,
+ "Failed to import contacts from SIM card", e);
+ return false;
+ }
+ return true;
+ }
+
+ public SimCard getSim() {
+ return mSim;
+ }
+
+ @Override
+ protected void onPostExecute(Boolean success) {
+ super.onPostExecute(success);
+ stopSelf(mStartId);
+
+ Intent result;
+ final Notification notification;
+ if (success) {
+ result = new Intent(BROADCAST_SIM_IMPORT_COMPLETE)
+ .putExtra(EXTRA_RESULT_CODE, RESULT_SUCCESS)
+ .putExtra(EXTRA_RESULT_COUNT, mContacts.size())
+ .putExtra(EXTRA_OPERATION_REQUESTED_AT_TIME, mStartTime)
+ .putExtra(EXTRA_SIM_SUBSCRIPTION_ID, mSim.getSubscriptionId());
+
+ notification = getCompletedNotification();
+ } else {
+ result = new Intent(BROADCAST_SIM_IMPORT_COMPLETE)
+ .putExtra(EXTRA_RESULT_CODE, RESULT_FAILURE)
+ .putExtra(EXTRA_OPERATION_REQUESTED_AT_TIME, mStartTime)
+ .putExtra(EXTRA_SIM_SUBSCRIPTION_ID, mSim.getSubscriptionId());
+
+ notification = getFailedNotification();
+ }
+ LocalBroadcastManager.getInstance(SimImportService.this).sendBroadcast(result);
+
+ sPending.remove(this);
+
+ // Only notify of completion if all the import requests have finished. We're using
+ // the same notification for imports so in the rare case that a user has started
+ // multiple imports the notification won't go away until all of them complete.
+ if (sPending.isEmpty()) {
+ stopForeground(false);
+ mNotificationManager.notify(NOTIFICATION_ID, notification);
+ }
+ notifyStateChanged();
+ }
+ }
+}
diff --git a/src/com/android/contacts/common/database/SimContactDao.java b/src/com/android/contacts/common/database/SimContactDao.java
index b5b6626..b7b3ae4 100644
--- a/src/com/android/contacts/common/database/SimContactDao.java
+++ b/src/com/android/contacts/common/database/SimContactDao.java
@@ -15,46 +15,19 @@
*/
package com.android.contacts.common.database;
-import android.annotation.TargetApi;
-import android.content.ContentProviderOperation;
import android.content.ContentProviderResult;
-import android.content.ContentResolver;
import android.content.Context;
import android.content.OperationApplicationException;
-import android.content.pm.PackageManager;
-import android.database.Cursor;
-import android.net.Uri;
-import android.os.Build;
import android.os.RemoteException;
-import android.provider.BaseColumns;
-import android.provider.ContactsContract;
-import android.provider.ContactsContract.CommonDataKinds.Phone;
-import android.provider.ContactsContract.CommonDataKinds.StructuredName;
-import android.provider.ContactsContract.Data;
-import android.provider.ContactsContract.RawContacts;
-import android.support.annotation.VisibleForTesting;
-import android.support.v4.util.ArrayMap;
-import android.support.v4.util.ArraySet;
-import android.telephony.SubscriptionInfo;
-import android.telephony.SubscriptionManager;
-import android.telephony.TelephonyManager;
-import android.text.TextUtils;
import android.util.SparseArray;
-import com.android.contacts.R;
-import com.android.contacts.common.compat.CompatUtils;
import com.android.contacts.common.model.SimCard;
import com.android.contacts.common.model.SimContact;
import com.android.contacts.common.model.account.AccountWithDataSet;
-import com.android.contacts.common.util.PermissionsUtil;
import com.android.contacts.util.SharedPreferenceUtil;
-import com.google.common.base.Joiner;
import java.util.ArrayList;
-import java.util.Arrays;
import java.util.Collections;
-import java.util.HashMap;
-import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
@@ -63,349 +36,15 @@
* Provides data access methods for loading contacts from a SIM card and and migrating these
* SIM contacts to a CP2 account.
*/
-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;
-
- // How many SIM contacts to consider in a single query. This prevents hitting the SQLite
- // query parameter limit.
- static final int QUERY_MAX_BATCH_SIZE = 100;
+public abstract class SimContactDao {
// 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;
- @VisibleForTesting
- public static final Uri ICC_CONTENT_URI = Uri.parse("content://icc/adn");
-
- public static String _ID = BaseColumns._ID;
- public static String NAME = "name";
- public static String NUMBER = "number";
- public static String EMAILS = "emails";
-
- private final Context mContext;
- private final ContentResolver mResolver;
- private final TelephonyManager mTelephonyManager;
-
- private SimContactDao(Context context) {
- mContext = context;
- mResolver = context.getContentResolver();
- mTelephonyManager = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
- }
-
-
- 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_READY;
- }
-
- public List<SimCard> getSimCards() {
- if (!canReadSimContacts()) {
- return Collections.emptyList();
- }
- final List<SimCard> sims = CompatUtils.isMSIMCompatible() ?
- getSimCardsFromSubscriptions() :
- Collections.singletonList(SimCard.create(mTelephonyManager,
- mContext.getString(R.string.single_sim_display_label)));
- return SharedPreferenceUtil.restoreSimStates(mContext, sims);
- }
-
- 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")
- .appendPath(String.valueOf(subscriptionId))
- .build());
- }
-
- public ArrayList<SimContact> loadSimContacts() {
- return loadFrom(ICC_CONTENT_URI);
- }
-
- 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 = SharedPreferenceUtil.restoreSimStates(mContext, 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;
- }
-
- /**
- * Finds SIM contacts that exist in CP2 and associates the account of the CP2 contact with
- * the SIM contact
- */
- public Map<AccountWithDataSet, Set<SimContact>> findAccountsOfExistingSimContacts(
- List<SimContact> contacts) {
- final Map<AccountWithDataSet, Set<SimContact>> result = new ArrayMap<>();
- for (int i = 0; i < contacts.size(); i += QUERY_MAX_BATCH_SIZE) {
- findAccountsOfExistingSimContacts(
- contacts.subList(i, Math.min(contacts.size(), i + QUERY_MAX_BATCH_SIZE)),
- result);
- }
- return result;
- }
-
- private void findAccountsOfExistingSimContacts(List<SimContact> contacts,
- Map<AccountWithDataSet, Set<SimContact>> result) {
- final Map<Long, List<SimContact>> rawContactToSimContact = new HashMap<>();
- Collections.sort(contacts, SimContact.compareByPhoneThenName());
-
- final Cursor dataCursor = queryRawContactsForSimContacts(contacts);
-
- try {
- while (dataCursor.moveToNext()) {
- final String number = DataQuery.getPhoneNumber(dataCursor);
- final String name = DataQuery.getDisplayName(dataCursor);
-
- final int index = SimContact.findByPhoneAndName(contacts, number, name);
- if (index < 0) {
- continue;
- }
- final SimContact contact = contacts.get(index);
- final long id = DataQuery.getRawContactId(dataCursor);
- if (!rawContactToSimContact.containsKey(id)) {
- rawContactToSimContact.put(id, new ArrayList<SimContact>());
- }
- rawContactToSimContact.get(id).add(contact);
- }
- } finally {
- dataCursor.close();
- }
-
- final Cursor accountsCursor = queryAccountsOfRawContacts(rawContactToSimContact.keySet());
- try {
- while (accountsCursor.moveToNext()) {
- final AccountWithDataSet account = AccountQuery.getAccount(accountsCursor);
- final long id = AccountQuery.getId(accountsCursor);
- if (!result.containsKey(account)) {
- result.put(account, new ArraySet<SimContact>());
- }
- for (SimContact contact : rawContactToSimContact.get(id)) {
- result.get(account).add(contact);
- }
- }
- } finally {
- accountsCursor.close();
- }
- }
-
- 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();
- 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);
- }
-
- // See b/32831092
- // Sometimes the SIM contacts provider seems to get stuck if read from multiple threads
- // concurrently. So we just have a global lock around it to prevent potential issues.
- private static final Object SIM_READ_LOCK = new Object();
- private ArrayList<SimContact> loadFrom(Uri uri) {
- synchronized (SIM_READ_LOCK) {
- final Cursor cursor = mResolver.query(uri, null, null, null, null);
-
- try {
- return loadFromCursor(cursor);
- } finally {
- cursor.close();
- }
- }
- }
-
- private ArrayList<SimContact> loadFromCursor(Cursor cursor) {
- final int colId = cursor.getColumnIndex(_ID);
- final int colName = cursor.getColumnIndex(NAME);
- final int colNumber = cursor.getColumnIndex(NUMBER);
- final int colEmails = cursor.getColumnIndex(EMAILS);
-
- final ArrayList<SimContact> result = new ArrayList<>();
-
- while (cursor.moveToNext()) {
- final long id = cursor.getLong(colId);
- final String name = cursor.getString(colName);
- final String number = cursor.getString(colNumber);
- final String emails = cursor.getString(colEmails);
-
- final SimContact contact = new SimContact(id, name, number, parseEmails(emails));
- result.add(contact);
- }
- return result;
- }
-
- private Cursor queryRawContactsForSimContacts(List<SimContact> contacts) {
- final StringBuilder selectionBuilder = new StringBuilder();
-
- int phoneCount = 0;
- int nameCount = 0;
- for (SimContact contact : contacts) {
- if (contact.hasPhone()) {
- phoneCount++;
- } else if (contact.hasName()) {
- nameCount++;
- }
- }
- List<String> selectionArgs = new ArrayList<>(phoneCount + 1);
-
- selectionBuilder.append('(');
- selectionBuilder.append(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());
- }
- }
- selectionBuilder.append(')');
-
- if (nameCount > 0) {
- selectionBuilder.append(" OR (");
-
- selectionBuilder.append(Data.MIMETYPE).append("=? AND ");
- selectionArgs.add(StructuredName.CONTENT_ITEM_TYPE);
-
- selectionBuilder.append(Data.DISPLAY_NAME).append(" IN (")
- .append(Joiner.on(',').join(Collections.nCopies(nameCount, '?')))
- .append(')');
- for (SimContact contact : contacts) {
- if (!contact.hasPhone() && contact.hasName()) {
- selectionArgs.add(contact.getName());
- }
- }
- selectionBuilder.append(')');
- }
-
- return mResolver.query(Data.CONTENT_URI.buildUpon()
- .appendQueryParameter(Data.VISIBLE_CONTACTS_ONLY, "true")
- .build(),
- DataQuery.PROJECTION,
- selectionBuilder.toString(),
- selectionArgs.toArray(new String[selectionArgs.size()]),
- null);
- }
-
- private Cursor queryAccountsOfRawContacts(Set<Long> ids) {
- final StringBuilder selectionBuilder = new StringBuilder();
-
- final String[] args = new String[ids.size()];
-
- selectionBuilder.append(RawContacts._ID).append(" IN (")
- .append(Joiner.on(',').join(Collections.nCopies(args.length, '?')))
- .append(")");
- int i = 0;
- for (long id : ids) {
- args[i++] = String.valueOf(id);
- }
- return mResolver.query(RawContacts.CONTENT_URI,
- AccountQuery.PROJECTION,
- selectionBuilder.toString(),
- args,
- null);
- }
-
- private ArrayList<ContentProviderOperation> createImportOperations(List<SimContact> contacts,
- AccountWithDataSet targetAccount) {
- final ArrayList<ContentProviderOperation> ops = new ArrayList<>();
- for (SimContact contact : contacts) {
- contact.appendCreateContactOperations(ops, targetAccount);
- }
- return ops;
- }
-
- private String[] parseEmails(String emails) {
- return emails != null ? emails.split(",") : null;
- }
-
- private boolean hasTelephony() {
- return mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_TELEPHONY);
- }
-
- private boolean hasPermissions() {
- return PermissionsUtil.hasContactsPermissions(mContext) &&
- PermissionsUtil.hasPhonePermissions(mContext);
- }
-
public static SimContactDao create(Context context) {
if (USE_FAKE_INSTANCE) {
- return new DebugImpl(context)
+ return new SimContactDaoImpl.DebugImpl(context)
.addSimCard(new SimCard("fake-sim-id1", 1, "Fake Carrier",
"Card 1", "15095550101", "us").withContacts(
new SimContact(1, "Sim One", "15095550111", null),
@@ -423,89 +62,27 @@
new SimContact(5, "Sim Duplicate", "15095550121", null)
));
}
- return new SimContactDao(context);
+ return new SimContactDaoImpl(context);
}
- // TODO remove this class and the USE_FAKE_INSTANCE flag once this code is not under
- // active development or anytime after 3/1/2017
- public static class DebugImpl extends SimContactDao {
+ public abstract boolean canReadSimContacts();
- private List<SimCard> mSimCards = new ArrayList<>();
- private SparseArray<SimCard> mCardsBySubscription = new SparseArray<>();
+ public abstract List<SimCard> getSimCards();
- public DebugImpl(Context context) {
- super(context);
- }
+ public abstract ArrayList<SimContact> loadContactsForSim(SimCard sim);
- public DebugImpl addSimCard(SimCard sim) {
- mSimCards.add(sim);
- mCardsBySubscription.put(sim.getSubscriptionId(), sim);
- return this;
- }
+ public abstract ContentProviderResult[] importContacts(List<SimContact> contacts,
+ AccountWithDataSet targetAccount)
+ throws RemoteException, OperationApplicationException;
- @Override
- public List<SimCard> getSimCards() {
- return SharedPreferenceUtil.restoreSimStates(getContext(), mSimCards);
- }
+ public abstract void persistSimStates(List<SimCard> simCards);
- @Override
- public ArrayList<SimContact> loadSimContacts() {
- return new ArrayList<>(mSimCards.get(0).getContacts());
- }
+ public abstract SimCard getSimBySubscriptionId(int subscriptionId);
- @Override
- public ArrayList<SimContact> loadSimContacts(int subscriptionId) {
- return new ArrayList<>(mCardsBySubscription.get(subscriptionId).getContacts());
- }
+ public abstract Map<AccountWithDataSet, Set<SimContact>> findAccountsOfExistingSimContacts(
+ List<SimContact> contacts);
- @Override
- public boolean canReadSimContacts() {
- return true;
- }
- }
-
- // Query used for detecting existing contacts that may match a SimContact.
- private static final class DataQuery {
-
- public static final String[] PROJECTION = new String[] {
- Data.RAW_CONTACT_ID, Phone.NUMBER, Data.DISPLAY_NAME, Data.MIMETYPE
- };
-
- public static final int RAW_CONTACT_ID = 0;
- public static final int PHONE_NUMBER = 1;
- public static final int DISPLAY_NAME = 2;
- public static final int MIMETYPE = 3;
-
- public static long getRawContactId(Cursor cursor) {
- return cursor.getLong(RAW_CONTACT_ID);
- }
-
- public static String getPhoneNumber(Cursor cursor) {
- return isPhoneNumber(cursor) ? cursor.getString(PHONE_NUMBER) : null;
- }
-
- public static String getDisplayName(Cursor cursor) {
- return cursor.getString(DISPLAY_NAME);
- }
-
- public static boolean isPhoneNumber(Cursor cursor) {
- return Phone.CONTENT_ITEM_TYPE.equals(cursor.getString(MIMETYPE));
- }
- }
-
- private static final class AccountQuery {
- public static final String[] PROJECTION = new String[] {
- RawContacts._ID, RawContacts.ACCOUNT_NAME, RawContacts.ACCOUNT_TYPE,
- RawContacts.DATA_SET
- };
-
- public static long getId(Cursor cursor) {
- return cursor.getLong(0);
- }
-
- public static AccountWithDataSet getAccount(Cursor cursor) {
- return new AccountWithDataSet(cursor.getString(1), cursor.getString(2),
- cursor.getString(3));
- }
+ public void persistSimState(SimCard sim) {
+ persistSimStates(Collections.singletonList(sim));
}
}
diff --git a/src/com/android/contacts/common/database/SimContactDaoImpl.java b/src/com/android/contacts/common/database/SimContactDaoImpl.java
new file mode 100644
index 0000000..6ddf663
--- /dev/null
+++ b/src/com/android/contacts/common/database/SimContactDaoImpl.java
@@ -0,0 +1,476 @@
+/*
+ * 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.
+ */
+package com.android.contacts.common.database;
+
+import android.annotation.TargetApi;
+import android.content.ContentProviderOperation;
+import android.content.ContentProviderResult;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.OperationApplicationException;
+import android.content.pm.PackageManager;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Build;
+import android.os.RemoteException;
+import android.provider.BaseColumns;
+import android.provider.ContactsContract;
+import android.provider.ContactsContract.CommonDataKinds.Phone;
+import android.provider.ContactsContract.CommonDataKinds.StructuredName;
+import android.provider.ContactsContract.Data;
+import android.provider.ContactsContract.RawContacts;
+import android.support.annotation.VisibleForTesting;
+import android.support.v4.util.ArrayMap;
+import android.support.v4.util.ArraySet;
+import android.telephony.SubscriptionInfo;
+import android.telephony.SubscriptionManager;
+import android.telephony.TelephonyManager;
+import android.text.TextUtils;
+import android.util.SparseArray;
+
+import com.android.contacts.R;
+import com.android.contacts.common.compat.CompatUtils;
+import com.android.contacts.common.model.SimCard;
+import com.android.contacts.common.model.SimContact;
+import com.android.contacts.common.model.account.AccountWithDataSet;
+import com.android.contacts.common.util.PermissionsUtil;
+import com.android.contacts.util.SharedPreferenceUtil;
+import com.google.common.base.Joiner;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Provides data access methods for loading contacts from a SIM card and and migrating these
+ * SIM contacts to a CP2 account.
+ */
+public class SimContactDaoImpl extends 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;
+
+ // How many SIM contacts to consider in a single query. This prevents hitting the SQLite
+ // query parameter limit.
+ static final int QUERY_MAX_BATCH_SIZE = 100;
+
+ @VisibleForTesting
+ public static final Uri ICC_CONTENT_URI = Uri.parse("content://icc/adn");
+
+ public static String _ID = BaseColumns._ID;
+ public static String NAME = "name";
+ public static String NUMBER = "number";
+ public static String EMAILS = "emails";
+
+ private final Context mContext;
+ private final ContentResolver mResolver;
+ private final TelephonyManager mTelephonyManager;
+
+ public SimContactDaoImpl(Context context) {
+ mContext = context;
+ mResolver = context.getContentResolver();
+ mTelephonyManager = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
+ }
+
+ public Context getContext() {
+ return mContext;
+ }
+
+ @Override
+ 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_READY;
+ }
+
+ @Override
+ public List<SimCard> getSimCards() {
+ if (!canReadSimContacts()) {
+ return Collections.emptyList();
+ }
+ final List<SimCard> sims = CompatUtils.isMSIMCompatible() ?
+ getSimCardsFromSubscriptions() :
+ Collections.singletonList(SimCard.create(mTelephonyManager,
+ mContext.getString(R.string.single_sim_display_label)));
+ return SharedPreferenceUtil.restoreSimStates(mContext, sims);
+ }
+
+ @Override
+ 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")
+ .appendPath(String.valueOf(subscriptionId))
+ .build());
+ }
+
+ public ArrayList<SimContact> loadSimContacts() {
+ return loadFrom(ICC_CONTENT_URI);
+ }
+
+ @Override
+ 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));
+ }
+
+ @Override
+ public void persistSimStates(List<SimCard> simCards) {
+ SharedPreferenceUtil.persistSimStates(mContext, simCards);
+ }
+
+ @Override
+ public SimCard getSimBySubscriptionId(int subscriptionId) {
+ final List<SimCard> sims = SharedPreferenceUtil.restoreSimStates(mContext, 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;
+ }
+
+ /**
+ * Finds SIM contacts that exist in CP2 and associates the account of the CP2 contact with
+ * the SIM contact
+ */
+ public Map<AccountWithDataSet, Set<SimContact>> findAccountsOfExistingSimContacts(
+ List<SimContact> contacts) {
+ final Map<AccountWithDataSet, Set<SimContact>> result = new ArrayMap<>();
+ for (int i = 0; i < contacts.size(); i += QUERY_MAX_BATCH_SIZE) {
+ findAccountsOfExistingSimContacts(
+ contacts.subList(i, Math.min(contacts.size(), i + QUERY_MAX_BATCH_SIZE)),
+ result);
+ }
+ return result;
+ }
+
+ private void findAccountsOfExistingSimContacts(List<SimContact> contacts,
+ Map<AccountWithDataSet, Set<SimContact>> result) {
+ final Map<Long, List<SimContact>> rawContactToSimContact = new HashMap<>();
+ Collections.sort(contacts, SimContact.compareByPhoneThenName());
+
+ final Cursor dataCursor = queryRawContactsForSimContacts(contacts);
+
+ try {
+ while (dataCursor.moveToNext()) {
+ final String number = DataQuery.getPhoneNumber(dataCursor);
+ final String name = DataQuery.getDisplayName(dataCursor);
+
+ final int index = SimContact.findByPhoneAndName(contacts, number, name);
+ if (index < 0) {
+ continue;
+ }
+ final SimContact contact = contacts.get(index);
+ final long id = DataQuery.getRawContactId(dataCursor);
+ if (!rawContactToSimContact.containsKey(id)) {
+ rawContactToSimContact.put(id, new ArrayList<SimContact>());
+ }
+ rawContactToSimContact.get(id).add(contact);
+ }
+ } finally {
+ dataCursor.close();
+ }
+
+ final Cursor accountsCursor = queryAccountsOfRawContacts(rawContactToSimContact.keySet());
+ try {
+ while (accountsCursor.moveToNext()) {
+ final AccountWithDataSet account = AccountQuery.getAccount(accountsCursor);
+ final long id = AccountQuery.getId(accountsCursor);
+ if (!result.containsKey(account)) {
+ result.put(account, new ArraySet<SimContact>());
+ }
+ for (SimContact contact : rawContactToSimContact.get(id)) {
+ result.get(account).add(contact);
+ }
+ }
+ } finally {
+ accountsCursor.close();
+ }
+ }
+
+
+ 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();
+ 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);
+ }
+
+ // See b/32831092
+ // Sometimes the SIM contacts provider seems to get stuck if read from multiple threads
+ // concurrently. So we just have a global lock around it to prevent potential issues.
+ private static final Object SIM_READ_LOCK = new Object();
+ private ArrayList<SimContact> loadFrom(Uri uri) {
+ synchronized (SIM_READ_LOCK) {
+ final Cursor cursor = mResolver.query(uri, null, null, null, null);
+
+ try {
+ return loadFromCursor(cursor);
+ } finally {
+ cursor.close();
+ }
+ }
+ }
+
+ private ArrayList<SimContact> loadFromCursor(Cursor cursor) {
+ final int colId = cursor.getColumnIndex(_ID);
+ final int colName = cursor.getColumnIndex(NAME);
+ final int colNumber = cursor.getColumnIndex(NUMBER);
+ final int colEmails = cursor.getColumnIndex(EMAILS);
+
+ final ArrayList<SimContact> result = new ArrayList<>();
+
+ while (cursor.moveToNext()) {
+ final long id = cursor.getLong(colId);
+ final String name = cursor.getString(colName);
+ final String number = cursor.getString(colNumber);
+ final String emails = cursor.getString(colEmails);
+
+ final SimContact contact = new SimContact(id, name, number, parseEmails(emails));
+ // Only include contact if it has some useful data
+ if (contact.hasName() || contact.hasPhone() || contact.hasEmails()) {
+ result.add(contact);
+ }
+ }
+ return result;
+ }
+
+ private Cursor queryRawContactsForSimContacts(List<SimContact> contacts) {
+ final StringBuilder selectionBuilder = new StringBuilder();
+
+ int phoneCount = 0;
+ int nameCount = 0;
+ for (SimContact contact : contacts) {
+ if (contact.hasPhone()) {
+ phoneCount++;
+ } else if (contact.hasName()) {
+ nameCount++;
+ }
+ }
+ List<String> selectionArgs = new ArrayList<>(phoneCount + 1);
+
+ selectionBuilder.append('(');
+ selectionBuilder.append(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());
+ }
+ }
+ selectionBuilder.append(')');
+
+ if (nameCount > 0) {
+ selectionBuilder.append(" OR (");
+
+ selectionBuilder.append(Data.MIMETYPE).append("=? AND ");
+ selectionArgs.add(StructuredName.CONTENT_ITEM_TYPE);
+
+ selectionBuilder.append(Data.DISPLAY_NAME).append(" IN (")
+ .append(Joiner.on(',').join(Collections.nCopies(nameCount, '?')))
+ .append(')');
+ for (SimContact contact : contacts) {
+ if (!contact.hasPhone() && contact.hasName()) {
+ selectionArgs.add(contact.getName());
+ }
+ }
+ selectionBuilder.append(')');
+ }
+
+ return mResolver.query(Data.CONTENT_URI.buildUpon()
+ .appendQueryParameter(Data.VISIBLE_CONTACTS_ONLY, "true")
+ .build(),
+ DataQuery.PROJECTION,
+ selectionBuilder.toString(),
+ selectionArgs.toArray(new String[selectionArgs.size()]),
+ null);
+ }
+
+ private Cursor queryAccountsOfRawContacts(Set<Long> ids) {
+ final StringBuilder selectionBuilder = new StringBuilder();
+
+ final String[] args = new String[ids.size()];
+
+ selectionBuilder.append(RawContacts._ID).append(" IN (")
+ .append(Joiner.on(',').join(Collections.nCopies(args.length, '?')))
+ .append(")");
+ int i = 0;
+ for (long id : ids) {
+ args[i++] = String.valueOf(id);
+ }
+ return mResolver.query(RawContacts.CONTENT_URI,
+ AccountQuery.PROJECTION,
+ selectionBuilder.toString(),
+ args,
+ null);
+ }
+
+ private ArrayList<ContentProviderOperation> createImportOperations(List<SimContact> contacts,
+ AccountWithDataSet targetAccount) {
+ final ArrayList<ContentProviderOperation> ops = new ArrayList<>();
+ for (SimContact contact : contacts) {
+ contact.appendCreateContactOperations(ops, targetAccount);
+ }
+ return ops;
+ }
+
+ private String[] parseEmails(String emails) {
+ return !TextUtils.isEmpty(emails) ? emails.split(",") : null;
+ }
+
+ private boolean hasTelephony() {
+ return mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_TELEPHONY);
+ }
+
+ private boolean hasPermissions() {
+ return PermissionsUtil.hasContactsPermissions(mContext) &&
+ PermissionsUtil.hasPhonePermissions(mContext);
+ }
+
+ // TODO remove this class and the USE_FAKE_INSTANCE flag once this code is not under
+ // active development or anytime after 3/1/2017
+ public static class DebugImpl extends SimContactDaoImpl {
+
+ private List<SimCard> mSimCards = new ArrayList<>();
+ private SparseArray<SimCard> mCardsBySubscription = new SparseArray<>();
+
+ public DebugImpl(Context context) {
+ super(context);
+ }
+
+ public DebugImpl addSimCard(SimCard sim) {
+ mSimCards.add(sim);
+ mCardsBySubscription.put(sim.getSubscriptionId(), sim);
+ return this;
+ }
+
+ @Override
+ public List<SimCard> getSimCards() {
+ return SharedPreferenceUtil.restoreSimStates(getContext(), mSimCards);
+ }
+
+ @Override
+ public ArrayList<SimContact> loadContactsForSim(SimCard card) {
+ return new ArrayList<>(card.getContacts());
+ }
+
+ @Override
+ public boolean canReadSimContacts() {
+ return true;
+ }
+ }
+
+ // Query used for detecting existing contacts that may match a SimContact.
+ private static final class DataQuery {
+
+ public static final String[] PROJECTION = new String[] {
+ Data.RAW_CONTACT_ID, Phone.NUMBER, Data.DISPLAY_NAME, Data.MIMETYPE
+ };
+
+ public static final int RAW_CONTACT_ID = 0;
+ public static final int PHONE_NUMBER = 1;
+ public static final int DISPLAY_NAME = 2;
+ public static final int MIMETYPE = 3;
+
+ public static long getRawContactId(Cursor cursor) {
+ return cursor.getLong(RAW_CONTACT_ID);
+ }
+
+ public static String getPhoneNumber(Cursor cursor) {
+ return isPhoneNumber(cursor) ? cursor.getString(PHONE_NUMBER) : null;
+ }
+
+ public static String getDisplayName(Cursor cursor) {
+ return cursor.getString(DISPLAY_NAME);
+ }
+
+ public static boolean isPhoneNumber(Cursor cursor) {
+ return Phone.CONTENT_ITEM_TYPE.equals(cursor.getString(MIMETYPE));
+ }
+ }
+
+ private static final class AccountQuery {
+ public static final String[] PROJECTION = new String[] {
+ RawContacts._ID, RawContacts.ACCOUNT_NAME, RawContacts.ACCOUNT_TYPE,
+ RawContacts.DATA_SET
+ };
+
+ public static long getId(Cursor cursor) {
+ return cursor.getLong(0);
+ }
+
+ public static AccountWithDataSet getAccount(Cursor cursor) {
+ return new AccountWithDataSet(cursor.getString(1), cursor.getString(2),
+ cursor.getString(3));
+ }
+ }
+}
diff --git a/src/com/android/contacts/common/model/SimCard.java b/src/com/android/contacts/common/model/SimCard.java
index 7b13096..3e826dd 100644
--- a/src/com/android/contacts/common/model/SimCard.java
+++ b/src/com/android/contacts/common/model/SimCard.java
@@ -221,6 +221,20 @@
return result;
}
+ @Override
+ public String toString() {
+ return "SimCard{" +
+ "mSimId='" + mSimId + '\'' +
+ ", mSubscriptionId=" + mSubscriptionId +
+ ", mCarrierName=" + mCarrierName +
+ ", mDisplayName=" + mDisplayName +
+ ", mPhoneNumber='" + mPhoneNumber + '\'' +
+ ", mCountryCode='" + mCountryCode + '\'' +
+ ", mDismissed=" + mDismissed +
+ ", mImported=" + mImported +
+ ", mContacts=" + mContacts +
+ '}';
+ }
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP_MR1)
public static SimCard create(SubscriptionInfo info) {
diff --git a/src/com/android/contacts/common/model/SimContact.java b/src/com/android/contacts/common/model/SimContact.java
index 2d26029..7442805 100644
--- a/src/com/android/contacts/common/model/SimContact.java
+++ b/src/com/android/contacts/common/model/SimContact.java
@@ -24,6 +24,7 @@
import android.provider.ContactsContract.CommonDataKinds.Email;
import android.provider.ContactsContract.CommonDataKinds.Phone;
import android.provider.ContactsContract.CommonDataKinds.StructuredName;
+import android.text.TextUtils;
import com.android.contacts.common.model.account.AccountWithDataSet;
import com.google.common.collect.ComparisonChain;
@@ -45,6 +46,10 @@
private final String mPhone;
private final String[] mEmails;
+ public SimContact(long id, String name, String phone) {
+ this(id, name, phone, null);
+ }
+
public SimContact(long id, String name, String phone, String[] emails) {
mId = id;
mName = name;
@@ -52,6 +57,10 @@
mEmails = emails;
}
+ public SimContact(SimContact other) {
+ this(other.mId, other.mName, other.mPhone, other.mEmails);
+ }
+
public long getId() {
return mId;
}
@@ -112,7 +121,7 @@
}
public boolean hasName() {
- return mName != null;
+ return !TextUtils.isEmpty(mName);
}
public boolean hasPhone() {
diff --git a/src/com/android/contacts/common/model/account/AccountWithDataSet.java b/src/com/android/contacts/common/model/account/AccountWithDataSet.java
index 3ee0aab..304dca7 100644
--- a/src/com/android/contacts/common/model/account/AccountWithDataSet.java
+++ b/src/com/android/contacts/common/model/account/AccountWithDataSet.java
@@ -17,6 +17,7 @@
package com.android.contacts.common.model.account;
import android.accounts.Account;
+import android.content.ContentProviderOperation;
import android.content.Context;
import android.database.Cursor;
import android.net.Uri;
@@ -191,6 +192,20 @@
}
/**
+ * Returns a {@link ContentProviderOperation} that will create a RawContact in this account
+ */
+ public ContentProviderOperation newRawContactOperation() {
+ final ContentProviderOperation.Builder builder =
+ ContentProviderOperation.newInsert(RawContacts.CONTENT_URI)
+ .withValue(RawContacts.ACCOUNT_NAME, name)
+ .withValue(RawContacts.ACCOUNT_TYPE, type);
+ if (dataSet != null) {
+ builder.withValue(RawContacts.DATA_SET, dataSet);
+ }
+ return builder.build();
+ }
+
+ /**
* Unpack a string created by {@link #stringify}.
*
* @throws IllegalArgumentException if it's an invalid string.
diff --git a/src/com/android/contacts/common/preference/DisplayOptionsPreferenceFragment.java b/src/com/android/contacts/common/preference/DisplayOptionsPreferenceFragment.java
index e2782f8..08846a8 100644
--- a/src/com/android/contacts/common/preference/DisplayOptionsPreferenceFragment.java
+++ b/src/com/android/contacts/common/preference/DisplayOptionsPreferenceFragment.java
@@ -43,8 +43,8 @@
import android.view.ViewGroup;
import android.widget.FrameLayout;
-import com.android.contacts.ContactSaveService;
import com.android.contacts.R;
+import com.android.contacts.SimImportService;
import com.android.contacts.common.ContactsUtils;
import com.android.contacts.common.compat.TelecomManagerUtil;
import com.android.contacts.common.compat.TelephonyManagerCompat;
@@ -192,7 +192,7 @@
mSaveServiceListener = new SaveServiceResultListener();
LocalBroadcastManager.getInstance(getActivity()).registerReceiver(
mSaveServiceListener,
- new IntentFilter(ContactSaveService.BROADCAST_SIM_IMPORT_COMPLETE));
+ new IntentFilter(SimImportService.BROADCAST_SIM_IMPORT_COMPLETE));
}
@Override
@@ -401,20 +401,20 @@
public void onReceive(Context context, Intent intent) {
final long now = System.currentTimeMillis();
final long opStart = intent.getLongExtra(
- ContactSaveService.EXTRA_OPERATION_REQUESTED_AT_TIME, now);
+ SimImportService.EXTRA_OPERATION_REQUESTED_AT_TIME, now);
// If it's been over 30 seconds the user is likely in a different context so suppress
// the toast message.
if (now - opStart > 30*1000) return;
- final int code = intent.getIntExtra(ContactSaveService.EXTRA_RESULT_CODE,
- ContactSaveService.RESULT_UNKNOWN);
- final int count = intent.getIntExtra(ContactSaveService.EXTRA_RESULT_COUNT, -1);
- if (code == ContactSaveService.RESULT_SUCCESS && count > 0) {
+ final int code = intent.getIntExtra(SimImportService.EXTRA_RESULT_CODE,
+ SimImportService.RESULT_UNKNOWN);
+ final int count = intent.getIntExtra(SimImportService.EXTRA_RESULT_COUNT, -1);
+ if (code == SimImportService.RESULT_SUCCESS && count > 0) {
Snackbar.make(mRootView, getResources().getQuantityString(
R.plurals.sim_import_success_toast_fmt, count, count),
Snackbar.LENGTH_LONG).show();
- } else if (code == ContactSaveService.RESULT_FAILURE) {
+ } else if (code == SimImportService.RESULT_FAILURE) {
Snackbar.make(mRootView, R.string.sim_import_failed_toast,
Snackbar.LENGTH_LONG).show();
}
diff --git a/tests/src/com/android/contacts/common/database/SimContactDaoTests.java b/tests/src/com/android/contacts/common/database/SimContactDaoTests.java
index ec03f0f..4702442 100644
--- a/tests/src/com/android/contacts/common/database/SimContactDaoTests.java
+++ b/tests/src/com/android/contacts/common/database/SimContactDaoTests.java
@@ -18,28 +18,44 @@
import android.content.ContentProviderOperation;
import android.content.ContentResolver;
import android.content.Context;
+import android.content.OperationApplicationException;
import android.database.Cursor;
import android.database.CursorWrapper;
import android.database.DatabaseUtils;
+import android.os.RemoteException;
import android.provider.ContactsContract;
+import android.provider.ContactsContract.CommonDataKinds.Email;
+import android.provider.ContactsContract.CommonDataKinds.Phone;
+import android.provider.ContactsContract.CommonDataKinds.StructuredName;
+import android.provider.ContactsContract.Data;
import android.support.annotation.RequiresApi;
import android.support.test.InstrumentationRegistry;
import android.support.test.filters.LargeTest;
-import android.support.test.filters.MediumTest;
import android.support.test.filters.SdkSuppress;
+import android.support.test.filters.SmallTest;
import android.support.test.filters.Suppress;
import android.support.test.runner.AndroidJUnit4;
+import android.test.mock.MockContentResolver;
+import android.test.mock.MockContext;
+import android.view.Menu;
+import com.android.contacts.common.model.SimCard;
import com.android.contacts.common.model.SimContact;
import com.android.contacts.common.model.account.AccountWithDataSet;
+import com.android.contacts.common.test.mocks.MockContentProvider;
import com.android.contacts.tests.AccountsTestHelper;
import com.android.contacts.tests.SimContactsTestHelper;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
import org.hamcrest.BaseMatcher;
import org.hamcrest.Description;
import org.hamcrest.Matcher;
+import org.hamcrest.Matchers;
import org.junit.After;
+import org.junit.AfterClass;
import org.junit.Before;
+import org.junit.BeforeClass;
import org.junit.Test;
import org.junit.experimental.runners.Enclosed;
import org.junit.runner.RunWith;
@@ -47,17 +63,32 @@
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
+import java.util.Locale;
import java.util.Map;
+import java.util.Random;
import java.util.Set;
import static android.os.Build.VERSION_CODES;
import static org.hamcrest.Matchers.allOf;
+import static org.hamcrest.Matchers.equalTo;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
@RunWith(Enclosed.class)
public class SimContactDaoTests {
+ // Some random area codes for generating realistic US phones when
+ // generating fake data for the SIM contacts or CP2
+ private static final String[] AREA_CODES =
+ {"360", "509", "416", "831", "212", "208"};
+ private static final Random sRandom = new Random();
+
+ // Approximate maximum number of contacts that can be stored on a SIM card for testing
+ // boundary cases
+ public static final int MAX_SIM_CONTACTS = 600;
+
// On pre-M addAccountExplicitly (which we call via AccountsTestHelper) causes a
// SecurityException to be thrown unless we add AUTHENTICATE_ACCOUNTS permission to the app
// manifest. Instead of adding the extra permission just for tests we'll just only run them
@@ -89,8 +120,8 @@
final SimContactDao sut = SimContactDao.create(getContext());
sut.importContacts(Arrays.asList(
- new SimContact(1, "Test One", "15095550101", null),
- new SimContact(2, "Test Two", "15095550102", null),
+ new SimContact(1, "Test One", "15095550101"),
+ new SimContact(2, "Test Two", "15095550102"),
new SimContact(3, "Test Three", "15095550103", new String[] {
"user@example.com", "user2@example.com"
})
@@ -169,6 +200,35 @@
dataCursor.close();
}
+ /**
+ * Tests importing a large number of contacts
+ *
+ * Make sure that {@link android.os.TransactionTooLargeException} is not thrown
+ */
+ @Test
+ public void largeImport() throws Exception {
+ final SimContactDao sut = SimContactDao.create(getContext());
+
+ final List<SimContact> contacts = new ArrayList<>();
+
+ for (int i = 0; i < MAX_SIM_CONTACTS; i++) {
+ contacts.add(new SimContact(i + 1, "Contact " + (i + 1), randomPhone(),
+ new String[] { randomEmail("contact" + (i + 1) + "_")}));
+ }
+
+ sut.importContacts(contacts, mAccount);
+
+ final Cursor contactsCursor = queryAllRawContactsInAccount();
+ assertThat(contactsCursor, hasCount(MAX_SIM_CONTACTS));
+ contactsCursor.close();
+
+ final Cursor dataCursor = queryAllDataInAccount();
+ // Each contact has one data row for each of name, phone and email
+ assertThat(dataCursor, hasCount(MAX_SIM_CONTACTS * 3));
+
+ dataCursor.close();
+ }
+
private Cursor queryAllRawContactsInAccount() {
return new StringableCursor(mResolver.query(ContactsContract.RawContacts.CONTENT_URI,
null, ContactsContract.RawContacts.ACCOUNT_NAME + "=? AND " +
@@ -180,7 +240,7 @@
}
private Cursor queryAllDataInAccount() {
- return new StringableCursor(mResolver.query(ContactsContract.Data.CONTENT_URI, null,
+ return new StringableCursor(mResolver.query(Data.CONTENT_URI, null,
ContactsContract.RawContacts.ACCOUNT_NAME + "=? AND " +
ContactsContract.RawContacts.ACCOUNT_TYPE+ "=?",
new String[] {
@@ -190,10 +250,10 @@
}
private Cursor queryContactWithName(String name) {
- return new StringableCursor(mResolver.query(ContactsContract.Data.CONTENT_URI, null,
+ return new StringableCursor(mResolver.query(Data.CONTENT_URI, null,
ContactsContract.RawContacts.ACCOUNT_NAME + "=? AND " +
ContactsContract.RawContacts.ACCOUNT_TYPE+ "=? AND " +
- ContactsContract.Data.DISPLAY_NAME + "=?",
+ Data.DISPLAY_NAME + "=?",
new String[] {
mAccount.name,
mAccount.type,
@@ -202,42 +262,73 @@
}
}
+ /**
+ * Tests for {@link SimContactDao#findAccountsOfExistingSimContacts(List)}
+ *
+ * These are integration tests that query CP2 so that the SQL will be validated in addition
+ * to the detection algorithm
+ */
@SdkSuppress(minSdkVersion = VERSION_CODES.M)
// Lollipop MR1 is required for removeAccountExplicitly
@RequiresApi(api = VERSION_CODES.LOLLIPOP_MR1)
- @MediumTest
+ @LargeTest
@RunWith(AndroidJUnit4.class)
- public static class ExistingContactsTest {
+ public static class FindAccountsIntegrationTests {
private Context mContext;
private AccountsTestHelper mAccountHelper;
- private AccountWithDataSet mAccount;
+ private List<AccountWithDataSet> mAccounts;
// We need to generate something distinct to prevent flakiness on devices that may not
// start with an empty CP2 DB
- private String mNameSuffix = "";
+ private String mNameSuffix;
+
+ private static AccountWithDataSet sSeedAccount;
+
+ @BeforeClass
+ public static void setUpClass() throws Exception {
+ final AccountsTestHelper helper = new AccountsTestHelper(
+ InstrumentationRegistry.getContext());
+ sSeedAccount = helper.addTestAccount(helper.generateAccountName("seedAccount"));
+
+ seedCp2();
+ }
+
+ @AfterClass
+ public static void tearDownClass() {
+ final AccountsTestHelper helper = new AccountsTestHelper(
+ InstrumentationRegistry.getContext());
+ helper.removeTestAccount(sSeedAccount);
+ sSeedAccount = null;
+ }
@Before
- public void setUp() {
+ public void setUp() throws Exception {
mContext = InstrumentationRegistry.getTargetContext();
mAccountHelper = new AccountsTestHelper(InstrumentationRegistry.getContext());
- mAccount = mAccountHelper.addTestAccount();
- mNameSuffix = "testAt" + System.nanoTime();
+ mAccounts = new ArrayList<>();
+ mNameSuffix = getClass().getSimpleName() + "At" + System.nanoTime();
+
+ seedCp2();
}
@After
public void tearDown() {
- mAccountHelper.cleanup();
+ for (AccountWithDataSet account : mAccounts) {
+ mAccountHelper.removeTestAccount(account);
+ }
}
@Test
- public void findAccountsOfExistingContactsReturnsEmptyMapWhenNoMatchingContactsExist() {
+ public void returnsEmptyMapWhenNoMatchingContactsExist() {
+ mAccounts.add(mAccountHelper.addTestAccount());
+
final SimContactDao sut = createDao();
final List<SimContact> contacts = Arrays.asList(
- new SimContact(1, "Name 1 " + mNameSuffix, "15095550101", null),
- new SimContact(2, "Name 2 " + mNameSuffix, "15095550102", null),
- new SimContact(3, "Name 3 " + mNameSuffix, "15095550103", null),
- new SimContact(4, "Name 4 " + mNameSuffix, "15095550104", null));
+ new SimContact(1, "Name 1 " + mNameSuffix, "5550101"),
+ new SimContact(2, "Name 2 " + mNameSuffix, "5550102"),
+ new SimContact(3, "Name 3 " + mNameSuffix, "5550103"),
+ new SimContact(4, "Name 4 " + mNameSuffix, "5550104"));
final Map<AccountWithDataSet, Set<SimContact>> existing = sut
.findAccountsOfExistingSimContacts(contacts);
@@ -245,16 +336,375 @@
assertTrue(existing.isEmpty());
}
+ @Test
+ public void hasAccountWithMatchingContactsWhenSingleMatchingContactExists()
+ throws Exception {
+ final SimContactDao sut = createDao();
+
+ final AccountWithDataSet account = mAccountHelper.addTestAccount(
+ mAccountHelper.generateAccountName("primary_"));
+ mAccounts.add(account);
+
+ final SimContact existing1 =
+ new SimContact(2, "Exists 2 " + mNameSuffix, "5550102");
+ final SimContact existing2 =
+ new SimContact(4, "Exists 4 " + mNameSuffix, "5550104");
+
+ final List<SimContact> contacts = Arrays.asList(
+ new SimContact(1, "Missing 1 " + mNameSuffix, "5550101"),
+ new SimContact(existing1),
+ new SimContact(3, "Missing 3 " + mNameSuffix, "5550103"),
+ new SimContact(existing2));
+
+ sut.importContacts(Arrays.asList(
+ new SimContact(existing1),
+ new SimContact(existing2)
+ ), account);
+
+
+ final Map<AccountWithDataSet, Set<SimContact>> existing = sut
+ .findAccountsOfExistingSimContacts(contacts);
+
+ assertThat(existing.size(), equalTo(1));
+ assertThat(existing.get(account),
+ Matchers.<Set<SimContact>>equalTo(ImmutableSet.of(existing1, existing2)));
+ }
+
+ @Test
+ public void hasMultipleAccountsWhenMultipleMatchingContactsExist() throws Exception {
+ final SimContactDao sut = createDao();
+
+ final AccountWithDataSet account1 = mAccountHelper.addTestAccount(
+ mAccountHelper.generateAccountName("account1_"));
+ mAccounts.add(account1);
+ final AccountWithDataSet account2 = mAccountHelper.addTestAccount(
+ mAccountHelper.generateAccountName("account2_"));
+ mAccounts.add(account2);
+
+ final SimContact existsInBoth =
+ new SimContact(2, "Exists Both " + mNameSuffix, "5550102");
+ final SimContact existsInAccount1 =
+ new SimContact(4, "Exists 1 " + mNameSuffix, "5550104");
+ final SimContact existsInAccount2 =
+ new SimContact(5, "Exists 2 " + mNameSuffix, "5550105");
+
+ final List<SimContact> contacts = Arrays.asList(
+ new SimContact(1, "Missing 1 " + mNameSuffix, "5550101"),
+ new SimContact(existsInBoth),
+ new SimContact(3, "Missing 3 " + mNameSuffix, "5550103"),
+ new SimContact(existsInAccount1),
+ new SimContact(existsInAccount2));
+
+ sut.importContacts(Arrays.asList(
+ new SimContact(existsInBoth),
+ new SimContact(existsInAccount1)
+ ), account1);
+
+ sut.importContacts(Arrays.asList(
+ new SimContact(existsInBoth),
+ new SimContact(existsInAccount2)
+ ), account2);
+
+
+ final Map<AccountWithDataSet, Set<SimContact>> existing = sut
+ .findAccountsOfExistingSimContacts(contacts);
+
+ assertThat(existing.size(), equalTo(2));
+ assertThat(existing, Matchers.<Map<AccountWithDataSet, Set<SimContact>>>equalTo(
+ ImmutableMap.<AccountWithDataSet, Set<SimContact>>of(
+ account1, ImmutableSet.of(existsInBoth, existsInAccount1),
+ account2, ImmutableSet.of(existsInBoth, existsInAccount2))));
+ }
+
+ @Test
+ public void matchesByNameIfSimContactHasNoPhone() throws Exception {
+ final SimContactDao sut = createDao();
+
+ final AccountWithDataSet account = mAccountHelper.addTestAccount(
+ mAccountHelper.generateAccountName("account_"));
+ mAccounts.add(account);
+
+ final SimContact noPhone = new SimContact(1, "Nophone " + mNameSuffix, null);
+ final SimContact otherExisting = new SimContact(
+ 5, "Exists 1 " + mNameSuffix, "5550105");
+
+ final List<SimContact> contacts = Arrays.asList(
+ new SimContact(noPhone),
+ new SimContact(2, "Name 2 " + mNameSuffix, "5550102"),
+ new SimContact(3, "Name 3 " + mNameSuffix, "5550103"),
+ new SimContact(4, "Name 4 " + mNameSuffix, "5550104"),
+ new SimContact(otherExisting));
+
+ sut.importContacts(Arrays.asList(
+ new SimContact(noPhone),
+ new SimContact(otherExisting)
+ ), account);
+
+ final Map<AccountWithDataSet, Set<SimContact>> existing = sut
+ .findAccountsOfExistingSimContacts(contacts);
+
+ assertThat(existing.size(), equalTo(1));
+ assertThat(existing.get(account), Matchers.<Set<SimContact>>equalTo(
+ ImmutableSet.of(noPhone, otherExisting)));
+ }
+
+ @Test
+ public void largeNumberOfSimContacts() throws Exception {
+ final SimContactDao sut = createDao();
+
+ final List<SimContact> contacts = new ArrayList<>();
+ for (int i = 0; i < MAX_SIM_CONTACTS; i++) {
+ contacts.add(new SimContact(
+ i + 1, "Contact " + (i + 1) + " " + mNameSuffix, randomPhone()));
+ }
+ // The work has to be split into batches to avoid hitting SQL query parameter limits
+ // so test contacts that will be at boundary points
+ final SimContact imported1 = contacts.get(0);
+ final SimContact imported2 = contacts.get(99);
+ final SimContact imported3 = contacts.get(100);
+ final SimContact imported4 = contacts.get(101);
+ final SimContact imported5 = contacts.get(MAX_SIM_CONTACTS - 1);
+
+ final AccountWithDataSet account = mAccountHelper.addTestAccount(
+ mAccountHelper.generateAccountName("account_"));
+ mAccounts.add(account);
+
+ sut.importContacts(Arrays.asList(imported1, imported2, imported3, imported4, imported5),
+ account);
+
+ mAccounts.add(account);
+
+ final Map<AccountWithDataSet, Set<SimContact>> existing = sut
+ .findAccountsOfExistingSimContacts(contacts);
+
+ assertThat(existing.size(), equalTo(1));
+ assertThat(existing.get(account), Matchers.<Set<SimContact>>equalTo(
+ ImmutableSet.of(imported1, imported2, imported3, imported4, imported5)));
+
+ }
+
private SimContactDao createDao() {
return SimContactDao.create(mContext);
}
+
+ /**
+ * Adds a bunch of random contact data to CP2 to make the test environment more realistic
+ */
+ private static void seedCp2() throws RemoteException, OperationApplicationException {
+
+ final ArrayList<ContentProviderOperation> ops = new ArrayList<>();
+
+ appendCreateContact("John Smith", sSeedAccount, ops);
+ appendCreateContact("Marcus Seed", sSeedAccount, ops);
+ appendCreateContact("Gary Seed", sSeedAccount, ops);
+ appendCreateContact("Michael Seed", sSeedAccount, ops);
+ appendCreateContact("Isaac Seed", sSeedAccount, ops);
+ appendCreateContact("Sean Seed", sSeedAccount, ops);
+ appendCreateContact("Nate Seed", sSeedAccount, ops);
+ appendCreateContact("Andrey Seed", sSeedAccount, ops);
+ appendCreateContact("Cody Seed", sSeedAccount, ops);
+ appendCreateContact("John Seed", sSeedAccount, ops);
+ appendCreateContact("Alex Seed", sSeedAccount, ops);
+
+ InstrumentationRegistry.getTargetContext()
+ .getContentResolver().applyBatch(ContactsContract.AUTHORITY, ops);
+ }
+
+ private static void appendCreateContact(String name, AccountWithDataSet account,
+ ArrayList<ContentProviderOperation> ops) {
+ final int emailCount = sRandom.nextInt(10);
+ final int phoneCount = sRandom.nextInt(5);
+
+ final List<String> phones = new ArrayList<>();
+ for (int i = 0; i < phoneCount; i++) {
+ phones.add(randomPhone());
+ }
+ final List<String> emails = new ArrayList<>();
+ for (int i = 0; i < emailCount; i++) {
+ emails.add(randomEmail(name));
+ }
+ appendCreateContact(name, phones, emails, account, ops);
+ }
+
+
+ private static void appendCreateContact(String name, List<String> phoneNumbers,
+ List<String> emails, AccountWithDataSet account, List<ContentProviderOperation> ops) {
+ int index = ops.size();
+
+ ops.add(account.newRawContactOperation());
+ ops.add(insertIntoData(name, StructuredName.CONTENT_ITEM_TYPE, index));
+ for (String phone : phoneNumbers) {
+ ops.add(insertIntoData(phone, Phone.CONTENT_ITEM_TYPE, Phone.TYPE_MOBILE, index));
+ }
+ for (String email : emails) {
+ ops.add(insertIntoData(email, Email.CONTENT_ITEM_TYPE, Email.TYPE_HOME, index));
+ }
+ }
+
+ private static ContentProviderOperation insertIntoData(String value, String mimeType,
+ int idBackReference) {
+ return ContentProviderOperation.newInsert(Data.CONTENT_URI)
+ .withValue(Data.DATA1, value)
+ .withValue(Data.MIMETYPE, mimeType)
+ .withValueBackReference(Data.RAW_CONTACT_ID, idBackReference).build();
+ }
+
+ private static ContentProviderOperation insertIntoData(String value, String mimeType,
+ int type, int idBackReference) {
+ return ContentProviderOperation.newInsert(Data.CONTENT_URI)
+ .withValue(Data.DATA1, value)
+ .withValue(ContactsContract.Data.DATA2, type)
+ .withValue(Data.MIMETYPE, mimeType)
+ .withValueBackReference(Data.RAW_CONTACT_ID, idBackReference).build();
+ }
+ }
+
+ /**
+ * Tests for {@link SimContactDao#loadContactsForSim(SimCard)}
+ *
+ * These are unit tests that verify that {@link SimContact}s are created correctly from
+ * the cursors that are returned by queries to the IccProvider
+ */
+ @SmallTest
+ @RunWith(AndroidJUnit4.class)
+ public static class LoadContactsUnitTests {
+
+ private MockContentProvider mMockIccProvider;
+ private Context mContext;
+
+ @Before
+ public void setUp() {
+ mContext = mock(MockContext.class);
+ final MockContentResolver mockResolver = new MockContentResolver();
+ mMockIccProvider = new MockContentProvider();
+ mockResolver.addProvider("icc", mMockIccProvider);
+ when(mContext.getContentResolver()).thenReturn(mockResolver);
+ }
+
+
+ @Test
+ public void createsContactsFromCursor() {
+ mMockIccProvider.expect(MockContentProvider.Query.forAnyUri())
+ .withDefaultProjection(
+ SimContactDaoImpl._ID, SimContactDaoImpl.NAME,
+ SimContactDaoImpl.NUMBER, SimContactDaoImpl.EMAILS)
+ .withAnyProjection()
+ .withAnySelection()
+ .withAnySortOrder()
+ .returnRow(1, "Name One", "5550101", null)
+ .returnRow(2, "Name Two", "5550102", null)
+ .returnRow(3, "Name Three", null, null)
+ .returnRow(4, null, "5550104", null)
+ .returnRow(5, "Name Five", "5550105",
+ "five@example.com,nf@example.com,name.five@example.com")
+ .returnRow(6, "Name Six", "5550106", "thesix@example.com");
+
+ final SimContactDao sut = SimContactDao.create(mContext);
+ final List<SimContact> contacts = sut
+ .loadContactsForSim(new SimCard("123", "carrier", "sim", null, "us"));
+
+ assertThat(contacts, equalTo(
+ Arrays.asList(
+ new SimContact(1, "Name One", "5550101", null),
+ new SimContact(2, "Name Two", "5550102", null),
+ new SimContact(3, "Name Three", null, null),
+ new SimContact(4, null, "5550104", null),
+ new SimContact(5, "Name Five", "5550105", new String[] {
+ "five@example.com", "nf@example.com", "name.five@example.com"
+ }),
+ new SimContact(6, "Name Six", "5550106", new String[] {
+ "thesix@example.com"
+ })
+ )));
+ }
+
+ @Test
+ public void excludesEmptyContactsFromResult() {
+ mMockIccProvider.expect(MockContentProvider.Query.forAnyUri())
+ .withDefaultProjection(
+ SimContactDaoImpl._ID, SimContactDaoImpl.NAME,
+ SimContactDaoImpl.NUMBER, SimContactDaoImpl.EMAILS)
+ .withAnyProjection()
+ .withAnySelection()
+ .withAnySortOrder()
+ .returnRow(1, "Non Empty1", "5550101", null)
+ .returnRow(2, "", "", "")
+ .returnRow(3, "Non Empty2", null, null)
+ .returnRow(4, null, null, null)
+ .returnRow(5, "", null, null)
+ .returnRow(6, null, "5550102", null)
+ .returnRow(7, null, null, "user@example.com");
+
+ final SimContactDao sut = SimContactDao.create(mContext);
+ final List<SimContact> contacts = sut
+ .loadContactsForSim(new SimCard("123", "carrier", "sim", null, "us"));
+
+ assertThat(contacts, equalTo(
+ Arrays.asList(
+ new SimContact(1, "Non Empty1", "5550101", null),
+ new SimContact(3, "Non Empty2", null, null),
+ new SimContact(6, null, "5550102", null),
+ new SimContact(7, null, null, new String[] { "user@example.com" })
+ )));
+ }
+
+ @Test
+ public void usesSimCardSubscriptionIdIfAvailable() {
+ mMockIccProvider.expectQuery(SimContactDaoImpl.ICC_CONTENT_URI.buildUpon()
+ .appendPath("subId").appendPath("2").build())
+ .withDefaultProjection(
+ SimContactDaoImpl._ID, SimContactDaoImpl.NAME,
+ SimContactDaoImpl.NUMBER, SimContactDaoImpl.EMAILS)
+ .withAnyProjection()
+ .withAnySelection()
+ .withAnySortOrder()
+ .returnEmptyCursor();
+
+ final SimContactDao sut = SimContactDao.create(mContext);
+ sut.loadContactsForSim(new SimCard("123", 2, "carrier", "sim", null, "us"));
+ mMockIccProvider.verify();
+ }
+
+ @Test
+ public void omitsSimCardSubscriptionIdIfUnavailable() {
+ mMockIccProvider.expectQuery(SimContactDaoImpl.ICC_CONTENT_URI)
+ .withDefaultProjection(
+ SimContactDaoImpl._ID, SimContactDaoImpl.NAME,
+ SimContactDaoImpl.NUMBER, SimContactDaoImpl.EMAILS)
+ .withAnyProjection()
+ .withAnySelection()
+ .withAnySortOrder()
+ .returnEmptyCursor();
+
+ final SimContactDao sut = SimContactDao.create(mContext);
+ sut.loadContactsForSim(new SimCard("123", SimCard.NO_SUBSCRIPTION_ID,
+ "carrier", "sim", null, "us"));
+ mMockIccProvider.verify();
+ }
+
+ @Test
+ public void returnsEmptyListForEmptyCursor() {
+ mMockIccProvider.expect(MockContentProvider.Query.forAnyUri())
+ .withDefaultProjection(
+ SimContactDaoImpl._ID, SimContactDaoImpl.NAME,
+ SimContactDaoImpl.NUMBER, SimContactDaoImpl.EMAILS)
+ .withAnyProjection()
+ .withAnySelection()
+ .withAnySortOrder()
+ .returnEmptyCursor();
+
+ final SimContactDao sut = SimContactDao.create(mContext);
+ List<SimContact> result = sut
+ .loadContactsForSim(new SimCard("123", "carrier", "sim", null, "us"));
+ assertTrue(result.isEmpty());
+ }
}
@LargeTest
// suppressed because failed assumptions are reported as test failures by the build server
@Suppress
@RunWith(AndroidJUnit4.class)
- public static class ReadIntegrationTest {
+ public static class LoadContactsIntegrationTest {
private SimContactsTestHelper mSimTestHelper;
private ArrayList<ContentProviderOperation> mSimSnapshot;
@@ -281,7 +731,8 @@
mSimTestHelper.addSimContact("Test Simthree", "15095550103");
final SimContactDao sut = SimContactDao.create(getContext());
- final ArrayList<SimContact> contacts = sut.loadSimContacts();
+ final SimCard sim = sut.getSimCards().get(0);
+ final ArrayList<SimContact> contacts = sut.loadContactsForSim(sim);
assertThat(contacts.get(0), isSimContactWithNameAndPhone("Test Simone", "15095550101"));
assertThat(contacts.get(1), isSimContactWithNameAndPhone("Test Simtwo", "15095550102"));
@@ -327,7 +778,7 @@
}
private static Matcher<Cursor> hasMimeType(String type) {
- return hasValueForColumn(ContactsContract.Data.MIMETYPE, type);
+ return hasValueForColumn(Data.MIMETYPE, type);
}
private static Matcher<Cursor> hasValueForColumn(final String column, final String value) {
@@ -374,23 +825,23 @@
private static Matcher<Cursor> hasName(final String name) {
return hasRowMatching(allOf(
- hasMimeType(ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE),
+ hasMimeType(StructuredName.CONTENT_ITEM_TYPE),
hasValueForColumn(
- ContactsContract.CommonDataKinds.StructuredName.DISPLAY_NAME, name)));
+ StructuredName.DISPLAY_NAME, name)));
}
private static Matcher<Cursor> hasPhone(final String phone) {
return hasRowMatching(allOf(
- hasMimeType(ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE),
+ hasMimeType(Phone.CONTENT_ITEM_TYPE),
hasValueForColumn(
- ContactsContract.CommonDataKinds.Phone.NUMBER, phone)));
+ Phone.NUMBER, phone)));
}
private static Matcher<Cursor> hasEmail(final String email) {
return hasRowMatching(allOf(
- hasMimeType(ContactsContract.CommonDataKinds.Email.CONTENT_ITEM_TYPE),
+ hasMimeType(Email.CONTENT_ITEM_TYPE),
hasValueForColumn(
- ContactsContract.CommonDataKinds.Email.ADDRESS, email)));
+ Email.ADDRESS, email)));
}
static class StringableCursor extends CursorWrapper {
@@ -410,7 +861,19 @@
}
}
+ private static String randomPhone() {
+ return String.format(Locale.US, "1%s55501%02d",
+ AREA_CODES[sRandom.nextInt(AREA_CODES.length)],
+ sRandom.nextInt(100));
+ }
+
+ private static String randomEmail(String name) {
+ return String.format("%s%d@example.com", name.replace(" ", ".").toLowerCase(Locale.US),
+ 1000 + sRandom.nextInt(1000));
+ }
+
+
static Context getContext() {
return InstrumentationRegistry.getTargetContext();
- }
+ }
}
diff --git a/tests/src/com/android/contacts/tests/AccountsTestHelper.java b/tests/src/com/android/contacts/tests/AccountsTestHelper.java
index 123d538..15cec3c 100644
--- a/tests/src/com/android/contacts/tests/AccountsTestHelper.java
+++ b/tests/src/com/android/contacts/tests/AccountsTestHelper.java
@@ -17,6 +17,7 @@
import android.accounts.Account;
import android.accounts.AccountManager;
+import android.annotation.TargetApi;
import android.content.ContentResolver;
import android.content.Context;
import android.os.Build;
@@ -27,6 +28,11 @@
import com.android.contacts.common.model.account.AccountWithDataSet;
+import org.junit.runner.RunWith;
+
+import java.util.Arrays;
+import java.util.List;
+
import static junit.framework.Assert.assertNotNull;
import static junit.framework.Assert.assertTrue;
@@ -80,12 +86,27 @@
mAccountManager.removeAccountExplicitly(remove);
}
+ @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP_MR1)
+ public void removeTestAccount(String accountName) {
+ final Account remove = new Account(accountName, TEST_ACCOUNT_TYPE);
+ mAccountManager.removeAccountExplicitly(remove);
+ }
+
+ public boolean hasTestAccount(String name) {
+ final List<Account> accounts = Arrays.asList(
+ mAccountManager.getAccountsByType(TEST_ACCOUNT_TYPE));
+ return accounts.contains(new Account(name, TEST_ACCOUNT_TYPE));
+ }
+
public void removeContactsForAccount() {
- // Not sure if this is necessary or if contacts are automatically cleaned up when the
- // account is removed.
+ removeContactsForAccount(
+ new AccountWithDataSet(mTestAccount.name, mTestAccount.type, null));
+ }
+
+ public void removeContactsForAccount(AccountWithDataSet account) {
mResolver.delete(RawContacts.CONTENT_URI,
RawContacts.ACCOUNT_NAME + "=? AND " + RawContacts.ACCOUNT_TYPE + "=?",
- new String[] { mTestAccount.name, mTestAccount.type });
+ new String[] { account.name, account.type });
}
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP_MR1)
diff --git a/tests/src/com/android/contacts/tests/AdbHelpers.java b/tests/src/com/android/contacts/tests/AdbHelpers.java
index 163d7cc..cf66cda 100644
--- a/tests/src/com/android/contacts/tests/AdbHelpers.java
+++ b/tests/src/com/android/contacts/tests/AdbHelpers.java
@@ -16,8 +16,10 @@
package com.android.contacts.tests;
import android.content.Context;
+import android.content.OperationApplicationException;
import android.os.Build;
import android.os.Bundle;
+import android.os.RemoteException;
import android.support.annotation.RequiresApi;
import android.support.test.InstrumentationRegistry;
import android.util.Log;
@@ -85,6 +87,11 @@
getAppContext().getPackageName(), Context.MODE_PRIVATE).getAll());
}
+ public static void clearSimCard(Context context)
+ throws RemoteException, OperationApplicationException {
+ new SimContactsTestHelper(context).deleteAllSimContacts();
+ }
+
private static Context getAppContext() {
return InstrumentationRegistry.getTargetContext();
}
diff --git a/tests/src/com/android/contacts/tests/FakeSimContactDao.java b/tests/src/com/android/contacts/tests/FakeSimContactDao.java
new file mode 100644
index 0000000..ba091ca
--- /dev/null
+++ b/tests/src/com/android/contacts/tests/FakeSimContactDao.java
@@ -0,0 +1,111 @@
+/*
+ * 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.
+ */
+package com.android.contacts.tests;
+
+import android.content.ContentProviderResult;
+import android.content.OperationApplicationException;
+import android.os.RemoteException;
+
+import com.android.contacts.common.database.SimContactDao;
+import com.android.contacts.common.model.SimCard;
+import com.android.contacts.common.model.SimContact;
+import com.android.contacts.common.model.account.AccountWithDataSet;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Fake implementation of SimContactDao for testing
+ */
+public class FakeSimContactDao extends SimContactDao {
+
+ public boolean canReadSimContacts = true;
+ public List<SimCard> simCards;
+ public Map<SimCard, ArrayList<SimContact>> simContacts;
+ public ContentProviderResult[] importResult;
+ public Map<AccountWithDataSet, Set<SimContact>> existingSimContacts;
+
+ public FakeSimContactDao() {
+ simCards = new ArrayList<>();
+ simContacts = new HashMap<>();
+ importResult = new ContentProviderResult[0];
+ existingSimContacts = new HashMap<>();
+ }
+
+ @Override
+ public boolean canReadSimContacts() {
+ return canReadSimContacts;
+ }
+
+ @Override
+ public List<SimCard> getSimCards() {
+ return simCards;
+ }
+
+ @Override
+ public ArrayList<SimContact> loadContactsForSim(SimCard sim) {
+ return simContacts.get(sim);
+ }
+
+ @Override
+ public ContentProviderResult[] importContacts(List<SimContact> contacts,
+ AccountWithDataSet targetAccount)
+ throws RemoteException, OperationApplicationException {
+ return importResult;
+ }
+
+ @Override
+ public void persistSimStates(List<SimCard> simCards) {
+ this.simCards = simCards;
+ }
+
+ @Override
+ public SimCard getSimBySubscriptionId(int subscriptionId) {
+ for (SimCard sim : simCards) {
+ if (sim.getSubscriptionId() == subscriptionId) {
+ return sim;
+ }
+ }
+ return null;
+ }
+
+ @Override
+ public Map<AccountWithDataSet, Set<SimContact>> findAccountsOfExistingSimContacts(
+ List<SimContact> contacts) {
+ return existingSimContacts;
+ }
+
+ public FakeSimContactDao addSim(SimCard sim, SimContact... contacts) {
+ simCards.add(sim);
+ simContacts.put(sim, new ArrayList<>(Arrays.asList(contacts)));
+ return this;
+ }
+
+ public static FakeSimContactDao singleSimWithContacts(SimCard sim, SimContact... contacts) {
+ return new FakeSimContactDao().addSim(sim, contacts);
+ }
+
+ public static FakeSimContactDao noSim() {
+ FakeSimContactDao result = new FakeSimContactDao();
+ result.canReadSimContacts = false;
+ return result;
+ }
+
+}
diff --git a/tests/src/com/android/contacts/tests/SimContactsTestHelper.java b/tests/src/com/android/contacts/tests/SimContactsTestHelper.java
index c2ffead..552d790 100644
--- a/tests/src/com/android/contacts/tests/SimContactsTestHelper.java
+++ b/tests/src/com/android/contacts/tests/SimContactsTestHelper.java
@@ -15,7 +15,6 @@
*/
package com.android.contacts.tests;
-import android.content.ContentProvider;
import android.content.ContentProviderOperation;
import android.content.ContentProviderResult;
import android.content.ContentResolver;
@@ -29,9 +28,10 @@
import android.support.test.InstrumentationRegistry;
import android.telephony.TelephonyManager;
+import com.android.contacts.common.database.SimContactDaoImpl;
+import com.android.contacts.common.model.SimCard;
import com.android.contacts.common.model.SimContact;
import com.android.contacts.common.database.SimContactDao;
-import com.android.contacts.common.test.mocks.MockContentProvider;
import java.util.ArrayList;
import java.util.List;
@@ -59,7 +59,7 @@
}
public int getSimContactCount() {
- Cursor cursor = mContext.getContentResolver().query(SimContactDao.ICC_CONTENT_URI,
+ Cursor cursor = mContext.getContentResolver().query(SimContactDaoImpl.ICC_CONTENT_URI,
null, null, null, null);
try {
return cursor.getCount();
@@ -68,32 +68,6 @@
}
}
- public ContentValues iccRow(long id, String name, String number, String emails) {
- ContentValues values = new ContentValues();
- values.put(SimContactDao._ID, id);
- values.put(SimContactDao.NAME, name);
- values.put(SimContactDao.NUMBER, number);
- values.put(SimContactDao.EMAILS, emails);
- return values;
- }
-
- public ContentProvider iccProviderExpectingNoQueries() {
- return new MockContentProvider();
- }
-
- public ContentProvider emptyIccProvider() {
- final MockContentProvider provider = new MockContentProvider();
- provider.expectQuery(SimContactDao.ICC_CONTENT_URI)
- .withDefaultProjection(
- SimContactDao._ID, SimContactDao.NAME,
- SimContactDao.NUMBER, SimContactDao.EMAILS)
- .withAnyProjection()
- .withAnySelection()
- .withAnySortOrder()
- .returnEmptyCursor();
- return provider;
- }
-
public Uri addSimContact(String name, String number) {
ContentValues values = new ContentValues();
// Oddly even though it's called name when querying we have to use "tag" for it to work
@@ -102,23 +76,26 @@
values.put("tag", name);
}
if (number != null) {
- values.put(SimContactDao.NUMBER, number);
+ values.put(SimContactDaoImpl.NUMBER, number);
}
- return mResolver.insert(SimContactDao.ICC_CONTENT_URI, values);
+ return mResolver.insert(SimContactDaoImpl.ICC_CONTENT_URI, values);
}
public ContentProviderResult[] deleteAllSimContacts()
throws RemoteException, OperationApplicationException {
- SimContactDao dao = SimContactDao.create(mContext);
- List<SimContact> contacts = dao.loadSimContacts();
+ final List<SimCard> sims = mSimDao.getSimCards();
+ if (sims.isEmpty()) {
+ throw new IllegalStateException("Expected SIM card");
+ }
+ final List<SimContact> contacts = mSimDao.loadContactsForSim(sims.get(0));
ArrayList<ContentProviderOperation> ops = new ArrayList<>();
for (SimContact contact : contacts) {
ops.add(ContentProviderOperation
- .newDelete(SimContactDao.ICC_CONTENT_URI)
+ .newDelete(SimContactDaoImpl.ICC_CONTENT_URI)
.withSelection(getWriteSelection(contact), null)
.build());
}
- return mResolver.applyBatch(SimContactDao.ICC_CONTENT_URI.getAuthority(), ops);
+ return mResolver.applyBatch(SimContactDaoImpl.ICC_CONTENT_URI.getAuthority(), ops);
}
public ContentProviderResult[] restore(ArrayList<ContentProviderOperation> restoreOps)
@@ -128,13 +105,17 @@
// Remove SIM contacts because we assume that caller wants the data to be in the exact
// state as when the restore ops were captured.
deleteAllSimContacts();
- return mResolver.applyBatch(SimContactDao.ICC_CONTENT_URI.getAuthority(), restoreOps);
+ return mResolver.applyBatch(SimContactDaoImpl.ICC_CONTENT_URI.getAuthority(), restoreOps);
}
public ArrayList<ContentProviderOperation> captureRestoreSnapshot() {
- ArrayList<SimContact> contacts = mSimDao.loadSimContacts();
+ final List<SimCard> sims = mSimDao.getSimCards();
+ if (sims.isEmpty()) {
+ throw new IllegalStateException("Expected SIM card");
+ }
+ final ArrayList<SimContact> contacts = mSimDao.loadContactsForSim(sims.get(0));
- ArrayList<ContentProviderOperation> ops = new ArrayList<>();
+ final ArrayList<ContentProviderOperation> ops = new ArrayList<>();
for (SimContact contact : contacts) {
final String[] emails = contact.getEmails();
if (emails != null && emails.length > 0) {
@@ -142,7 +123,7 @@
" Please manually remove SIM contacts with emails.");
}
ops.add(ContentProviderOperation
- .newInsert(SimContactDao.ICC_CONTENT_URI)
+ .newInsert(SimContactDaoImpl.ICC_CONTENT_URI)
.withValue("tag", contact.getName())
.withValue("number", contact.getPhone())
.build());
@@ -151,15 +132,15 @@
}
public String getWriteSelection(SimContact simContact) {
- return "tag='" + simContact.getName() + "' AND " + SimContactDao.NUMBER + "='" +
+ return "tag='" + simContact.getName() + "' AND " + SimContactDaoImpl.NUMBER + "='" +
simContact.getPhone() + "'";
}
public int deleteSimContact(@NonNull String name, @NonNull String number) {
// IccProvider doesn't use the selection args.
final String selection = "tag='" + name + "' AND " +
- SimContactDao.NUMBER + "='" + number + "'";
- return mResolver.delete(SimContactDao.ICC_CONTENT_URI, selection, null);
+ SimContactDaoImpl.NUMBER + "='" + number + "'";
+ return mResolver.delete(SimContactDaoImpl.ICC_CONTENT_URI, selection, null);
}
public boolean isSimReady() {