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