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