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