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