• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 androidx.annotation.VisibleForTesting;
36 import androidx.collection.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 
SimContactDaoImpl(Context context)90     public SimContactDaoImpl(Context context) {
91         this(context, context.getContentResolver(),
92                 (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE));
93     }
94 
SimContactDaoImpl(Context context, ContentResolver resolver, TelephonyManager telephonyManager)95     public SimContactDaoImpl(Context context, ContentResolver resolver,
96             TelephonyManager telephonyManager) {
97         mContext = context;
98         mResolver = resolver;
99         mTelephonyManager = telephonyManager;
100     }
101 
getContext()102     public Context getContext() {
103         return mContext;
104     }
105 
106     @Override
canReadSimContacts()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
getSimCards()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
loadContactsForSim(SimCard sim)127     public ArrayList<SimContact> loadContactsForSim(SimCard sim) {
128         if (sim.hasValidSubscriptionId()) {
129             return loadSimContacts(sim.getSubscriptionId());
130         }
131         return loadSimContacts();
132     }
133 
loadSimContacts(int subscriptionId)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 
loadSimContacts()141     public ArrayList<SimContact> loadSimContacts() {
142         return loadFrom(ICC_CONTENT_URI);
143     }
144 
145     @Override
importContacts(List<SimContact> contacts, AccountWithDataSet targetAccount)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 
persistSimState(SimCard sim)161     public void persistSimState(SimCard sim) {
162         SharedPreferenceUtil.persistSimStates(mContext, Collections.singletonList(sim));
163     }
164 
165     @Override
persistSimStates(List<SimCard> simCards)166     public void persistSimStates(List<SimCard> simCards) {
167         SharedPreferenceUtil.persistSimStates(mContext, simCards);
168     }
169 
170     @Override
getSimBySubscriptionId(int subscriptionId)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      */
findAccountsOfExistingSimContacts( List<SimContact> contacts)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 
findAccountsOfExistingSimContacts(List<SimContact> contacts, Map<AccountWithDataSet, Set<SimContact>> result)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 
importBatch(List<SimContact> contacts, AccountWithDataSet targetAccount)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)
getSimCardsFromSubscriptions()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 
getContactsForSim(SimCard sim)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();
loadFrom(Uri uri)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 
loadFromCursor(Cursor cursor)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 
queryRawContactsForSimContacts(List<SimContact> contacts)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 
queryAccountsOfRawContacts(Set<Long> ids)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 
createImportOperations(List<SimContact> contacts, AccountWithDataSet targetAccount)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 
parseEmails(String emails)395     private String[] parseEmails(String emails) {
396         return !TextUtils.isEmpty(emails) ? emails.split(",") : null;
397     }
398 
hasTelephony()399     private boolean hasTelephony() {
400         return mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_TELEPHONY);
401     }
402 
hasPermissions()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 
DebugImpl(Context context)415         public DebugImpl(Context context) {
416             super(context);
417         }
418 
addSimCard(SimCard sim)419         public DebugImpl addSimCard(SimCard sim) {
420             mSimCards.add(sim);
421             mCardsBySubscription.put(sim.getSubscriptionId(), sim);
422             return this;
423         }
424 
425         @Override
getSimCards()426         public List<SimCard> getSimCards() {
427             return SharedPreferenceUtil.restoreSimStates(getContext(), mSimCards);
428         }
429 
430         @Override
loadContactsForSim(SimCard card)431         public ArrayList<SimContact> loadContactsForSim(SimCard card) {
432             return new ArrayList<>(card.getContacts());
433         }
434 
435         @Override
canReadSimContacts()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 
getRawContactId(Cursor cursor)453         public static long getRawContactId(Cursor cursor) {
454             return cursor.getLong(RAW_CONTACT_ID);
455         }
456 
getPhoneNumber(Cursor cursor)457         public static String getPhoneNumber(Cursor cursor) {
458             return isPhoneNumber(cursor) ? cursor.getString(PHONE_NUMBER) : null;
459         }
460 
getDisplayName(Cursor cursor)461         public static String getDisplayName(Cursor cursor) {
462             return cursor.getString(DISPLAY_NAME);
463         }
464 
isPhoneNumber(Cursor cursor)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 
getId(Cursor cursor)476         public static long getId(Cursor cursor) {
477             return cursor.getLong(0);
478         }
479 
getAccount(Cursor cursor)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