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