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