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