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