1 /*
2  * Copyright (C) 2024 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 
17 package com.android.bluetooth.pbap;
18 
19 import android.bluetooth.BluetoothProfile;
20 import android.bluetooth.BluetoothProtoEnums;
21 import android.content.ContentResolver;
22 import android.content.ContentValues;
23 import android.content.Context;
24 import android.database.Cursor;
25 import android.database.sqlite.SQLiteException;
26 import android.net.Uri;
27 import android.provider.ContactsContract.CommonDataKinds;
28 import android.provider.ContactsContract.CommonDataKinds.Phone;
29 import android.provider.ContactsContract.CommonDataKinds.StructuredName;
30 import android.provider.ContactsContract.Contacts;
31 import android.text.TextUtils;
32 import android.util.Log;
33 
34 import com.android.bluetooth.BluetoothMethodProxy;
35 import com.android.bluetooth.BluetoothStatsLog;
36 import com.android.bluetooth.R;
37 import com.android.bluetooth.content_profiles.ContentProfileErrorReportUtils;
38 import com.android.internal.annotations.VisibleForTesting;
39 import com.android.obex.Operation;
40 import com.android.obex.ResponseCodes;
41 import com.android.obex.ServerOperation;
42 import com.android.vcard.VCardBuilder;
43 import com.android.vcard.VCardConfig;
44 
45 import java.util.ArrayList;
46 import java.util.Collections;
47 import java.util.Comparator;
48 import java.util.List;
49 
50 /** VCard composer especially for Call Log used in Bluetooth. */
51 // Next tag value for ContentProfileErrorReportUtils.report(): 6
52 public class BluetoothPbapSimVcardManager {
53     private static final String TAG = "PbapSIMvCardComposer";
54 
55     @VisibleForTesting
56     public static final String FAILURE_REASON_FAILED_TO_GET_DATABASE_INFO =
57             "Failed to get database information";
58 
59     @VisibleForTesting
60     public static final String FAILURE_REASON_NO_ENTRY = "There's no exportable in the database";
61 
62     @VisibleForTesting
63     public static final String FAILURE_REASON_NOT_INITIALIZED =
64             "The vCard composer object is not correctly initialized";
65 
66     /** Should be visible only from developers... (no need to translate, hopefully) */
67     @VisibleForTesting
68     public static final String FAILURE_REASON_UNSUPPORTED_URI =
69             "The Uri vCard composer received is not supported by the composer.";
70 
71     @VisibleForTesting public static final String NO_ERROR = "No error";
72 
73     @VisibleForTesting public static final Uri SIM_URI = Uri.parse("content://icc/adn");
74 
75     @VisibleForTesting public static final String SIM_PATH = "/SIM1/telecom";
76 
77     private static final String[] SIM_PROJECTION =
78             new String[] {
79                 Contacts.DISPLAY_NAME,
80                 CommonDataKinds.Phone.NUMBER,
81                 CommonDataKinds.Phone.TYPE,
82                 CommonDataKinds.Phone.LABEL
83             };
84 
85     @VisibleForTesting public static final int NAME_COLUMN_INDEX = 0;
86     @VisibleForTesting public static final int NUMBER_COLUMN_INDEX = 1;
87     private static final int NUMBERTYPE_COLUMN_INDEX = 2;
88     private static final int NUMBERLABEL_COLUMN_INDEX = 3;
89 
90     private final Context mContext;
91     private ContentResolver mContentResolver;
92     private Cursor mCursor;
93     private boolean mTerminateIsCalled;
94     private String mErrorReason = NO_ERROR;
95 
BluetoothPbapSimVcardManager(final Context context)96     public BluetoothPbapSimVcardManager(final Context context) {
97         mContext = context;
98         mContentResolver = context.getContentResolver();
99     }
100 
init( final Uri contentUri, final String selection, final String[] selectionArgs, final String sortOrder)101     public boolean init(
102             final Uri contentUri,
103             final String selection,
104             final String[] selectionArgs,
105             final String sortOrder) {
106         if (!SIM_URI.equals(contentUri)) {
107             mErrorReason = FAILURE_REASON_UNSUPPORTED_URI;
108             return false;
109         }
110 
111         // checkpoint Figure out if we can apply selection, projection and sort order.
112         mCursor =
113                 BluetoothMethodProxy.getInstance()
114                         .contentResolverQuery(
115                                 mContentResolver,
116                                 contentUri,
117                                 SIM_PROJECTION,
118                                 null,
119                                 null,
120                                 sortOrder);
121 
122         if (mCursor == null) {
123             mErrorReason = FAILURE_REASON_FAILED_TO_GET_DATABASE_INFO;
124             return false;
125         }
126         if (mCursor.getCount() == 0 || !mCursor.moveToFirst()) {
127             try {
128                 mCursor.close();
129             } catch (SQLiteException e) {
130                 ContentProfileErrorReportUtils.report(
131                         BluetoothProfile.PBAP,
132                         BluetoothProtoEnums.BLUETOOTH_PBAP_SIM_VCARD_MANAGER,
133                         BluetoothStatsLog.BLUETOOTH_CONTENT_PROFILE_ERROR_REPORTED__TYPE__EXCEPTION,
134                         0);
135                 Log.e(TAG, "SQLiteException on Cursor#close(): " + e.getMessage());
136             } finally {
137                 mErrorReason = FAILURE_REASON_NO_ENTRY;
138                 mCursor = null;
139             }
140             return false;
141         }
142         return true;
143     }
144 
createOneEntry(boolean vcardVer21)145     public String createOneEntry(boolean vcardVer21) {
146         if (mCursor == null || mCursor.isAfterLast()) {
147             mErrorReason = FAILURE_REASON_NOT_INITIALIZED;
148             return null;
149         }
150         try {
151             return createOnevCardEntryInternal(vcardVer21);
152         } finally {
153             mCursor.moveToNext();
154         }
155     }
156 
createOnevCardEntryInternal(boolean vcardVer21)157     private String createOnevCardEntryInternal(boolean vcardVer21) {
158         final int vcardType =
159                 (vcardVer21
160                                 ? VCardConfig.VCARD_TYPE_V21_GENERIC
161                                 : VCardConfig.VCARD_TYPE_V30_GENERIC)
162                         | VCardConfig.FLAG_REFRAIN_PHONE_NUMBER_FORMATTING;
163         final VCardBuilder builder = new VCardBuilder(vcardType);
164         String name = mCursor.getString(NAME_COLUMN_INDEX);
165         if (TextUtils.isEmpty(name)) {
166             name = mCursor.getString(NUMBER_COLUMN_INDEX);
167         }
168         // Create ContentValues for making name as Structured name
169         List<ContentValues> contentValuesList = new ArrayList<ContentValues>();
170         ContentValues nameContentValues = new ContentValues();
171         nameContentValues.put(StructuredName.DISPLAY_NAME, name);
172         contentValuesList.add(nameContentValues);
173         builder.appendNameProperties(contentValuesList);
174 
175         String number = mCursor.getString(NUMBER_COLUMN_INDEX);
176         if (TextUtils.isEmpty(number)) {
177             // To avoid Spec violation and IOT issues, initialize with invalid number
178             number = "000000";
179         }
180         if (number.equals("-1")) {
181             number = mContext.getString(R.string.unknownNumber);
182         }
183 
184         // checkpoint Figure out what are the type and label
185         int type = mCursor.getInt(NUMBERTYPE_COLUMN_INDEX);
186         String label = mCursor.getString(NUMBERLABEL_COLUMN_INDEX);
187         if (type == 0) { // value for type is not present in db
188             type = Phone.TYPE_MOBILE;
189         }
190         if (TextUtils.isEmpty(label)) {
191             label = Integer.toString(type);
192         }
193         builder.appendTelLine(type, label, number, false);
194         return builder.toString();
195     }
196 
terminate()197     public void terminate() {
198         if (mCursor != null) {
199             try {
200                 mCursor.close();
201             } catch (SQLiteException e) {
202                 ContentProfileErrorReportUtils.report(
203                         BluetoothProfile.PBAP,
204                         BluetoothProtoEnums.BLUETOOTH_PBAP_SIM_VCARD_MANAGER,
205                         BluetoothStatsLog.BLUETOOTH_CONTENT_PROFILE_ERROR_REPORTED__TYPE__EXCEPTION,
206                         1);
207                 Log.e(TAG, "SQLiteException on Cursor#close(): " + e.getMessage());
208             }
209             mCursor = null;
210         }
211 
212         mTerminateIsCalled = true;
213     }
214 
215     @Override
finalize()216     public void finalize() {
217         if (!mTerminateIsCalled) {
218             terminate();
219         }
220     }
221 
getCount()222     public int getCount() {
223         if (mCursor == null) {
224             return 0;
225         }
226         return mCursor.getCount();
227     }
228 
isAfterLast()229     public boolean isAfterLast() {
230         if (mCursor == null) {
231             return false;
232         }
233         return mCursor.isAfterLast();
234     }
235 
moveToPosition(final int position, boolean sortalpha)236     public void moveToPosition(final int position, boolean sortalpha) {
237         if (mCursor == null) {
238             return;
239         }
240         if (sortalpha) {
241             setPositionByAlpha(position);
242             return;
243         }
244         mCursor.moveToPosition(position);
245     }
246 
getErrorReason()247     public String getErrorReason() {
248         return mErrorReason;
249     }
250 
setPositionByAlpha(int position)251     private void setPositionByAlpha(int position) {
252         if (mCursor == null) {
253             return;
254         }
255         ArrayList<String> nameList = new ArrayList<String>();
256         for (mCursor.moveToFirst(); !mCursor.isAfterLast(); mCursor.moveToNext()) {
257             String name = mCursor.getString(NAME_COLUMN_INDEX);
258             if (TextUtils.isEmpty(name)) {
259                 name = mContext.getString(android.R.string.unknownName);
260             }
261             nameList.add(name);
262         }
263 
264         Collections.sort(
265                 nameList,
266                 new Comparator<String>() {
267                     @Override
268                     public int compare(String str1, String str2) {
269                         return str1.compareToIgnoreCase(str2);
270                     }
271                 });
272 
273         for (mCursor.moveToFirst(); !mCursor.isAfterLast(); mCursor.moveToNext()) {
274             if (mCursor.getString(NAME_COLUMN_INDEX).equals(nameList.get(position))) {
275                 break;
276             }
277         }
278     }
279 
getSIMContactsSize()280     public final int getSIMContactsSize() {
281         int size = 0;
282         Cursor contactCursor = null;
283         try {
284             contactCursor =
285                     BluetoothMethodProxy.getInstance()
286                             .contentResolverQuery(
287                                     mContentResolver, SIM_URI, SIM_PROJECTION, null, null, null);
288             if (contactCursor != null) {
289                 size = contactCursor.getCount();
290             }
291         } finally {
292             if (contactCursor != null) {
293                 contactCursor.close();
294             }
295         }
296         return size;
297     }
298 
getSIMPhonebookNameList(final int orderByWhat)299     public final ArrayList<String> getSIMPhonebookNameList(final int orderByWhat) {
300         ArrayList<String> nameList = new ArrayList<String>();
301         nameList.add(BluetoothPbapService.getLocalPhoneName());
302         // Since owner card should always be 0.vcf, maintain a separate list to avoid sorting
303         ArrayList<String> allnames = new ArrayList<String>();
304         Cursor contactCursor = null;
305         try {
306             contactCursor =
307                     BluetoothMethodProxy.getInstance()
308                             .contentResolverQuery(
309                                     mContentResolver, SIM_URI, SIM_PROJECTION, null, null, null);
310             if (contactCursor != null) {
311                 for (contactCursor.moveToFirst();
312                         !contactCursor.isAfterLast();
313                         contactCursor.moveToNext()) {
314                     String name = contactCursor.getString(NAME_COLUMN_INDEX);
315                     if (TextUtils.isEmpty(name)) {
316                         name = mContext.getString(android.R.string.unknownName);
317                     }
318                     allnames.add(name);
319                 }
320             }
321         } finally {
322             if (contactCursor != null) {
323                 contactCursor.close();
324             }
325         }
326         if (orderByWhat == BluetoothPbapObexServer.ORDER_BY_INDEXED) {
327             Log.v(TAG, "getPhonebookNameList, order by index");
328         } else if (orderByWhat == BluetoothPbapObexServer.ORDER_BY_ALPHABETICAL) {
329             Log.v(TAG, "getPhonebookNameList, order by alpha");
330             Collections.sort(
331                     allnames,
332                     new Comparator<String>() {
333                         @Override
334                         public int compare(String str1, String str2) {
335                             return str1.compareToIgnoreCase(str2);
336                         }
337                     });
338         }
339 
340         nameList.addAll(allnames);
341         return nameList;
342     }
343 
getSIMContactNamesByNumber(final String phoneNumber)344     public final ArrayList<String> getSIMContactNamesByNumber(final String phoneNumber) {
345         ArrayList<String> nameList = new ArrayList<String>();
346         ArrayList<String> startNameList = new ArrayList<String>();
347         Cursor contactCursor = null;
348 
349         try {
350             contactCursor =
351                     BluetoothMethodProxy.getInstance()
352                             .contentResolverQuery(
353                                     mContentResolver, SIM_URI, SIM_PROJECTION, null, null, null);
354 
355             if (contactCursor != null) {
356                 for (contactCursor.moveToFirst();
357                         !contactCursor.isAfterLast();
358                         contactCursor.moveToNext()) {
359                     String number = contactCursor.getString(NUMBER_COLUMN_INDEX);
360                     if (number == null) {
361                         Log.v(TAG, "number is null");
362                         continue;
363                     }
364 
365                     Log.v(TAG, "number: " + number + " phoneNumber:" + phoneNumber);
366                     if ((number.endsWith(phoneNumber)) || (number.startsWith(phoneNumber))) {
367                         String name = contactCursor.getString(NAME_COLUMN_INDEX);
368                         if (TextUtils.isEmpty(name)) {
369                             name = mContext.getString(android.R.string.unknownName);
370                         }
371                         Log.v(TAG, "got name " + name + " by number " + phoneNumber);
372 
373                         if (number.endsWith(phoneNumber)) {
374                             Log.v(TAG, "Adding to end name list");
375                             nameList.add(name);
376                         } else {
377                             Log.v(TAG, "Adding to start name list");
378                             startNameList.add(name);
379                         }
380                     }
381                 }
382             }
383         } finally {
384             if (contactCursor != null) {
385                 contactCursor.close();
386             }
387         }
388         int startListSize = startNameList.size();
389         for (int index = 0; index < startListSize; index++) {
390             String object = startNameList.get(index);
391             if (!nameList.contains(object)) nameList.add(object);
392         }
393 
394         return nameList;
395     }
396 
composeAndSendSIMPhonebookVcards( Context context, Operation op, final int startPoint, final int endPoint, final boolean vcardType21, String ownerVCard)397     public static final int composeAndSendSIMPhonebookVcards(
398             Context context,
399             Operation op,
400             final int startPoint,
401             final int endPoint,
402             final boolean vcardType21,
403             String ownerVCard) {
404         if (startPoint < 1 || startPoint > endPoint) {
405             Log.e(TAG, "internal error: startPoint or endPoint is not correct.");
406             ContentProfileErrorReportUtils.report(
407                     BluetoothProfile.PBAP,
408                     BluetoothProtoEnums.BLUETOOTH_PBAP_SIM_VCARD_MANAGER,
409                     BluetoothStatsLog.BLUETOOTH_CONTENT_PROFILE_ERROR_REPORTED__TYPE__LOG_ERROR,
410                     2);
411             return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR;
412         }
413         BluetoothPbapSimVcardManager composer = null;
414         HandlerForStringBuffer buffer = null;
415         try {
416             composer = new BluetoothPbapSimVcardManager(context);
417             buffer = new HandlerForStringBuffer(op, ownerVCard);
418 
419             if (!composer.init(SIM_URI, null, null, null) || !buffer.init()) {
420                 return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR;
421             }
422             composer.moveToPosition(startPoint - 1, false);
423             for (int count = startPoint - 1; count < endPoint; count++) {
424                 if (BluetoothPbapObexServer.sIsAborted) {
425                     ((ServerOperation) op).setAborted(true);
426                     BluetoothPbapObexServer.sIsAborted = false;
427                     break;
428                 }
429                 String vcard = composer.createOneEntry(vcardType21);
430                 if (vcard == null) {
431                     Log.e(
432                             TAG,
433                             "Failed to read a contact. Error reason: "
434                                     + composer.getErrorReason()
435                                     + ", count:"
436                                     + count);
437                     ContentProfileErrorReportUtils.report(
438                             BluetoothProfile.PBAP,
439                             BluetoothProtoEnums.BLUETOOTH_PBAP_SIM_VCARD_MANAGER,
440                             BluetoothStatsLog
441                                     .BLUETOOTH_CONTENT_PROFILE_ERROR_REPORTED__TYPE__LOG_ERROR,
442                             3);
443                     return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR;
444                 }
445                 buffer.writeVCard(vcard);
446             }
447         } finally {
448             if (composer != null) {
449                 composer.terminate();
450             }
451             if (buffer != null) {
452                 buffer.terminate();
453             }
454         }
455         return ResponseCodes.OBEX_HTTP_OK;
456     }
457 
composeAndSendSIMPhonebookOneVcard( Context context, Operation op, final int offset, final boolean vcardType21, String ownerVCard, int orderByWhat)458     public static final int composeAndSendSIMPhonebookOneVcard(
459             Context context,
460             Operation op,
461             final int offset,
462             final boolean vcardType21,
463             String ownerVCard,
464             int orderByWhat) {
465         if (offset < 1) {
466             Log.e(TAG, "Internal error: offset is not correct.");
467             ContentProfileErrorReportUtils.report(
468                     BluetoothProfile.PBAP,
469                     BluetoothProtoEnums.BLUETOOTH_PBAP_SIM_VCARD_MANAGER,
470                     BluetoothStatsLog.BLUETOOTH_CONTENT_PROFILE_ERROR_REPORTED__TYPE__LOG_ERROR,
471                     4);
472             return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR;
473         }
474         Log.v(TAG, "composeAndSendSIMPhonebookOneVcard orderByWhat " + orderByWhat);
475         BluetoothPbapSimVcardManager composer = null;
476         HandlerForStringBuffer buffer = null;
477         try {
478             composer = new BluetoothPbapSimVcardManager(context);
479             buffer = new HandlerForStringBuffer(op, ownerVCard);
480             if (!composer.init(SIM_URI, null, null, null) || !buffer.init()) {
481                 return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR;
482             }
483             if (orderByWhat == BluetoothPbapObexServer.ORDER_BY_INDEXED) {
484                 composer.moveToPosition(offset - 1, false);
485             } else if (orderByWhat == BluetoothPbapObexServer.ORDER_BY_ALPHABETICAL) {
486                 composer.moveToPosition(offset - 1, true);
487             }
488             if (BluetoothPbapObexServer.sIsAborted) {
489                 ((ServerOperation) op).setAborted(true);
490                 BluetoothPbapObexServer.sIsAborted = false;
491             }
492             String vcard = composer.createOneEntry(vcardType21);
493             if (vcard == null) {
494                 Log.e(TAG, "Failed to read a contact. Error reason: " + composer.getErrorReason());
495                 ContentProfileErrorReportUtils.report(
496                         BluetoothProfile.PBAP,
497                         BluetoothProtoEnums.BLUETOOTH_PBAP_SIM_VCARD_MANAGER,
498                         BluetoothStatsLog.BLUETOOTH_CONTENT_PROFILE_ERROR_REPORTED__TYPE__LOG_ERROR,
499                         5);
500                 return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR;
501             }
502             buffer.writeVCard(vcard);
503         } finally {
504             if (composer != null) {
505                 composer.terminate();
506             }
507             if (buffer != null) {
508                 buffer.terminate();
509             }
510         }
511 
512         return ResponseCodes.OBEX_HTTP_OK;
513     }
514 
isSimPhoneBook( String name, String type, String PB, String SIM1, String TYPE_PB, String TYPE_LISTING, String mCurrentPath)515     protected boolean isSimPhoneBook(
516             String name,
517             String type,
518             String PB,
519             String SIM1,
520             String TYPE_PB,
521             String TYPE_LISTING,
522             String mCurrentPath) {
523 
524         return ((name.contains(PB.subSequence(0, PB.length()))
525                                 && name.contains(SIM1.subSequence(0, SIM1.length())))
526                         && (type.equals(TYPE_PB)))
527                 || (((name.contains(PB.subSequence(0, PB.length())))
528                                 && (mCurrentPath.equals(SIM_PATH)))
529                         && (type.equals(TYPE_LISTING)));
530     }
531 
getType(String searchAttr)532     protected String getType(String searchAttr) {
533         String type = "";
534         if (searchAttr.equals("0")) {
535             type = "name";
536         } else if (searchAttr.equals("1")) {
537             type = "number";
538         }
539         return type;
540     }
541 }
542