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