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