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