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