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