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 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 
     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 
    111     public void cleanup() {
    112         mPhonebooks.clear();
    113     }
    114 
    115     /** Returns the last dialled number, or null if no numbers have been called */
    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 
    138     public boolean getCheckingAccessPermission() {
    139         return mCheckingAccessPermission;
    140     }
    141 
    142     public void setCheckingAccessPermission(boolean checkingAccessPermission) {
    143         mCheckingAccessPermission = checkingAccessPermission;
    144     }
    145 
    146     public void setCpbrIndex(int cpbrIndex) {
    147         mCpbrIndex1 = mCpbrIndex2 = cpbrIndex;
    148     }
    149 
    150     private byte[] getByteAddress(BluetoothDevice device) {
    151         return Utils.getBytesFromAddress(device.getAddress());
    152     }
    153 
    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 
    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 
    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      */
    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 
    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 
    451     synchronized void resetAtState() {
    452         mCharacterSet = "UTF-8";
    453         mCpbrIndex1 = mCpbrIndex2 = -1;
    454         mCheckingAccessPermission = false;
    455     }
    456 
    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 
    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
    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      */
    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 
    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 
    657     private static void log(String msg) {
    658         Log.d(TAG, msg);
    659     }
    660 }
    661