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