• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2008 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.bluetooth.hfp;
18 
19 import com.android.bluetooth.R;
20 import com.android.internal.telephony.GsmAlphabet;
21 
22 import android.bluetooth.BluetoothDevice;
23 import android.content.ContentResolver;
24 import android.content.Context;
25 import android.content.Intent;
26 import android.database.Cursor;
27 import android.net.Uri;
28 import android.provider.CallLog.Calls;
29 import android.provider.ContactsContract.CommonDataKinds.Phone;
30 import android.provider.ContactsContract.PhoneLookup;
31 import android.telephony.PhoneNumberUtils;
32 import android.util.Log;
33 
34 import com.android.bluetooth.R;
35 import com.android.bluetooth.Utils;
36 import com.android.bluetooth.util.DevicePolicyUtils;
37 
38 import java.util.HashMap;
39 
40 /**
41  * Helper for managing phonebook presentation over AT commands
42  * @hide
43  */
44 public class AtPhonebook {
45     private static final String TAG = "BluetoothAtPhonebook";
46     private static final boolean DBG = false;
47 
48     /** The projection to use when querying the call log database in response
49      *  to AT+CPBR for the MC, RC, and DC phone books (missed, received, and
50      *   dialed calls respectively)
51      */
52     private static final String[] CALLS_PROJECTION = new String[] {
53         Calls._ID, Calls.NUMBER, Calls.NUMBER_PRESENTATION
54     };
55 
56     /** The projection to use when querying the contacts database in response
57      *   to AT+CPBR for the ME phonebook (saved phone numbers).
58      */
59     private static final String[] PHONES_PROJECTION = new String[] {
60         Phone._ID, Phone.DISPLAY_NAME, Phone.NUMBER, Phone.TYPE
61     };
62 
63     /** Android supports as many phonebook entries as the flash can hold, but
64      *  BT periphals don't. Limit the number we'll report. */
65     private static final int MAX_PHONEBOOK_SIZE = 16384;
66 
67     private static final String OUTGOING_CALL_WHERE = Calls.TYPE + "=" + Calls.OUTGOING_TYPE;
68     private static final String INCOMING_CALL_WHERE = Calls.TYPE + "=" + Calls.INCOMING_TYPE;
69     private static final String MISSED_CALL_WHERE = Calls.TYPE + "=" + Calls.MISSED_TYPE;
70 
71     private class PhonebookResult {
72         public Cursor  cursor; // result set of last query
73         public int     numberColumn;
74         public int     numberPresentationColumn;
75         public int     typeColumn;
76         public int     nameColumn;
77     };
78 
79     private Context mContext;
80     private ContentResolver mContentResolver;
81     private HeadsetStateMachine mStateMachine;
82     private String mCurrentPhonebook;
83     private String mCharacterSet = "UTF-8";
84 
85     private int mCpbrIndex1, mCpbrIndex2;
86     private boolean mCheckingAccessPermission;
87 
88     // package and class name to which we send intent to check phone book access permission
89     private final String mPairingPackage;
90     private static final String BLUETOOTH_ADMIN_PERM = android.Manifest.permission.BLUETOOTH_ADMIN;
91 
92     private final HashMap<String, PhonebookResult> mPhonebooks =
93             new HashMap<String, PhonebookResult>(4);
94 
95     final int TYPE_UNKNOWN = -1;
96     final int TYPE_READ = 0;
97     final int TYPE_SET = 1;
98     final int TYPE_TEST = 2;
99 
AtPhonebook(Context context, HeadsetStateMachine headsetState)100     public AtPhonebook(Context context, HeadsetStateMachine headsetState) {
101         mContext = context;
102         mPairingPackage = context.getString(R.string.pairing_ui_package);
103         mContentResolver = context.getContentResolver();
104         mStateMachine = headsetState;
105         mPhonebooks.put("DC", new PhonebookResult());  // dialled calls
106         mPhonebooks.put("RC", new PhonebookResult());  // received calls
107         mPhonebooks.put("MC", new PhonebookResult());  // missed calls
108         mPhonebooks.put("ME", new PhonebookResult());  // mobile phonebook
109 
110         mCurrentPhonebook = "ME";  // default to mobile phonebook
111 
112         mCpbrIndex1 = mCpbrIndex2 = -1;
113         mCheckingAccessPermission = false;
114     }
115 
cleanup()116     public void cleanup() {
117         mPhonebooks.clear();
118     }
119 
120     /** Returns the last dialled number, or null if no numbers have been called */
getLastDialledNumber()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 
getCheckingAccessPermission()139     public boolean getCheckingAccessPermission() {
140         return mCheckingAccessPermission;
141     }
142 
setCheckingAccessPermission(boolean checkingAccessPermission)143     public void setCheckingAccessPermission(boolean checkingAccessPermission) {
144         mCheckingAccessPermission = checkingAccessPermission;
145     }
146 
setCpbrIndex(int cpbrIndex)147     public void setCpbrIndex(int cpbrIndex) {
148         mCpbrIndex1 = mCpbrIndex2 = cpbrIndex;
149     }
150 
getByteAddress(BluetoothDevice device)151     private byte[] getByteAddress(BluetoothDevice device) {
152         return Utils.getBytesFromAddress(device.getAddress());
153     }
154 
handleCscsCommand(String atString, int type, BluetoothDevice device)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 
handleCpbsCommand(String atString, int type, BluetoothDevice device)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 
handleCpbrCommand(String atString, int type, BluetoothDevice remoteDevice)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      */
getPhonebookResult(String pb, boolean force)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 
queryPhonebook(String pb, PhonebookResult pbr)398     private synchronized boolean queryPhonebook(String pb, PhonebookResult pbr) {
399         String where;
400         boolean ancillaryPhonebook = true;
401 
402         if (pb.equals("ME")) {
403             ancillaryPhonebook = false;
404             where = null;
405         } else if (pb.equals("DC")) {
406             where = OUTGOING_CALL_WHERE;
407         } else if (pb.equals("RC")) {
408             where = INCOMING_CALL_WHERE;
409         } else if (pb.equals("MC")) {
410             where = MISSED_CALL_WHERE;
411         } else {
412             return false;
413         }
414 
415         if (pbr.cursor != null) {
416             pbr.cursor.close();
417             pbr.cursor = null;
418         }
419 
420         if (ancillaryPhonebook) {
421             pbr.cursor = mContentResolver.query(
422                     Calls.CONTENT_URI, CALLS_PROJECTION, where, null,
423                     Calls.DEFAULT_SORT_ORDER + " LIMIT " + MAX_PHONEBOOK_SIZE);
424             if (pbr.cursor == null) return false;
425 
426             pbr.numberColumn = pbr.cursor.getColumnIndexOrThrow(Calls.NUMBER);
427             pbr.numberPresentationColumn =
428                     pbr.cursor.getColumnIndexOrThrow(Calls.NUMBER_PRESENTATION);
429             pbr.typeColumn = -1;
430             pbr.nameColumn = -1;
431         } else {
432             final Uri phoneContentUri = DevicePolicyUtils.getEnterprisePhoneUri(mContext);
433             pbr.cursor = mContentResolver.query(phoneContentUri, PHONES_PROJECTION,
434                     where, null, Phone.NUMBER + " LIMIT " + MAX_PHONEBOOK_SIZE);
435             if (pbr.cursor == null) return false;
436 
437             pbr.numberColumn = pbr.cursor.getColumnIndex(Phone.NUMBER);
438             pbr.numberPresentationColumn = -1;
439             pbr.typeColumn = pbr.cursor.getColumnIndex(Phone.TYPE);
440             pbr.nameColumn = pbr.cursor.getColumnIndex(Phone.DISPLAY_NAME);
441         }
442         Log.i(TAG, "Refreshed phonebook " + pb + " with " + pbr.cursor.getCount() + " results");
443         return true;
444     }
445 
resetAtState()446     synchronized void resetAtState() {
447         mCharacterSet = "UTF-8";
448         mCpbrIndex1 = mCpbrIndex2 = -1;
449         mCheckingAccessPermission = false;
450     }
451 
getMaxPhoneBookSize(int currSize)452     private synchronized int getMaxPhoneBookSize(int currSize) {
453         // some car kits ignore the current size and request max phone book
454         // size entries. Thus, it takes a long time to transfer all the
455         // entries. Use a heuristic to calculate the max phone book size
456         // considering future expansion.
457         // maxSize = currSize + currSize / 2 rounded up to nearest power of 2
458         // If currSize < 100, use 100 as the currSize
459 
460         int maxSize = (currSize < 100) ? 100 : currSize;
461         maxSize += maxSize / 2;
462         return roundUpToPowerOfTwo(maxSize);
463     }
464 
roundUpToPowerOfTwo(int x)465     private int roundUpToPowerOfTwo(int x) {
466         x |= x >> 1;
467         x |= x >> 2;
468         x |= x >> 4;
469         x |= x >> 8;
470         x |= x >> 16;
471         return x + 1;
472     }
473 
474     // process CPBR command after permission check
processCpbrCommand(BluetoothDevice device)475     /*package*/ int processCpbrCommand(BluetoothDevice device)
476     {
477         log("processCpbrCommand");
478         int atCommandResult = HeadsetHalConstants.AT_RESPONSE_ERROR;
479         int atCommandErrorCode = -1;
480         String atCommandResponse = null;
481         StringBuilder response = new StringBuilder();
482         String record;
483 
484         // Shortcut SM phonebook
485         if ("SM".equals(mCurrentPhonebook)) {
486             atCommandResult = HeadsetHalConstants.AT_RESPONSE_OK;
487             return atCommandResult;
488         }
489 
490         // Check phonebook
491         PhonebookResult pbr = getPhonebookResult(mCurrentPhonebook, true); //false);
492         if (pbr == null) {
493             atCommandErrorCode = BluetoothCmeError.OPERATION_NOT_ALLOWED;
494             return atCommandResult;
495         }
496 
497         // More sanity checks
498         // Send OK instead of ERROR if these checks fail.
499         // When we send error, certain kits like BMW disconnect the
500         // Handsfree connection.
501         if (pbr.cursor.getCount() == 0 || mCpbrIndex1 <= 0 || mCpbrIndex2 < mCpbrIndex1  ||
502             mCpbrIndex2 > pbr.cursor.getCount() || mCpbrIndex1 > pbr.cursor.getCount()) {
503             atCommandResult = HeadsetHalConstants.AT_RESPONSE_OK;
504             return atCommandResult;
505         }
506 
507         // Process
508         atCommandResult = HeadsetHalConstants.AT_RESPONSE_OK;
509         int errorDetected = -1; // no error
510         pbr.cursor.moveToPosition(mCpbrIndex1 - 1);
511         log("mCpbrIndex1 = "+mCpbrIndex1+ " and mCpbrIndex2 = "+mCpbrIndex2);
512         for (int index = mCpbrIndex1; index <= mCpbrIndex2; index++) {
513             String number = pbr.cursor.getString(pbr.numberColumn);
514             String name = null;
515             int type = -1;
516             if (pbr.nameColumn == -1 && number != null && number.length() > 0) {
517                 // try caller id lookup
518                 // TODO: This code is horribly inefficient. I saw it
519                 // take 7 seconds to process 100 missed calls.
520                 Cursor c = mContentResolver.query(
521                         Uri.withAppendedPath(PhoneLookup.ENTERPRISE_CONTENT_FILTER_URI, number),
522                         new String[] {
523                                 PhoneLookup.DISPLAY_NAME, PhoneLookup.TYPE
524                         }, null, null, null);
525                 if (c != null) {
526                     if (c.moveToFirst()) {
527                         name = c.getString(0);
528                         type = c.getInt(1);
529                     }
530                     c.close();
531                 }
532                 if (DBG && name == null) log("Caller ID lookup failed for " + number);
533 
534             } else if (pbr.nameColumn != -1) {
535                 name = pbr.cursor.getString(pbr.nameColumn);
536             } else {
537                 log("processCpbrCommand: empty name and number");
538             }
539             if (name == null) name = "";
540             name = name.trim();
541             if (name.length() > 28) name = name.substring(0, 28);
542 
543             if (pbr.typeColumn != -1) {
544                 type = pbr.cursor.getInt(pbr.typeColumn);
545                 name = name + "/" + getPhoneType(type);
546             }
547 
548             if (number == null) number = "";
549             int regionType = PhoneNumberUtils.toaFromString(number);
550 
551             number = number.trim();
552             number = PhoneNumberUtils.stripSeparators(number);
553             if (number.length() > 30) number = number.substring(0, 30);
554             int numberPresentation = Calls.PRESENTATION_ALLOWED;
555             if (pbr.numberPresentationColumn != -1) {
556                 numberPresentation = pbr.cursor.getInt(pbr.numberPresentationColumn);
557             }
558             if (numberPresentation != Calls.PRESENTATION_ALLOWED) {
559                 number = "";
560                 // TODO: there are 3 types of numbers should have resource
561                 // strings for: unknown, private, and payphone
562                 name = mContext.getString(R.string.unknownNumber);
563             }
564 
565             // TODO(): Handle IRA commands. It's basically
566             // a 7 bit ASCII character set.
567             if (!name.equals("") && mCharacterSet.equals("GSM")) {
568                 byte[] nameByte = GsmAlphabet.stringToGsm8BitPacked(name);
569                 if (nameByte == null) {
570                     name = mContext.getString(R.string.unknownNumber);
571                 } else {
572                     name = new String(nameByte);
573                 }
574             }
575 
576             record = "+CPBR: " + index + ",\"" + number + "\"," + regionType + ",\"" + name + "\"";
577             record = record + "\r\n\r\n";
578             atCommandResponse = record;
579             mStateMachine.atResponseStringNative(atCommandResponse, getByteAddress(device));
580             if (!pbr.cursor.moveToNext()) {
581                 break;
582             }
583         }
584         if(pbr != null && pbr.cursor != null) {
585             pbr.cursor.close();
586             pbr.cursor = null;
587         }
588         return atCommandResult;
589     }
590 
591     /**
592      * Checks if the remote device has premission to read our phone book.
593      * If the return value is {@link BluetoothDevice#ACCESS_UNKNOWN}, it means this method has sent
594      * an Intent to Settings application to ask user preference.
595      *
596      * @return {@link BluetoothDevice#ACCESS_UNKNOWN}, {@link BluetoothDevice#ACCESS_ALLOWED} or
597      *         {@link BluetoothDevice#ACCESS_REJECTED}.
598      */
checkAccessPermission(BluetoothDevice remoteDevice)599     private int checkAccessPermission(BluetoothDevice remoteDevice) {
600         log("checkAccessPermission");
601         int permission = remoteDevice.getPhonebookAccessPermission();
602 
603         if (permission == BluetoothDevice.ACCESS_UNKNOWN) {
604             log("checkAccessPermission - ACTION_CONNECTION_ACCESS_REQUEST");
605             Intent intent = new Intent(BluetoothDevice.ACTION_CONNECTION_ACCESS_REQUEST);
606             intent.setPackage(mPairingPackage);
607             intent.putExtra(BluetoothDevice.EXTRA_ACCESS_REQUEST_TYPE,
608             BluetoothDevice.REQUEST_TYPE_PHONEBOOK_ACCESS);
609             intent.putExtra(BluetoothDevice.EXTRA_DEVICE, remoteDevice);
610             // Leave EXTRA_PACKAGE_NAME and EXTRA_CLASS_NAME field empty.
611             // BluetoothHandsfree's broadcast receiver is anonymous, cannot be targeted.
612             mContext.sendOrderedBroadcast(intent, BLUETOOTH_ADMIN_PERM);
613         }
614 
615         return permission;
616     }
617 
getPhoneType(int type)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 
log(String msg)636     private static void log(String msg) {
637         Log.d(TAG, msg);
638     }
639 }
640