| /* |
| * 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)); |
| } |
| } |
| } |