Home | History | Annotate | Download | only in database
      1 /*
      2  * Copyright (C) 2016 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 package com.android.contacts.database;
     17 
     18 import android.annotation.TargetApi;
     19 import android.content.ContentProviderOperation;
     20 import android.content.ContentProviderResult;
     21 import android.content.ContentResolver;
     22 import android.content.Context;
     23 import android.content.OperationApplicationException;
     24 import android.content.pm.PackageManager;
     25 import android.database.Cursor;
     26 import android.net.Uri;
     27 import android.os.Build;
     28 import android.os.RemoteException;
     29 import android.provider.BaseColumns;
     30 import android.provider.ContactsContract;
     31 import android.provider.ContactsContract.CommonDataKinds.Phone;
     32 import android.provider.ContactsContract.CommonDataKinds.StructuredName;
     33 import android.provider.ContactsContract.Data;
     34 import android.provider.ContactsContract.RawContacts;
     35 import android.support.annotation.VisibleForTesting;
     36 import android.support.v4.util.ArrayMap;
     37 import android.telephony.SubscriptionInfo;
     38 import android.telephony.SubscriptionManager;
     39 import android.telephony.TelephonyManager;
     40 import android.text.TextUtils;
     41 import android.util.SparseArray;
     42 
     43 import com.android.contacts.R;
     44 import com.android.contacts.compat.CompatUtils;
     45 import com.android.contacts.model.SimCard;
     46 import com.android.contacts.model.SimContact;
     47 import com.android.contacts.model.account.AccountWithDataSet;
     48 import com.android.contacts.util.PermissionsUtil;
     49 import com.android.contacts.util.SharedPreferenceUtil;
     50 import com.google.common.base.Joiner;
     51 
     52 import java.util.ArrayList;
     53 import java.util.Arrays;
     54 import java.util.Collections;
     55 import java.util.HashMap;
     56 import java.util.HashSet;
     57 import java.util.List;
     58 import java.util.Map;
     59 import java.util.Set;
     60 
     61 /**
     62  * Provides data access methods for loading contacts from a SIM card and and migrating these
     63  * SIM contacts to a CP2 account.
     64  */
     65 public class SimContactDaoImpl extends SimContactDao {
     66     private static final String TAG = "SimContactDao";
     67 
     68     // Maximum number of SIM contacts to import in a single ContentResolver.applyBatch call.
     69     // This is necessary to avoid TransactionTooLargeException when there are a large number of
     70     // contacts. This has been tested on Nexus 6 NME70B and is probably be conservative enough
     71     // to work on any phone.
     72     private static final int IMPORT_MAX_BATCH_SIZE = 300;
     73 
     74     // How many SIM contacts to consider in a single query. This prevents hitting the SQLite
     75     // query parameter limit.
     76     static final int QUERY_MAX_BATCH_SIZE = 100;
     77 
     78     @VisibleForTesting
     79     public static final Uri ICC_CONTENT_URI = Uri.parse("content://icc/adn");
     80 
     81     public static String _ID = BaseColumns._ID;
     82     public static String NAME = "name";
     83     public static String NUMBER = "number";
     84     public static String EMAILS = "emails";
     85 
     86     private final Context mContext;
     87     private final ContentResolver mResolver;
     88     private final TelephonyManager mTelephonyManager;
     89 
     90     public SimContactDaoImpl(Context context) {
     91         this(context, context.getContentResolver(),
     92                 (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE));
     93     }
     94 
     95     public SimContactDaoImpl(Context context, ContentResolver resolver,
     96             TelephonyManager telephonyManager) {
     97         mContext = context;
     98         mResolver = resolver;
     99         mTelephonyManager = telephonyManager;
    100     }
    101 
    102     public Context getContext() {
    103         return mContext;
    104     }
    105 
    106     @Override
    107     public boolean canReadSimContacts() {
    108         // Require SIM_STATE_READY because the TelephonyManager methods related to SIM require
    109         // this state
    110         return hasTelephony() && hasPermissions() &&
    111                 mTelephonyManager.getSimState() == TelephonyManager.SIM_STATE_READY;
    112     }
    113 
    114     @Override
    115     public List<SimCard> getSimCards() {
    116         if (!canReadSimContacts()) {
    117             return Collections.emptyList();
    118         }
    119         final List<SimCard> sims = CompatUtils.isMSIMCompatible() ?
    120                 getSimCardsFromSubscriptions() :
    121                 Collections.singletonList(SimCard.create(mTelephonyManager,
    122                         mContext.getString(R.string.single_sim_display_label)));
    123         return SharedPreferenceUtil.restoreSimStates(mContext, sims);
    124     }
    125 
    126     @Override
    127     public ArrayList<SimContact> loadContactsForSim(SimCard sim) {
    128         if (sim.hasValidSubscriptionId()) {
    129             return loadSimContacts(sim.getSubscriptionId());
    130         }
    131         return loadSimContacts();
    132     }
    133 
    134     public ArrayList<SimContact> loadSimContacts(int subscriptionId) {
    135         return loadFrom(ICC_CONTENT_URI.buildUpon()
    136                 .appendPath("subId")
    137                 .appendPath(String.valueOf(subscriptionId))
    138                 .build());
    139     }
    140 
    141     public ArrayList<SimContact> loadSimContacts() {
    142         return loadFrom(ICC_CONTENT_URI);
    143     }
    144 
    145     @Override
    146     public ContentProviderResult[] importContacts(List<SimContact> contacts,
    147             AccountWithDataSet targetAccount)
    148             throws RemoteException, OperationApplicationException {
    149         if (contacts.size() < IMPORT_MAX_BATCH_SIZE) {
    150             return importBatch(contacts, targetAccount);
    151         }
    152         final List<ContentProviderResult> results = new ArrayList<>();
    153         for (int i = 0; i < contacts.size(); i += IMPORT_MAX_BATCH_SIZE) {
    154             results.addAll(Arrays.asList(importBatch(
    155                     contacts.subList(i, Math.min(contacts.size(), i + IMPORT_MAX_BATCH_SIZE)),
    156                     targetAccount)));
    157         }
    158         return results.toArray(new ContentProviderResult[results.size()]);
    159     }
    160 
    161     public void persistSimState(SimCard sim) {
    162         SharedPreferenceUtil.persistSimStates(mContext, Collections.singletonList(sim));
    163     }
    164 
    165     @Override
    166     public void persistSimStates(List<SimCard> simCards) {
    167         SharedPreferenceUtil.persistSimStates(mContext, simCards);
    168     }
    169 
    170     @Override
    171     public SimCard getSimBySubscriptionId(int subscriptionId) {
    172         final List<SimCard> sims = SharedPreferenceUtil.restoreSimStates(mContext, getSimCards());
    173         if (subscriptionId == SimCard.NO_SUBSCRIPTION_ID && !sims.isEmpty()) {
    174             return sims.get(0);
    175         }
    176         for (SimCard sim : getSimCards()) {
    177             if (sim.getSubscriptionId() == subscriptionId) {
    178                 return sim;
    179             }
    180         }
    181         return null;
    182     }
    183 
    184     /**
    185      * Finds SIM contacts that exist in CP2 and associates the account of the CP2 contact with
    186      * the SIM contact
    187      */
    188     public Map<AccountWithDataSet, Set<SimContact>> findAccountsOfExistingSimContacts(
    189             List<SimContact> contacts) {
    190         final Map<AccountWithDataSet, Set<SimContact>> result = new ArrayMap<>();
    191         for (int i = 0; i < contacts.size(); i += QUERY_MAX_BATCH_SIZE) {
    192             findAccountsOfExistingSimContacts(
    193                     contacts.subList(i, Math.min(contacts.size(), i + QUERY_MAX_BATCH_SIZE)),
    194                     result);
    195         }
    196         return result;
    197     }
    198 
    199     private void findAccountsOfExistingSimContacts(List<SimContact> contacts,
    200             Map<AccountWithDataSet, Set<SimContact>> result) {
    201         final Map<Long, List<SimContact>> rawContactToSimContact = new HashMap<>();
    202         Collections.sort(contacts, SimContact.compareByPhoneThenName());
    203 
    204         final Cursor dataCursor = queryRawContactsForSimContacts(contacts);
    205 
    206         try {
    207             while (dataCursor.moveToNext()) {
    208                 final String number = DataQuery.getPhoneNumber(dataCursor);
    209                 final String name = DataQuery.getDisplayName(dataCursor);
    210 
    211                 final int index = SimContact.findByPhoneAndName(contacts, number, name);
    212                 if (index < 0) {
    213                     continue;
    214                 }
    215                 final SimContact contact = contacts.get(index);
    216                 final long id = DataQuery.getRawContactId(dataCursor);
    217                 if (!rawContactToSimContact.containsKey(id)) {
    218                     rawContactToSimContact.put(id, new ArrayList<SimContact>());
    219                 }
    220                 rawContactToSimContact.get(id).add(contact);
    221             }
    222         } finally {
    223             dataCursor.close();
    224         }
    225 
    226         final Cursor accountsCursor = queryAccountsOfRawContacts(rawContactToSimContact.keySet());
    227         try {
    228             while (accountsCursor.moveToNext()) {
    229                 final AccountWithDataSet account = AccountQuery.getAccount(accountsCursor);
    230                 final long id = AccountQuery.getId(accountsCursor);
    231                 if (!result.containsKey(account)) {
    232                     result.put(account, new HashSet<SimContact>());
    233                 }
    234                 for (SimContact contact : rawContactToSimContact.get(id)) {
    235                     result.get(account).add(contact);
    236                 }
    237             }
    238         } finally {
    239             accountsCursor.close();
    240         }
    241     }
    242 
    243 
    244     private ContentProviderResult[] importBatch(List<SimContact> contacts,
    245             AccountWithDataSet targetAccount)
    246             throws RemoteException, OperationApplicationException {
    247         final ArrayList<ContentProviderOperation> ops =
    248                 createImportOperations(contacts, targetAccount);
    249         return mResolver.applyBatch(ContactsContract.AUTHORITY, ops);
    250     }
    251 
    252     @TargetApi(Build.VERSION_CODES.LOLLIPOP_MR1)
    253     private List<SimCard> getSimCardsFromSubscriptions() {
    254         final SubscriptionManager subscriptionManager = (SubscriptionManager)
    255                 mContext.getSystemService(Context.TELEPHONY_SUBSCRIPTION_SERVICE);
    256         final List<SubscriptionInfo> subscriptions = subscriptionManager
    257                 .getActiveSubscriptionInfoList();
    258         final ArrayList<SimCard> result = new ArrayList<>();
    259         for (SubscriptionInfo subscriptionInfo : subscriptions) {
    260             result.add(SimCard.create(subscriptionInfo));
    261         }
    262         return result;
    263     }
    264 
    265     private List<SimContact> getContactsForSim(SimCard sim) {
    266         final List<SimContact> contacts = sim.getContacts();
    267         return contacts != null ? contacts : loadContactsForSim(sim);
    268     }
    269 
    270     // See b/32831092
    271     // Sometimes the SIM contacts provider seems to get stuck if read from multiple threads
    272     // concurrently. So we just have a global lock around it to prevent potential issues.
    273     private static final Object SIM_READ_LOCK = new Object();
    274     private ArrayList<SimContact> loadFrom(Uri uri) {
    275         synchronized (SIM_READ_LOCK) {
    276             final Cursor cursor = mResolver.query(uri, null, null, null, null);
    277             if (cursor == null) {
    278                 // Assume null means there are no SIM contacts.
    279                 return new ArrayList<>(0);
    280             }
    281 
    282             try {
    283                 return loadFromCursor(cursor);
    284             } finally {
    285                 cursor.close();
    286             }
    287         }
    288     }
    289 
    290     private ArrayList<SimContact> loadFromCursor(Cursor cursor) {
    291         final int colId = cursor.getColumnIndex(_ID);
    292         final int colName = cursor.getColumnIndex(NAME);
    293         final int colNumber = cursor.getColumnIndex(NUMBER);
    294         final int colEmails = cursor.getColumnIndex(EMAILS);
    295 
    296         final ArrayList<SimContact> result = new ArrayList<>();
    297 
    298         while (cursor.moveToNext()) {
    299             final long id = cursor.getLong(colId);
    300             final String name = cursor.getString(colName);
    301             final String number = cursor.getString(colNumber);
    302             final String emails = cursor.getString(colEmails);
    303 
    304             final SimContact contact = new SimContact(id, name, number, parseEmails(emails));
    305             // Only include contact if it has some useful data
    306             if (contact.hasName() || contact.hasPhone() || contact.hasEmails()) {
    307                 result.add(contact);
    308             }
    309         }
    310         return result;
    311     }
    312 
    313     private Cursor queryRawContactsForSimContacts(List<SimContact> contacts) {
    314         final StringBuilder selectionBuilder = new StringBuilder();
    315 
    316         int phoneCount = 0;
    317         int nameCount = 0;
    318         for (SimContact contact : contacts) {
    319             if (contact.hasPhone()) {
    320                 phoneCount++;
    321             } else if (contact.hasName()) {
    322                 nameCount++;
    323             }
    324         }
    325         List<String> selectionArgs = new ArrayList<>(phoneCount + 1);
    326 
    327         selectionBuilder.append('(');
    328         selectionBuilder.append(Data.MIMETYPE).append("=? AND ");
    329         selectionArgs.add(Phone.CONTENT_ITEM_TYPE);
    330 
    331         selectionBuilder.append(Phone.NUMBER).append(" IN (")
    332                 .append(Joiner.on(',').join(Collections.nCopies(phoneCount, '?')))
    333                 .append(')');
    334         for (SimContact contact : contacts) {
    335             if (contact.hasPhone()) {
    336                 selectionArgs.add(contact.getPhone());
    337             }
    338         }
    339         selectionBuilder.append(')');
    340 
    341         if (nameCount > 0) {
    342             selectionBuilder.append(" OR (");
    343 
    344             selectionBuilder.append(Data.MIMETYPE).append("=? AND ");
    345             selectionArgs.add(StructuredName.CONTENT_ITEM_TYPE);
    346 
    347             selectionBuilder.append(Data.DISPLAY_NAME).append(" IN (")
    348                     .append(Joiner.on(',').join(Collections.nCopies(nameCount, '?')))
    349                     .append(')');
    350             for (SimContact contact : contacts) {
    351                 if (!contact.hasPhone() && contact.hasName()) {
    352                     selectionArgs.add(contact.getName());
    353                 }
    354             }
    355             selectionBuilder.append(')');
    356         }
    357 
    358         return mResolver.query(Data.CONTENT_URI.buildUpon()
    359                         .appendQueryParameter(Data.VISIBLE_CONTACTS_ONLY, "true")
    360                         .build(),
    361                 DataQuery.PROJECTION,
    362                 selectionBuilder.toString(),
    363                 selectionArgs.toArray(new String[selectionArgs.size()]),
    364                 null);
    365     }
    366 
    367     private Cursor queryAccountsOfRawContacts(Set<Long> ids) {
    368         final StringBuilder selectionBuilder = new StringBuilder();
    369 
    370         final String[] args = new String[ids.size()];
    371 
    372         selectionBuilder.append(RawContacts._ID).append(" IN (")
    373                 .append(Joiner.on(',').join(Collections.nCopies(args.length, '?')))
    374                 .append(")");
    375         int i = 0;
    376         for (long id : ids) {
    377             args[i++] = String.valueOf(id);
    378         }
    379         return mResolver.query(RawContacts.CONTENT_URI,
    380                 AccountQuery.PROJECTION,
    381                 selectionBuilder.toString(),
    382                 args,
    383                 null);
    384     }
    385 
    386     private ArrayList<ContentProviderOperation> createImportOperations(List<SimContact> contacts,
    387             AccountWithDataSet targetAccount) {
    388         final ArrayList<ContentProviderOperation> ops = new ArrayList<>();
    389         for (SimContact contact : contacts) {
    390             contact.appendCreateContactOperations(ops, targetAccount);
    391         }
    392         return ops;
    393     }
    394 
    395     private String[] parseEmails(String emails) {
    396         return !TextUtils.isEmpty(emails) ? emails.split(",") : null;
    397     }
    398 
    399     private boolean hasTelephony() {
    400         return mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_TELEPHONY);
    401     }
    402 
    403     private boolean hasPermissions() {
    404         return PermissionsUtil.hasContactsPermissions(mContext) &&
    405                 PermissionsUtil.hasPhonePermissions(mContext);
    406     }
    407 
    408     // TODO remove this class and the USE_FAKE_INSTANCE flag once this code is not under
    409     // active development or anytime after 3/1/2017
    410     public static class DebugImpl extends SimContactDaoImpl {
    411 
    412         private List<SimCard> mSimCards = new ArrayList<>();
    413         private SparseArray<SimCard> mCardsBySubscription = new SparseArray<>();
    414 
    415         public DebugImpl(Context context) {
    416             super(context);
    417         }
    418 
    419         public DebugImpl addSimCard(SimCard sim) {
    420             mSimCards.add(sim);
    421             mCardsBySubscription.put(sim.getSubscriptionId(), sim);
    422             return this;
    423         }
    424 
    425         @Override
    426         public List<SimCard> getSimCards() {
    427             return SharedPreferenceUtil.restoreSimStates(getContext(), mSimCards);
    428         }
    429 
    430         @Override
    431         public ArrayList<SimContact> loadContactsForSim(SimCard card) {
    432             return new ArrayList<>(card.getContacts());
    433         }
    434 
    435         @Override
    436         public boolean canReadSimContacts() {
    437             return true;
    438         }
    439     }
    440 
    441     // Query used for detecting existing contacts that may match a SimContact.
    442     private static final class DataQuery {
    443 
    444         public static final String[] PROJECTION = new String[] {
    445                 Data.RAW_CONTACT_ID, Phone.NUMBER, Data.DISPLAY_NAME, Data.MIMETYPE
    446         };
    447 
    448         public static final int RAW_CONTACT_ID = 0;
    449         public static final int PHONE_NUMBER = 1;
    450         public static final int DISPLAY_NAME = 2;
    451         public static final int MIMETYPE = 3;
    452 
    453         public static long getRawContactId(Cursor cursor) {
    454             return cursor.getLong(RAW_CONTACT_ID);
    455         }
    456 
    457         public static String getPhoneNumber(Cursor cursor) {
    458             return isPhoneNumber(cursor) ? cursor.getString(PHONE_NUMBER) : null;
    459         }
    460 
    461         public static String getDisplayName(Cursor cursor) {
    462             return cursor.getString(DISPLAY_NAME);
    463         }
    464 
    465         public static boolean isPhoneNumber(Cursor cursor) {
    466             return Phone.CONTENT_ITEM_TYPE.equals(cursor.getString(MIMETYPE));
    467         }
    468     }
    469 
    470     private static final class AccountQuery {
    471         public static final String[] PROJECTION = new String[] {
    472                 RawContacts._ID, RawContacts.ACCOUNT_NAME, RawContacts.ACCOUNT_TYPE,
    473                 RawContacts.DATA_SET
    474         };
    475 
    476         public static long getId(Cursor cursor) {
    477             return cursor.getLong(0);
    478         }
    479 
    480         public static AccountWithDataSet getAccount(Cursor cursor) {
    481             return new AccountWithDataSet(cursor.getString(1), cursor.getString(2),
    482                     cursor.getString(3));
    483         }
    484     }
    485 }
    486