AtPhonebook.java revision 5006c8597521a7652eafa89a6fb5483b5cb567b6
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
17package com.android.bluetooth.hfp;
18
19import com.android.bluetooth.R;
20import com.android.internal.telephony.GsmAlphabet;
21
22import android.bluetooth.BluetoothDevice;
23import android.content.ContentResolver;
24import android.content.Context;
25import android.content.Intent;
26import android.database.Cursor;
27import android.net.Uri;
28import android.provider.CallLog.Calls;
29import android.provider.ContactsContract.CommonDataKinds.Phone;
30import android.provider.ContactsContract.PhoneLookup;
31import android.telephony.PhoneNumberUtils;
32import android.util.Log;
33
34import com.android.bluetooth.Utils;
35
36import java.util.HashMap;
37
38/**
39 * Helper for managing phonebook presentation over AT commands
40 * @hide
41 */
42public 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    private static final String VISIBLE_PHONEBOOK_WHERE = Phone.IN_VISIBLE_GROUP + "=1";
69
70    private class PhonebookResult {
71        public Cursor  cursor; // result set of last query
72        public int     numberColumn;
73        public int     numberPresentationColumn;
74        public int     typeColumn;
75        public int     nameColumn;
76    };
77
78    private Context mContext;
79    private ContentResolver mContentResolver;
80    private HeadsetStateMachine mStateMachine;
81    private String mCurrentPhonebook;
82    private String mCharacterSet = "UTF-8";
83
84    private int mCpbrIndex1, mCpbrIndex2;
85    private boolean mCheckingAccessPermission;
86
87    // package and class name to which we send intent to check phone book access permission
88    private static final String ACCESS_AUTHORITY_PACKAGE = "com.android.settings";
89    private static final String ACCESS_AUTHORITY_CLASS =
90        "com.android.settings.bluetooth.BluetoothPermissionRequest";
91    private static final String BLUETOOTH_ADMIN_PERM = android.Manifest.permission.BLUETOOTH_ADMIN;
92
93    private final HashMap<String, PhonebookResult> mPhonebooks =
94            new HashMap<String, PhonebookResult>(4);
95
96    final int TYPE_UNKNOWN = -1;
97    final int TYPE_READ = 0;
98    final int TYPE_SET = 1;
99    final int TYPE_TEST = 2;
100
101    public AtPhonebook(Context context, HeadsetStateMachine headsetState) {
102        mContext = context;
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 = VISIBLE_PHONEBOOK_WHERE;
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            pbr.cursor = mContentResolver.query(Phone.ENTERPRISE_CONTENT_URI, PHONES_PROJECTION,
433                    where, null, Phone.NUMBER + " LIMIT " + MAX_PHONEBOOK_SIZE);
434            if (pbr.cursor == null) return false;
435
436            pbr.numberColumn = pbr.cursor.getColumnIndex(Phone.NUMBER);
437            pbr.numberPresentationColumn = -1;
438            pbr.typeColumn = pbr.cursor.getColumnIndex(Phone.TYPE);
439            pbr.nameColumn = pbr.cursor.getColumnIndex(Phone.DISPLAY_NAME);
440        }
441        Log.i(TAG, "Refreshed phonebook " + pb + " with " + pbr.cursor.getCount() + " results");
442        return true;
443    }
444
445    synchronized void resetAtState() {
446        mCharacterSet = "UTF-8";
447        mCpbrIndex1 = mCpbrIndex2 = -1;
448        mCheckingAccessPermission = false;
449    }
450
451    private synchronized int getMaxPhoneBookSize(int currSize) {
452        // some car kits ignore the current size and request max phone book
453        // size entries. Thus, it takes a long time to transfer all the
454        // entries. Use a heuristic to calculate the max phone book size
455        // considering future expansion.
456        // maxSize = currSize + currSize / 2 rounded up to nearest power of 2
457        // If currSize < 100, use 100 as the currSize
458
459        int maxSize = (currSize < 100) ? 100 : currSize;
460        maxSize += maxSize / 2;
461        return roundUpToPowerOfTwo(maxSize);
462    }
463
464    private int roundUpToPowerOfTwo(int x) {
465        x |= x >> 1;
466        x |= x >> 2;
467        x |= x >> 4;
468        x |= x >> 8;
469        x |= x >> 16;
470        return x + 1;
471    }
472
473    // process CPBR command after permission check
474    /*package*/ int processCpbrCommand(BluetoothDevice device)
475    {
476        log("processCpbrCommand");
477        int atCommandResult = HeadsetHalConstants.AT_RESPONSE_ERROR;
478        int atCommandErrorCode = -1;
479        String atCommandResponse = null;
480        StringBuilder response = new StringBuilder();
481        String record;
482
483        // Shortcut SM phonebook
484        if ("SM".equals(mCurrentPhonebook)) {
485            atCommandResult = HeadsetHalConstants.AT_RESPONSE_OK;
486            return atCommandResult;
487        }
488
489        // Check phonebook
490        PhonebookResult pbr = getPhonebookResult(mCurrentPhonebook, true); //false);
491        if (pbr == null) {
492            atCommandErrorCode = BluetoothCmeError.OPERATION_NOT_ALLOWED;
493            return atCommandResult;
494        }
495
496        // More sanity checks
497        // Send OK instead of ERROR if these checks fail.
498        // When we send error, certain kits like BMW disconnect the
499        // Handsfree connection.
500        if (pbr.cursor.getCount() == 0 || mCpbrIndex1 <= 0 || mCpbrIndex2 < mCpbrIndex1  ||
501            mCpbrIndex2 > pbr.cursor.getCount() || mCpbrIndex1 > pbr.cursor.getCount()) {
502            atCommandResult = HeadsetHalConstants.AT_RESPONSE_OK;
503            return atCommandResult;
504        }
505
506        // Process
507        atCommandResult = HeadsetHalConstants.AT_RESPONSE_OK;
508        int errorDetected = -1; // no error
509        pbr.cursor.moveToPosition(mCpbrIndex1 - 1);
510        log("mCpbrIndex1 = "+mCpbrIndex1+ " and mCpbrIndex2 = "+mCpbrIndex2);
511        for (int index = mCpbrIndex1; index <= mCpbrIndex2; index++) {
512            String number = pbr.cursor.getString(pbr.numberColumn);
513            String name = null;
514            int type = -1;
515            if (pbr.nameColumn == -1 && number != null && number.length() > 0) {
516                // try caller id lookup
517                // TODO: This code is horribly inefficient. I saw it
518                // take 7 seconds to process 100 missed calls.
519                Cursor c = mContentResolver.query(
520                        Uri.withAppendedPath(PhoneLookup.ENTERPRISE_CONTENT_FILTER_URI, number),
521                        new String[] {
522                                PhoneLookup.DISPLAY_NAME, PhoneLookup.TYPE
523                        }, null, null, null);
524                if (c != null) {
525                    if (c.moveToFirst()) {
526                        name = c.getString(0);
527                        type = c.getInt(1);
528                    }
529                    c.close();
530                }
531                if (DBG && name == null) log("Caller ID lookup failed for " + number);
532
533            } else if (pbr.nameColumn != -1) {
534                name = pbr.cursor.getString(pbr.nameColumn);
535            } else {
536                log("processCpbrCommand: empty name and number");
537            }
538            if (name == null) name = "";
539            name = name.trim();
540            if (name.length() > 28) name = name.substring(0, 28);
541
542            if (pbr.typeColumn != -1) {
543                type = pbr.cursor.getInt(pbr.typeColumn);
544                name = name + "/" + getPhoneType(type);
545            }
546
547            if (number == null) number = "";
548            int regionType = PhoneNumberUtils.toaFromString(number);
549
550            number = number.trim();
551            number = PhoneNumberUtils.stripSeparators(number);
552            if (number.length() > 30) number = number.substring(0, 30);
553            int numberPresentation = Calls.PRESENTATION_ALLOWED;
554            if (pbr.numberPresentationColumn != -1) {
555                numberPresentation = pbr.cursor.getInt(pbr.numberPresentationColumn);
556            }
557            if (numberPresentation != Calls.PRESENTATION_ALLOWED) {
558                number = "";
559                // TODO: there are 3 types of numbers should have resource
560                // strings for: unknown, private, and payphone
561                name = mContext.getString(R.string.unknownNumber);
562            }
563
564            // TODO(): Handle IRA commands. It's basically
565            // a 7 bit ASCII character set.
566            if (!name.equals("") && mCharacterSet.equals("GSM")) {
567                byte[] nameByte = GsmAlphabet.stringToGsm8BitPacked(name);
568                if (nameByte == null) {
569                    name = mContext.getString(R.string.unknownNumber);
570                } else {
571                    name = new String(nameByte);
572                }
573            }
574
575            record = "+CPBR: " + index + ",\"" + number + "\"," + regionType + ",\"" + name + "\"";
576            record = record + "\r\n\r\n";
577            atCommandResponse = record;
578            log("processCpbrCommand - atCommandResponse = "+atCommandResponse);
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.setClassName(ACCESS_AUTHORITY_PACKAGE, ACCESS_AUTHORITY_CLASS);
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