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