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