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