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