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