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