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