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