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