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