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