blob: be6b3231729e256b4cb33989071586f07230f197 [file] [log] [blame]
/*
* 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.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.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.compat.CompatUtils;
import com.android.contacts.model.SimCard;
import com.android.contacts.model.SimContact;
import com.android.contacts.model.account.AccountWithDataSet;
import com.android.contacts.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.HashSet;
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) {
this(context, context.getContentResolver(),
(TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE));
}
public SimContactDaoImpl(Context context, ContentResolver resolver,
TelephonyManager telephonyManager) {
mContext = context;
mResolver = resolver;
mTelephonyManager = telephonyManager;
}
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 HashSet<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);
if (cursor == null) {
// Assume null means there are no SIM contacts.
return new ArrayList<>(0);
}
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));
}
}
}