1 /* 2 * Copyright (C) 2008 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.hfp; 18 19 import static android.Manifest.permission.BLUETOOTH_CONNECT; 20 21 import android.app.Activity; 22 import android.bluetooth.BluetoothDevice; 23 import android.content.ContentResolver; 24 import android.content.Context; 25 import android.content.Intent; 26 import android.database.Cursor; 27 import android.net.Uri; 28 import android.os.Bundle; 29 import android.provider.CallLog.Calls; 30 import android.provider.ContactsContract.CommonDataKinds.Phone; 31 import android.provider.ContactsContract.PhoneLookup; 32 import android.telephony.PhoneNumberUtils; 33 import android.util.Log; 34 35 import com.android.bluetooth.BluetoothMethodProxy; 36 import com.android.bluetooth.R; 37 import com.android.bluetooth.Utils; 38 import com.android.bluetooth.util.DevicePolicyUtils; 39 import com.android.bluetooth.util.GsmAlphabet; 40 import com.android.internal.annotations.VisibleForTesting; 41 42 import java.util.HashMap; 43 44 /** 45 * Helper for managing phonebook presentation over AT commands 46 * @hide 47 */ 48 public class AtPhonebook { 49 private static final String TAG = "BluetoothAtPhonebook"; 50 private static final boolean DBG = false; 51 52 /** The projection to use when querying the call log database in response 53 * to AT+CPBR for the MC, RC, and DC phone books (missed, received, and 54 * dialed calls respectively) 55 */ 56 private static final String[] CALLS_PROJECTION = new String[]{ 57 Calls._ID, Calls.NUMBER, Calls.NUMBER_PRESENTATION 58 }; 59 60 /** The projection to use when querying the contacts database in response 61 * to AT+CPBR for the ME phonebook (saved phone numbers). 62 */ 63 private static final String[] PHONES_PROJECTION = new String[]{ 64 Phone._ID, Phone.DISPLAY_NAME, Phone.NUMBER, Phone.TYPE 65 }; 66 67 /** Android supports as many phonebook entries as the flash can hold, but 68 * BT periphals don't. Limit the number we'll report. */ 69 private static final int MAX_PHONEBOOK_SIZE = 16384; 70 71 private static final String OUTGOING_CALL_WHERE = Calls.TYPE + "=" + Calls.OUTGOING_TYPE; 72 private static final String INCOMING_CALL_WHERE = Calls.TYPE + "=" + Calls.INCOMING_TYPE; 73 private static final String MISSED_CALL_WHERE = Calls.TYPE + "=" + Calls.MISSED_TYPE; 74 75 @VisibleForTesting 76 class PhonebookResult { 77 public Cursor cursor; // result set of last query 78 public int numberColumn; 79 public int numberPresentationColumn; 80 public int typeColumn; 81 public int nameColumn; 82 } 83 84 private Context mContext; 85 private ContentResolver mContentResolver; 86 private HeadsetNativeInterface mNativeInterface; 87 @VisibleForTesting 88 String mCurrentPhonebook; 89 @VisibleForTesting 90 String mCharacterSet = "UTF-8"; 91 92 @VisibleForTesting 93 int mCpbrIndex1, mCpbrIndex2; 94 private boolean mCheckingAccessPermission; 95 96 // package and class name to which we send intent to check phone book access permission 97 private final String mPairingPackage; 98 99 @VisibleForTesting 100 final HashMap<String, PhonebookResult> mPhonebooks = 101 new HashMap<String, PhonebookResult>(4); 102 103 static final int TYPE_UNKNOWN = -1; 104 static final int TYPE_READ = 0; 105 static final int TYPE_SET = 1; 106 static final int TYPE_TEST = 2; 107 AtPhonebook(Context context, HeadsetNativeInterface nativeInterface)108 public AtPhonebook(Context context, HeadsetNativeInterface nativeInterface) { 109 mContext = context; 110 mPairingPackage = context.getString(R.string.pairing_ui_package); 111 mContentResolver = context.getContentResolver(); 112 mNativeInterface = nativeInterface; 113 mPhonebooks.put("DC", new PhonebookResult()); // dialled calls 114 mPhonebooks.put("RC", new PhonebookResult()); // received calls 115 mPhonebooks.put("MC", new PhonebookResult()); // missed calls 116 mPhonebooks.put("ME", new PhonebookResult()); // mobile phonebook 117 mCurrentPhonebook = "ME"; // default to mobile phonebook 118 mCpbrIndex1 = mCpbrIndex2 = -1; 119 } 120 cleanup()121 public void cleanup() { 122 mPhonebooks.clear(); 123 } 124 125 /** Returns the last dialled number, or null if no numbers have been called */ getLastDialledNumber()126 public String getLastDialledNumber() { 127 String[] projection = {Calls.NUMBER}; 128 Bundle queryArgs = new Bundle(); 129 queryArgs.putString(ContentResolver.QUERY_ARG_SQL_SELECTION, 130 Calls.TYPE + "=" + Calls.OUTGOING_TYPE); 131 queryArgs.putString(ContentResolver.QUERY_ARG_SQL_SORT_ORDER, Calls.DEFAULT_SORT_ORDER); 132 queryArgs.putInt(ContentResolver.QUERY_ARG_LIMIT, 1); 133 134 Cursor cursor = mContentResolver.query(Calls.CONTENT_URI, projection, queryArgs, null); 135 if (cursor == null) { 136 Log.w(TAG, "getLastDialledNumber, cursor is null"); 137 return null; 138 } 139 140 if (cursor.getCount() < 1) { 141 cursor.close(); 142 Log.w(TAG, "getLastDialledNumber, cursor.getCount is 0"); 143 return null; 144 } 145 cursor.moveToNext(); 146 int column = cursor.getColumnIndexOrThrow(Calls.NUMBER); 147 String number = cursor.getString(column); 148 cursor.close(); 149 return number; 150 } 151 getCheckingAccessPermission()152 public boolean getCheckingAccessPermission() { 153 return mCheckingAccessPermission; 154 } 155 setCheckingAccessPermission(boolean checkingAccessPermission)156 public void setCheckingAccessPermission(boolean checkingAccessPermission) { 157 mCheckingAccessPermission = checkingAccessPermission; 158 } 159 setCpbrIndex(int cpbrIndex)160 public void setCpbrIndex(int cpbrIndex) { 161 mCpbrIndex1 = mCpbrIndex2 = cpbrIndex; 162 } 163 getByteAddress(BluetoothDevice device)164 private byte[] getByteAddress(BluetoothDevice device) { 165 return Utils.getBytesFromAddress(device.getAddress()); 166 } 167 handleCscsCommand(String atString, int type, BluetoothDevice device)168 public void handleCscsCommand(String atString, int type, BluetoothDevice device) { 169 log("handleCscsCommand - atString = " + atString); 170 // Select Character Set 171 int atCommandResult = HeadsetHalConstants.AT_RESPONSE_ERROR; 172 int atCommandErrorCode = -1; 173 String atCommandResponse = null; 174 switch (type) { 175 case TYPE_READ: // Read 176 log("handleCscsCommand - Read Command"); 177 atCommandResponse = "+CSCS: \"" + mCharacterSet + "\""; 178 atCommandResult = HeadsetHalConstants.AT_RESPONSE_OK; 179 break; 180 case TYPE_TEST: // Test 181 log("handleCscsCommand - Test Command"); 182 atCommandResponse = ("+CSCS: (\"UTF-8\",\"IRA\",\"GSM\")"); 183 atCommandResult = HeadsetHalConstants.AT_RESPONSE_OK; 184 break; 185 case TYPE_SET: // Set 186 log("handleCscsCommand - Set Command"); 187 String[] args = atString.split("="); 188 if (args.length < 2 || args[1] == null) { 189 mNativeInterface.atResponseCode(device, atCommandResult, atCommandErrorCode); 190 break; 191 } 192 String characterSet = ((atString.split("="))[1]); 193 characterSet = characterSet.replace("\"", ""); 194 if (characterSet.equals("GSM") || characterSet.equals("IRA") || characterSet.equals( 195 "UTF-8") || characterSet.equals("UTF8")) { 196 mCharacterSet = characterSet; 197 atCommandResult = HeadsetHalConstants.AT_RESPONSE_OK; 198 } else { 199 atCommandErrorCode = BluetoothCmeError.OPERATION_NOT_SUPPORTED; 200 } 201 break; 202 case TYPE_UNKNOWN: 203 default: 204 log("handleCscsCommand - Invalid chars"); 205 atCommandErrorCode = BluetoothCmeError.TEXT_HAS_INVALID_CHARS; 206 } 207 if (atCommandResponse != null) { 208 mNativeInterface.atResponseString(device, atCommandResponse); 209 } 210 mNativeInterface.atResponseCode(device, atCommandResult, atCommandErrorCode); 211 } 212 handleCpbsCommand(String atString, int type, BluetoothDevice device)213 public void handleCpbsCommand(String atString, int type, BluetoothDevice device) { 214 // Select PhoneBook memory Storage 215 log("handleCpbsCommand - atString = " + atString); 216 int atCommandResult = HeadsetHalConstants.AT_RESPONSE_ERROR; 217 int atCommandErrorCode = -1; 218 String atCommandResponse = null; 219 switch (type) { 220 case TYPE_READ: // Read 221 log("handleCpbsCommand - read command"); 222 // Return current size and max size 223 if ("SM".equals(mCurrentPhonebook)) { 224 atCommandResponse = "+CPBS: \"SM\",0," + getMaxPhoneBookSize(0); 225 atCommandResult = HeadsetHalConstants.AT_RESPONSE_OK; 226 break; 227 } 228 PhonebookResult pbr = getPhonebookResult(mCurrentPhonebook, true); 229 if (pbr == null) { 230 atCommandErrorCode = BluetoothCmeError.OPERATION_NOT_SUPPORTED; 231 break; 232 } 233 int size = pbr.cursor.getCount(); 234 atCommandResponse = 235 "+CPBS: \"" + mCurrentPhonebook + "\"," + size + "," + getMaxPhoneBookSize( 236 size); 237 pbr.cursor.close(); 238 pbr.cursor = null; 239 atCommandResult = HeadsetHalConstants.AT_RESPONSE_OK; 240 break; 241 case TYPE_TEST: // Test 242 log("handleCpbsCommand - test command"); 243 atCommandResponse = ("+CPBS: (\"ME\",\"SM\",\"DC\",\"RC\",\"MC\")"); 244 atCommandResult = HeadsetHalConstants.AT_RESPONSE_OK; 245 break; 246 case TYPE_SET: // Set 247 log("handleCpbsCommand - set command"); 248 String[] args = atString.split("="); 249 // Select phonebook memory 250 if (args.length < 2 || args[1] == null) { 251 atCommandErrorCode = BluetoothCmeError.OPERATION_NOT_SUPPORTED; 252 break; 253 } 254 String pb = args[1].trim(); 255 while (pb.endsWith("\"")) { 256 pb = pb.substring(0, pb.length() - 1); 257 } 258 while (pb.startsWith("\"")) { 259 pb = pb.substring(1, pb.length()); 260 } 261 if (getPhonebookResult(pb, false) == null && !"SM".equals(pb)) { 262 if (DBG) { 263 log("Dont know phonebook: '" + pb + "'"); 264 } 265 atCommandErrorCode = BluetoothCmeError.OPERATION_NOT_ALLOWED; 266 break; 267 } 268 mCurrentPhonebook = pb; 269 atCommandResult = HeadsetHalConstants.AT_RESPONSE_OK; 270 break; 271 case TYPE_UNKNOWN: 272 default: 273 log("handleCpbsCommand - invalid chars"); 274 atCommandErrorCode = BluetoothCmeError.TEXT_HAS_INVALID_CHARS; 275 } 276 if (atCommandResponse != null) { 277 mNativeInterface.atResponseString(device, atCommandResponse); 278 } 279 mNativeInterface.atResponseCode(device, atCommandResult, atCommandErrorCode); 280 } 281 handleCpbrCommand(String atString, int type, BluetoothDevice remoteDevice)282 void handleCpbrCommand(String atString, int type, BluetoothDevice remoteDevice) { 283 log("handleCpbrCommand - atString = " + atString); 284 int atCommandResult = HeadsetHalConstants.AT_RESPONSE_ERROR; 285 int atCommandErrorCode = -1; 286 String atCommandResponse = null; 287 switch (type) { 288 case TYPE_TEST: // Test 289 /* Ideally we should return the maximum range of valid index's 290 * for the selected phone book, but this causes problems for the 291 * Parrot CK3300. So instead send just the range of currently 292 * valid index's. 293 */ 294 log("handleCpbrCommand - test command"); 295 int size; 296 if ("SM".equals(mCurrentPhonebook)) { 297 size = 0; 298 } else { 299 PhonebookResult pbr = getPhonebookResult(mCurrentPhonebook, true); //false); 300 if (pbr == null) { 301 atCommandErrorCode = BluetoothCmeError.OPERATION_NOT_ALLOWED; 302 mNativeInterface.atResponseCode(remoteDevice, atCommandResult, 303 atCommandErrorCode); 304 break; 305 } 306 size = pbr.cursor.getCount(); 307 log("handleCpbrCommand - size = " + size); 308 pbr.cursor.close(); 309 pbr.cursor = null; 310 } 311 if (size == 0) { 312 /* Sending "+CPBR: (1-0)" can confused some carkits, send "1-1" * instead */ 313 size = 1; 314 } 315 atCommandResponse = "+CPBR: (1-" + size + "),30,30"; 316 atCommandResult = HeadsetHalConstants.AT_RESPONSE_OK; 317 mNativeInterface.atResponseString(remoteDevice, atCommandResponse); 318 mNativeInterface.atResponseCode(remoteDevice, atCommandResult, atCommandErrorCode); 319 break; 320 // Read PhoneBook Entries 321 case TYPE_READ: 322 case TYPE_SET: // Set & read 323 // Phone Book Read Request 324 // AT+CPBR=<index1>[,<index2>] 325 log("handleCpbrCommand - set/read command"); 326 if (mCpbrIndex1 != -1) { 327 /* handling a CPBR at the moment, reject this CPBR command */ 328 atCommandErrorCode = BluetoothCmeError.OPERATION_NOT_ALLOWED; 329 mNativeInterface.atResponseCode(remoteDevice, atCommandResult, 330 atCommandErrorCode); 331 break; 332 } 333 // Parse indexes 334 int index1; 335 int index2; 336 if ((atString.split("=")).length < 2) { 337 mNativeInterface.atResponseCode(remoteDevice, atCommandResult, 338 atCommandErrorCode); 339 break; 340 } 341 String atCommand = (atString.split("="))[1]; 342 String[] indices = atCommand.split(","); 343 //replace AT command separator ';' from the index if any 344 for (int i = 0; i < indices.length; i++) { 345 indices[i] = indices[i].replace(';', ' ').trim(); 346 } 347 try { 348 index1 = Integer.parseInt(indices[0]); 349 if (indices.length == 1) { 350 index2 = index1; 351 } else { 352 index2 = Integer.parseInt(indices[1]); 353 } 354 } catch (Exception e) { 355 log("handleCpbrCommand - exception - invalid chars: " + e.toString()); 356 atCommandErrorCode = BluetoothCmeError.TEXT_HAS_INVALID_CHARS; 357 mNativeInterface.atResponseCode(remoteDevice, atCommandResult, 358 atCommandErrorCode); 359 break; 360 } 361 mCpbrIndex1 = index1; 362 mCpbrIndex2 = index2; 363 mCheckingAccessPermission = true; 364 365 int permission = checkAccessPermission(remoteDevice); 366 if (permission == BluetoothDevice.ACCESS_ALLOWED) { 367 mCheckingAccessPermission = false; 368 atCommandResult = processCpbrCommand(remoteDevice); 369 mCpbrIndex1 = mCpbrIndex2 = -1; 370 mNativeInterface.atResponseCode(remoteDevice, atCommandResult, 371 atCommandErrorCode); 372 break; 373 } else if (permission == BluetoothDevice.ACCESS_REJECTED) { 374 mCheckingAccessPermission = false; 375 mCpbrIndex1 = mCpbrIndex2 = -1; 376 mNativeInterface.atResponseCode(remoteDevice, 377 HeadsetHalConstants.AT_RESPONSE_ERROR, BluetoothCmeError.AG_FAILURE); 378 } 379 // If checkAccessPermission(remoteDevice) has returned 380 // BluetoothDevice.ACCESS_UNKNOWN, we will continue the process in 381 // HeadsetStateMachine.handleAccessPermissionResult(Intent) once HeadsetService 382 // receives BluetoothDevice.ACTION_CONNECTION_ACCESS_REPLY from Settings app. 383 break; 384 case TYPE_UNKNOWN: 385 default: 386 log("handleCpbrCommand - invalid chars"); 387 atCommandErrorCode = BluetoothCmeError.TEXT_HAS_INVALID_CHARS; 388 mNativeInterface.atResponseCode(remoteDevice, atCommandResult, atCommandErrorCode); 389 } 390 } 391 392 /** Get the most recent result for the given phone book, 393 * with the cursor ready to go. 394 * If force then re-query that phonebook 395 * Returns null if the cursor is not ready 396 */ 397 @VisibleForTesting getPhonebookResult(String pb, boolean force)398 synchronized PhonebookResult getPhonebookResult(String pb, boolean force) { 399 if (pb == null) { 400 return null; 401 } 402 PhonebookResult pbr = mPhonebooks.get(pb); 403 if (pbr == null) { 404 pbr = new PhonebookResult(); 405 } 406 if (force || pbr.cursor == null) { 407 if (!queryPhonebook(pb, pbr)) { 408 return null; 409 } 410 } 411 412 return pbr; 413 } 414 queryPhonebook(String pb, PhonebookResult pbr)415 private synchronized boolean queryPhonebook(String pb, PhonebookResult pbr) { 416 String where; 417 boolean ancillaryPhonebook = true; 418 419 if (pb.equals("ME")) { 420 ancillaryPhonebook = false; 421 where = null; 422 } else if (pb.equals("DC")) { 423 where = OUTGOING_CALL_WHERE; 424 } else if (pb.equals("RC")) { 425 where = INCOMING_CALL_WHERE; 426 } else if (pb.equals("MC")) { 427 where = MISSED_CALL_WHERE; 428 } else { 429 return false; 430 } 431 432 if (pbr.cursor != null) { 433 pbr.cursor.close(); 434 pbr.cursor = null; 435 } 436 437 if (ancillaryPhonebook) { 438 Bundle queryArgs = new Bundle(); 439 queryArgs.putString(ContentResolver.QUERY_ARG_SQL_SELECTION, where); 440 queryArgs.putString(ContentResolver.QUERY_ARG_SQL_SORT_ORDER, Calls.DEFAULT_SORT_ORDER); 441 queryArgs.putInt(ContentResolver.QUERY_ARG_LIMIT, MAX_PHONEBOOK_SIZE); 442 pbr.cursor = BluetoothMethodProxy.getInstance().contentResolverQuery(mContentResolver, 443 Calls.CONTENT_URI, CALLS_PROJECTION, queryArgs, null); 444 445 if (pbr.cursor == null) { 446 return false; 447 } 448 pbr.numberColumn = pbr.cursor.getColumnIndexOrThrow(Calls.NUMBER); 449 pbr.numberPresentationColumn = 450 pbr.cursor.getColumnIndexOrThrow(Calls.NUMBER_PRESENTATION); 451 pbr.typeColumn = -1; 452 pbr.nameColumn = -1; 453 } else { 454 Bundle queryArgs = new Bundle(); 455 queryArgs.putString(ContentResolver.QUERY_ARG_SQL_SELECTION, where); 456 queryArgs.putInt(ContentResolver.QUERY_ARG_LIMIT, MAX_PHONEBOOK_SIZE); 457 final Uri phoneContentUri = DevicePolicyUtils.getEnterprisePhoneUri(mContext); 458 pbr.cursor = BluetoothMethodProxy.getInstance().contentResolverQuery(mContentResolver, 459 phoneContentUri, PHONES_PROJECTION, queryArgs, null); 460 461 if (pbr.cursor == null) { 462 return false; 463 } 464 465 pbr.numberColumn = pbr.cursor.getColumnIndex(Phone.NUMBER); 466 pbr.numberPresentationColumn = -1; 467 pbr.typeColumn = pbr.cursor.getColumnIndex(Phone.TYPE); 468 pbr.nameColumn = pbr.cursor.getColumnIndex(Phone.DISPLAY_NAME); 469 } 470 Log.i(TAG, "Refreshed phonebook " + pb + " with " + pbr.cursor.getCount() + " results"); 471 return true; 472 } 473 resetAtState()474 synchronized void resetAtState() { 475 mCharacterSet = "UTF-8"; 476 mCpbrIndex1 = mCpbrIndex2 = -1; 477 mCheckingAccessPermission = false; 478 } 479 480 @VisibleForTesting getMaxPhoneBookSize(int currSize)481 synchronized int getMaxPhoneBookSize(int currSize) { 482 // some car kits ignore the current size and request max phone book 483 // size entries. Thus, it takes a long time to transfer all the 484 // entries. Use a heuristic to calculate the max phone book size 485 // considering future expansion. 486 // maxSize = currSize + currSize / 2 rounded up to nearest power of 2 487 // If currSize < 100, use 100 as the currSize 488 489 int maxSize = (currSize < 100) ? 100 : currSize; 490 maxSize += maxSize / 2; 491 return roundUpToPowerOfTwo(maxSize); 492 } 493 roundUpToPowerOfTwo(int x)494 private int roundUpToPowerOfTwo(int x) { 495 x |= x >> 1; 496 x |= x >> 2; 497 x |= x >> 4; 498 x |= x >> 8; 499 x |= x >> 16; 500 return x + 1; 501 } 502 503 // process CPBR command after permission check processCpbrCommand(BluetoothDevice device)504 /*package*/ int processCpbrCommand(BluetoothDevice device) { 505 log("processCpbrCommand"); 506 int atCommandResult = HeadsetHalConstants.AT_RESPONSE_ERROR; 507 int atCommandErrorCode = -1; 508 String atCommandResponse = null; 509 StringBuilder response = new StringBuilder(); 510 String record; 511 512 // Shortcut SM phonebook 513 if ("SM".equals(mCurrentPhonebook)) { 514 atCommandResult = HeadsetHalConstants.AT_RESPONSE_OK; 515 return atCommandResult; 516 } 517 518 // Check phonebook 519 PhonebookResult pbr = getPhonebookResult(mCurrentPhonebook, true); //false); 520 if (pbr == null) { 521 Log.e(TAG, "pbr is null"); 522 atCommandErrorCode = BluetoothCmeError.OPERATION_NOT_ALLOWED; 523 return atCommandResult; 524 } 525 526 // More sanity checks 527 // Send OK instead of ERROR if these checks fail. 528 // When we send error, certain kits like BMW disconnect the 529 // Handsfree connection. 530 if (pbr.cursor.getCount() == 0 || mCpbrIndex1 <= 0 || mCpbrIndex2 < mCpbrIndex1 531 || mCpbrIndex1 > pbr.cursor.getCount()) { 532 atCommandResult = HeadsetHalConstants.AT_RESPONSE_OK; 533 Log.e(TAG, "Invalid request or no results, returning"); 534 return atCommandResult; 535 } 536 537 if (mCpbrIndex2 > pbr.cursor.getCount()) { 538 Log.w(TAG, "max index requested is greater than number of records" 539 + " available, resetting it"); 540 mCpbrIndex2 = pbr.cursor.getCount(); 541 } 542 // Process 543 atCommandResult = HeadsetHalConstants.AT_RESPONSE_OK; 544 int errorDetected = -1; // no error 545 pbr.cursor.moveToPosition(mCpbrIndex1 - 1); 546 log("mCpbrIndex1 = " + mCpbrIndex1 + " and mCpbrIndex2 = " + mCpbrIndex2); 547 for (int index = mCpbrIndex1; index <= mCpbrIndex2; index++) { 548 String number = pbr.cursor.getString(pbr.numberColumn); 549 String name = null; 550 int type = -1; 551 if (pbr.nameColumn == -1 && number != null && number.length() > 0) { 552 // try caller id lookup 553 // TODO: This code is horribly inefficient. I saw it 554 // take 7 seconds to process 100 missed calls. 555 Cursor c = BluetoothMethodProxy.getInstance().contentResolverQuery(mContentResolver, 556 Uri.withAppendedPath(PhoneLookup.ENTERPRISE_CONTENT_FILTER_URI, number), 557 new String[]{ 558 PhoneLookup.DISPLAY_NAME, PhoneLookup.TYPE 559 }, null, null, null); 560 if (c != null) { 561 if (c.moveToFirst()) { 562 name = c.getString(0); 563 type = c.getInt(1); 564 } 565 c.close(); 566 } 567 if (DBG && name == null) { 568 log("Caller ID lookup failed for " + number); 569 } 570 571 } else if (pbr.nameColumn != -1) { 572 name = pbr.cursor.getString(pbr.nameColumn); 573 } else { 574 log("processCpbrCommand: empty name and number"); 575 } 576 if (name == null) { 577 name = ""; 578 } 579 name = name.trim(); 580 if (name.length() > 28) { 581 name = name.substring(0, 28); 582 } 583 584 if (pbr.typeColumn != -1) { 585 type = pbr.cursor.getInt(pbr.typeColumn); 586 name = name + "/" + getPhoneType(type); 587 } 588 589 if (number == null) { 590 number = ""; 591 } 592 int regionType = PhoneNumberUtils.toaFromString(number); 593 594 number = number.trim(); 595 number = PhoneNumberUtils.stripSeparators(number); 596 if (number.length() > 30) { 597 number = number.substring(0, 30); 598 } 599 int numberPresentation = Calls.PRESENTATION_ALLOWED; 600 if (pbr.numberPresentationColumn != -1) { 601 numberPresentation = pbr.cursor.getInt(pbr.numberPresentationColumn); 602 } 603 if (numberPresentation != Calls.PRESENTATION_ALLOWED) { 604 number = ""; 605 // TODO: there are 3 types of numbers should have resource 606 // strings for: unknown, private, and payphone 607 name = mContext.getString(R.string.unknownNumber); 608 } 609 610 // TODO(): Handle IRA commands. It's basically 611 // a 7 bit ASCII character set. 612 if (!name.isEmpty() && mCharacterSet.equals("GSM")) { 613 byte[] nameByte = GsmAlphabet.stringToGsm8BitPacked(name); 614 if (nameByte == null) { 615 name = mContext.getString(R.string.unknownNumber); 616 } else { 617 name = new String(nameByte); 618 } 619 } 620 621 record = "+CPBR: " + index + ",\"" + number + "\"," + regionType + ",\"" + name + "\""; 622 record = record + "\r\n\r\n"; 623 atCommandResponse = record; 624 mNativeInterface.atResponseString(device, atCommandResponse); 625 if (!pbr.cursor.moveToNext()) { 626 break; 627 } 628 } 629 if (pbr.cursor != null) { 630 pbr.cursor.close(); 631 pbr.cursor = null; 632 } 633 return atCommandResult; 634 } 635 636 /** 637 * Checks if the remote device has premission to read our phone book. 638 * If the return value is {@link BluetoothDevice#ACCESS_UNKNOWN}, it means this method has sent 639 * an Intent to Settings application to ask user preference. 640 * 641 * @return {@link BluetoothDevice#ACCESS_UNKNOWN}, {@link BluetoothDevice#ACCESS_ALLOWED} or 642 * {@link BluetoothDevice#ACCESS_REJECTED}. 643 */ 644 @VisibleForTesting checkAccessPermission(BluetoothDevice remoteDevice)645 int checkAccessPermission(BluetoothDevice remoteDevice) { 646 log("checkAccessPermission"); 647 int permission = remoteDevice.getPhonebookAccessPermission(); 648 649 if (permission == BluetoothDevice.ACCESS_UNKNOWN) { 650 log("checkAccessPermission - ACTION_CONNECTION_ACCESS_REQUEST"); 651 Intent intent = new Intent(BluetoothDevice.ACTION_CONNECTION_ACCESS_REQUEST); 652 intent.setPackage(mPairingPackage); 653 intent.putExtra(BluetoothDevice.EXTRA_ACCESS_REQUEST_TYPE, 654 BluetoothDevice.REQUEST_TYPE_PHONEBOOK_ACCESS); 655 intent.putExtra(BluetoothDevice.EXTRA_DEVICE, remoteDevice); 656 // Leave EXTRA_PACKAGE_NAME and EXTRA_CLASS_NAME field empty. 657 // BluetoothHandsfree's broadcast receiver is anonymous, cannot be targeted. 658 mContext.sendOrderedBroadcast(intent, BLUETOOTH_CONNECT, 659 Utils.getTempAllowlistBroadcastOptions(), null, null, 660 Activity.RESULT_OK, null, null); 661 } 662 663 return permission; 664 } 665 666 @VisibleForTesting getPhoneType(int type)667 static String getPhoneType(int type) { 668 switch (type) { 669 case Phone.TYPE_HOME: 670 return "H"; 671 case Phone.TYPE_MOBILE: 672 return "M"; 673 case Phone.TYPE_WORK: 674 return "W"; 675 case Phone.TYPE_FAX_HOME: 676 case Phone.TYPE_FAX_WORK: 677 return "F"; 678 case Phone.TYPE_OTHER: 679 case Phone.TYPE_CUSTOM: 680 default: 681 return "O"; 682 } 683 } 684 log(String msg)685 private static void log(String msg) { 686 Log.d(TAG, msg); 687 } 688 } 689