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 */ 16package com.android.contacts.database; 17 18import android.annotation.TargetApi; 19import android.content.ContentProviderOperation; 20import android.content.ContentProviderResult; 21import android.content.ContentResolver; 22import android.content.Context; 23import android.content.OperationApplicationException; 24import android.content.pm.PackageManager; 25import android.database.Cursor; 26import android.net.Uri; 27import android.os.Build; 28import android.os.RemoteException; 29import android.provider.BaseColumns; 30import android.provider.ContactsContract; 31import android.provider.ContactsContract.CommonDataKinds.Phone; 32import android.provider.ContactsContract.CommonDataKinds.StructuredName; 33import android.provider.ContactsContract.Data; 34import android.provider.ContactsContract.RawContacts; 35import android.support.annotation.VisibleForTesting; 36import android.support.v4.util.ArrayMap; 37import android.telephony.SubscriptionInfo; 38import android.telephony.SubscriptionManager; 39import android.telephony.TelephonyManager; 40import android.text.TextUtils; 41import android.util.SparseArray; 42 43import com.android.contacts.R; 44import com.android.contacts.compat.CompatUtils; 45import com.android.contacts.model.SimCard; 46import com.android.contacts.model.SimContact; 47import com.android.contacts.model.account.AccountWithDataSet; 48import com.android.contacts.util.PermissionsUtil; 49import com.android.contacts.util.SharedPreferenceUtil; 50import com.google.common.base.Joiner; 51 52import java.util.ArrayList; 53import java.util.Arrays; 54import java.util.Collections; 55import java.util.HashMap; 56import java.util.HashSet; 57import java.util.List; 58import java.util.Map; 59import 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 */ 65public 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