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