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