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