• 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.phone;
18 
19 import com.android.internal.telephony.GsmAlphabet;
20 
21 import android.bluetooth.AtCommandHandler;
22 import android.bluetooth.AtCommandResult;
23 import android.bluetooth.AtParser;
24 import android.bluetooth.BluetoothDevice;
25 import android.bluetooth.HeadsetBase;
26 import android.content.Context;
27 import android.content.Intent;
28 import android.database.Cursor;
29 import android.net.Uri;
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 java.util.HashMap;
37 
38 /**
39  * Helper for managing phonebook presentation over AT commands
40  * @hide
41  */
42 public class BluetoothAtPhonebook {
43     private static final String TAG = "BluetoothAtPhonebook";
44     private static final boolean DBG = false;
45 
46     /** The projection to use when querying the call log database in response
47      *  to AT+CPBR for the MC, RC, and DC phone books (missed, received, and
48      *   dialed calls respectively)
49      */
50     private static final String[] CALLS_PROJECTION = new String[] {
51         Calls._ID, Calls.NUMBER
52     };
53 
54     /** The projection to use when querying the contacts database in response
55      *   to AT+CPBR for the ME phonebook (saved phone numbers).
56      */
57     private static final String[] PHONES_PROJECTION = new String[] {
58         Phone._ID, Phone.DISPLAY_NAME, Phone.NUMBER, Phone.TYPE
59     };
60 
61     /** Android supports as many phonebook entries as the flash can hold, but
62      *  BT periphals don't. Limit the number we'll report. */
63     private static final int MAX_PHONEBOOK_SIZE = 16384;
64 
65     private static final String OUTGOING_CALL_WHERE = Calls.TYPE + "=" + Calls.OUTGOING_TYPE;
66     private static final String INCOMING_CALL_WHERE = Calls.TYPE + "=" + Calls.INCOMING_TYPE;
67     private static final String MISSED_CALL_WHERE = Calls.TYPE + "=" + Calls.MISSED_TYPE;
68     private static final String VISIBLE_PHONEBOOK_WHERE = Phone.IN_VISIBLE_GROUP + "=1";
69 
70     private class PhonebookResult {
71         public Cursor  cursor; // result set of last query
72         public int     numberColumn;
73         public int     typeColumn;
74         public int     nameColumn;
75     };
76 
77     private final Context mContext;
78     private final BluetoothHandsfree mHandsfree;
79 
80     private String mCurrentPhonebook;
81     private String mCharacterSet = "UTF-8";
82 
83     private int mCpbrIndex1, mCpbrIndex2;
84     private boolean mCheckingAccessPermission;
85 
86     // package and class name to which we send intent to check phone book access permission
87     private static final String ACCESS_AUTHORITY_PACKAGE = "com.android.settings";
88     private static final String ACCESS_AUTHORITY_CLASS =
89         "com.android.settings.bluetooth.BluetoothPermissionRequest";
90     private static final String BLUETOOTH_ADMIN_PERM = android.Manifest.permission.BLUETOOTH_ADMIN;
91 
92     private final HashMap<String, PhonebookResult> mPhonebooks =
93             new HashMap<String, PhonebookResult>(4);
94 
BluetoothAtPhonebook(Context context, BluetoothHandsfree handsfree)95     public BluetoothAtPhonebook(Context context, BluetoothHandsfree handsfree) {
96         mContext = context;
97         mHandsfree = handsfree;
98         mPhonebooks.put("DC", new PhonebookResult());  // dialled calls
99         mPhonebooks.put("RC", new PhonebookResult());  // received calls
100         mPhonebooks.put("MC", new PhonebookResult());  // missed calls
101         mPhonebooks.put("ME", new PhonebookResult());  // mobile phonebook
102 
103         mCurrentPhonebook = "ME";  // default to mobile phonebook
104 
105         mCpbrIndex1 = mCpbrIndex2 = -1;
106         mCheckingAccessPermission = false;
107     }
108 
109     /** Returns the last dialled number, or null if no numbers have been called */
getLastDialledNumber()110     public String getLastDialledNumber() {
111         String[] projection = {Calls.NUMBER};
112         Cursor cursor = mContext.getContentResolver().query(Calls.CONTENT_URI, projection,
113                 Calls.TYPE + "=" + Calls.OUTGOING_TYPE, null, Calls.DEFAULT_SORT_ORDER +
114                 " LIMIT 1");
115         if (cursor == null) return null;
116 
117         if (cursor.getCount() < 1) {
118             cursor.close();
119             return null;
120         }
121         cursor.moveToNext();
122         int column = cursor.getColumnIndexOrThrow(Calls.NUMBER);
123         String number = cursor.getString(column);
124         cursor.close();
125         return number;
126     }
127 
register(AtParser parser)128     public void register(AtParser parser) {
129         // Select Character Set
130         parser.register("+CSCS", new AtCommandHandler() {
131             @Override
132             public AtCommandResult handleReadCommand() {
133                 String result = "+CSCS: \"" + mCharacterSet + "\"";
134                 return new AtCommandResult(result);
135             }
136             @Override
137             public AtCommandResult handleSetCommand(Object[] args) {
138                 if (args.length < 1) {
139                     return new AtCommandResult(AtCommandResult.ERROR);
140                 }
141                 String characterSet = (String)args[0];
142                 characterSet = characterSet.replace("\"", "");
143                 if (characterSet.equals("GSM") || characterSet.equals("IRA") ||
144                     characterSet.equals("UTF-8") || characterSet.equals("UTF8")) {
145                     mCharacterSet = characterSet;
146                     return new AtCommandResult(AtCommandResult.OK);
147                 } else {
148                     return mHandsfree.reportCmeError(BluetoothCmeError.OPERATION_NOT_SUPPORTED);
149                 }
150             }
151             @Override
152             public AtCommandResult handleTestCommand() {
153                 return new AtCommandResult( "+CSCS: (\"UTF-8\",\"IRA\",\"GSM\")");
154             }
155         });
156 
157         // Select PhoneBook memory Storage
158         parser.register("+CPBS", new AtCommandHandler() {
159             @Override
160             public AtCommandResult handleReadCommand() {
161                 // Return current size and max size
162                 if ("SM".equals(mCurrentPhonebook)) {
163                     return new AtCommandResult("+CPBS: \"SM\",0," + getMaxPhoneBookSize(0));
164                 }
165 
166                 PhonebookResult pbr = getPhonebookResult(mCurrentPhonebook, true);
167                 if (pbr == null) {
168                     return mHandsfree.reportCmeError(BluetoothCmeError.OPERATION_NOT_ALLOWED);
169                 }
170                 int size = pbr.cursor.getCount();
171                 return new AtCommandResult("+CPBS: \"" + mCurrentPhonebook + "\"," +
172                         size + "," + getMaxPhoneBookSize(size));
173             }
174             @Override
175             public AtCommandResult handleSetCommand(Object[] args) {
176                 // Select phonebook memory
177                 if (args.length < 1 || !(args[0] instanceof String)) {
178                     return new AtCommandResult(AtCommandResult.ERROR);
179                 }
180                 String pb = ((String)args[0]).trim();
181                 while (pb.endsWith("\"")) pb = pb.substring(0, pb.length() - 1);
182                 while (pb.startsWith("\"")) pb = pb.substring(1, pb.length());
183                 if (getPhonebookResult(pb, false) == null && !"SM".equals(pb)) {
184                     if (DBG) log("Dont know phonebook: '" + pb + "'");
185                     return mHandsfree.reportCmeError(BluetoothCmeError.OPERATION_NOT_SUPPORTED);
186                 }
187                 mCurrentPhonebook = pb;
188                 return new AtCommandResult(AtCommandResult.OK);
189             }
190             @Override
191             public AtCommandResult handleTestCommand() {
192                 return new AtCommandResult("+CPBS: (\"ME\",\"SM\",\"DC\",\"RC\",\"MC\")");
193             }
194         });
195 
196         // Read PhoneBook Entries
197         parser.register("+CPBR", new AtCommandHandler() {
198             @Override
199             public AtCommandResult handleSetCommand(Object[] args) {
200                 // Phone Book Read Request
201                 // AT+CPBR=<index1>[,<index2>]
202 
203                 if (mCpbrIndex1 != -1) {
204                     /* handling a CPBR at the moment, reject this CPBR command */
205                     return mHandsfree.reportCmeError(BluetoothCmeError.OPERATION_NOT_ALLOWED);
206                 }
207 
208                 // Parse indexes
209                 int index1;
210                 int index2;
211                 if (args.length < 1 || !(args[0] instanceof Integer)) {
212                     return new AtCommandResult(AtCommandResult.ERROR);
213                 } else {
214                     index1 = (Integer)args[0];
215                 }
216 
217                 if (args.length == 1) {
218                     index2 = index1;
219                 } else if (!(args[1] instanceof Integer)) {
220                     return mHandsfree.reportCmeError(BluetoothCmeError.TEXT_HAS_INVALID_CHARS);
221                 } else {
222                     index2 = (Integer)args[1];
223                 }
224 
225                 mCpbrIndex1 = index1;
226                 mCpbrIndex2 = index2;
227                 mCheckingAccessPermission = true;
228 
229                 if (checkAccessPermission()) {
230                     mCheckingAccessPermission = false;
231                     AtCommandResult atResult = processCpbrCommand();
232                     mCpbrIndex1 = mCpbrIndex2 = -1;
233                     return atResult;
234                 }
235 
236                 // no reponse here, will continue the process in handleAccessPermissionResult
237                 return new AtCommandResult(AtCommandResult.UNSOLICITED);
238             };
239 
240             @Override
241             public AtCommandResult handleTestCommand() {
242                 /* Ideally we should return the maximum range of valid index's
243                  * for the selected phone book, but this causes problems for the
244                  * Parrot CK3300. So instead send just the range of currently
245                  * valid index's.
246                  */
247                 int size;
248                 if ("SM".equals(mCurrentPhonebook)) {
249                     size = 0;
250                 } else {
251                     PhonebookResult pbr = getPhonebookResult(mCurrentPhonebook, false);
252                     if (pbr == null) {
253                         return mHandsfree.reportCmeError(BluetoothCmeError.OPERATION_NOT_ALLOWED);
254                     }
255                     size = pbr.cursor.getCount();
256                 }
257 
258                 if (size == 0) {
259                     /* Sending "+CPBR: (1-0)" can confused some carkits, send "1-1"
260                      * instead */
261                     size = 1;
262                 }
263                 return new AtCommandResult("+CPBR: (1-" + size + "),30,30");
264             }
265         });
266     }
267 
handleAccessPermissionResult(Intent intent)268     /* package */ void handleAccessPermissionResult(Intent intent) {
269         if (!mCheckingAccessPermission) {
270             return;
271         }
272 
273         HeadsetBase headset = mHandsfree.getHeadset();
274         // ASSERT: (headset != null) && headSet.isConnected()
275         // REASON: mCheckingAccessPermission is true, otherwise resetAtState
276         //         has set mCheckingAccessPermission to false
277 
278         if (intent.getAction().equals(BluetoothDevice.ACTION_CONNECTION_ACCESS_REPLY)) {
279 
280             if (intent.getIntExtra(BluetoothDevice.EXTRA_CONNECTION_ACCESS_RESULT,
281                                    BluetoothDevice.CONNECTION_ACCESS_NO) ==
282                 BluetoothDevice.CONNECTION_ACCESS_YES) {
283                 BluetoothDevice remoteDevice = headset.getRemoteDevice();
284                 if (intent.getBooleanExtra(BluetoothDevice.EXTRA_ALWAYS_ALLOWED, false)) {
285                     remoteDevice.setTrust(true);
286                 }
287 
288                 AtCommandResult cpbrResult = processCpbrCommand();
289                 headset.sendURC(cpbrResult.toString());
290             } else {
291                 headset.sendURC("ERROR");
292             }
293         }
294         mCpbrIndex1 = mCpbrIndex2 = -1;
295         mCheckingAccessPermission = false;
296     }
297 
298     /** Get the most recent result for the given phone book,
299      *  with the cursor ready to go.
300      *  If force then re-query that phonebook
301      *  Returns null if the cursor is not ready
302      */
getPhonebookResult(String pb, boolean force)303     private synchronized PhonebookResult getPhonebookResult(String pb, boolean force) {
304         if (pb == null) {
305             return null;
306         }
307         PhonebookResult pbr = mPhonebooks.get(pb);
308         if (pbr == null) {
309             pbr = new PhonebookResult();
310         }
311         if (force || pbr.cursor == null) {
312             if (!queryPhonebook(pb, pbr)) {
313                 return null;
314             }
315         }
316 
317         return pbr;
318     }
319 
queryPhonebook(String pb, PhonebookResult pbr)320     private synchronized boolean queryPhonebook(String pb, PhonebookResult pbr) {
321         String where;
322         boolean ancillaryPhonebook = true;
323 
324         if (pb.equals("ME")) {
325             ancillaryPhonebook = false;
326             where = VISIBLE_PHONEBOOK_WHERE;
327         } else if (pb.equals("DC")) {
328             where = OUTGOING_CALL_WHERE;
329         } else if (pb.equals("RC")) {
330             where = INCOMING_CALL_WHERE;
331         } else if (pb.equals("MC")) {
332             where = MISSED_CALL_WHERE;
333         } else {
334             return false;
335         }
336 
337         if (pbr.cursor != null) {
338             pbr.cursor.close();
339             pbr.cursor = null;
340         }
341 
342         if (ancillaryPhonebook) {
343             pbr.cursor = mContext.getContentResolver().query(
344                     Calls.CONTENT_URI, CALLS_PROJECTION, where, null,
345                     Calls.DEFAULT_SORT_ORDER + " LIMIT " + MAX_PHONEBOOK_SIZE);
346             if (pbr.cursor == null) return false;
347 
348             pbr.numberColumn = pbr.cursor.getColumnIndexOrThrow(Calls.NUMBER);
349             pbr.typeColumn = -1;
350             pbr.nameColumn = -1;
351         } else {
352             pbr.cursor = mContext.getContentResolver().query(Phone.CONTENT_URI, PHONES_PROJECTION,
353                     where, null, Phone.NUMBER + " LIMIT " + MAX_PHONEBOOK_SIZE);
354             if (pbr.cursor == null) return false;
355 
356             pbr.numberColumn = pbr.cursor.getColumnIndex(Phone.NUMBER);
357             pbr.typeColumn = pbr.cursor.getColumnIndex(Phone.TYPE);
358             pbr.nameColumn = pbr.cursor.getColumnIndex(Phone.DISPLAY_NAME);
359         }
360         Log.i(TAG, "Refreshed phonebook " + pb + " with " + pbr.cursor.getCount() + " results");
361         return true;
362     }
363 
resetAtState()364     synchronized void resetAtState() {
365         mCharacterSet = "UTF-8";
366         mCpbrIndex1 = mCpbrIndex2 = -1;
367         mCheckingAccessPermission = false;
368     }
369 
getMaxPhoneBookSize(int currSize)370     private synchronized int getMaxPhoneBookSize(int currSize) {
371         // some car kits ignore the current size and request max phone book
372         // size entries. Thus, it takes a long time to transfer all the
373         // entries. Use a heuristic to calculate the max phone book size
374         // considering future expansion.
375         // maxSize = currSize + currSize / 2 rounded up to nearest power of 2
376         // If currSize < 100, use 100 as the currSize
377 
378         int maxSize = (currSize < 100) ? 100 : currSize;
379         maxSize += maxSize / 2;
380         return roundUpToPowerOfTwo(maxSize);
381     }
382 
roundUpToPowerOfTwo(int x)383     private int roundUpToPowerOfTwo(int x) {
384         x |= x >> 1;
385         x |= x >> 2;
386         x |= x >> 4;
387         x |= x >> 8;
388         x |= x >> 16;
389         return x + 1;
390     }
391 
392     // process CPBR command after permission check
processCpbrCommand()393     private AtCommandResult processCpbrCommand()
394     {
395         // Shortcut SM phonebook
396         if ("SM".equals(mCurrentPhonebook)) {
397             return new AtCommandResult(AtCommandResult.OK);
398         }
399 
400         // Check phonebook
401         PhonebookResult pbr = getPhonebookResult(mCurrentPhonebook, false);
402         if (pbr == null) {
403             return mHandsfree.reportCmeError(BluetoothCmeError.OPERATION_NOT_ALLOWED);
404         }
405 
406         // More sanity checks
407         // Send OK instead of ERROR if these checks fail.
408         // When we send error, certain kits like BMW disconnect the
409         // Handsfree connection.
410         if (pbr.cursor.getCount() == 0 || mCpbrIndex1 <= 0 || mCpbrIndex2 < mCpbrIndex1  ||
411             mCpbrIndex2 > pbr.cursor.getCount() || mCpbrIndex1 > pbr.cursor.getCount()) {
412             return new AtCommandResult(AtCommandResult.OK);
413         }
414 
415         // Process
416         AtCommandResult result = new AtCommandResult(AtCommandResult.OK);
417         int errorDetected = -1; // no error
418         pbr.cursor.moveToPosition(mCpbrIndex1 - 1);
419         for (int index = mCpbrIndex1; index <= mCpbrIndex2; index++) {
420             String number = pbr.cursor.getString(pbr.numberColumn);
421             String name = null;
422             int type = -1;
423             if (pbr.nameColumn == -1) {
424                 // try caller id lookup
425                 // TODO: This code is horribly inefficient. I saw it
426                 // take 7 seconds to process 100 missed calls.
427                 Cursor c = mContext.getContentResolver().
428                     query(Uri.withAppendedPath(PhoneLookup.CONTENT_FILTER_URI, number),
429                           new String[] {PhoneLookup.DISPLAY_NAME, PhoneLookup.TYPE},
430                           null, null, null);
431                 if (c != null) {
432                     if (c.moveToFirst()) {
433                         name = c.getString(0);
434                         type = c.getInt(1);
435                     }
436                     c.close();
437                 }
438                 if (DBG && name == null) log("Caller ID lookup failed for " + number);
439 
440             } else {
441                 name = pbr.cursor.getString(pbr.nameColumn);
442             }
443             if (name == null) name = "";
444             name = name.trim();
445             if (name.length() > 28) name = name.substring(0, 28);
446 
447             if (pbr.typeColumn != -1) {
448                 type = pbr.cursor.getInt(pbr.typeColumn);
449                 name = name + "/" + getPhoneType(type);
450             }
451 
452             if (number == null) number = "";
453             int regionType = PhoneNumberUtils.toaFromString(number);
454 
455             number = number.trim();
456             number = PhoneNumberUtils.stripSeparators(number);
457             if (number.length() > 30) number = number.substring(0, 30);
458             if (number.equals("-1")) {
459                 // unknown numbers are stored as -1 in our database
460                 number = "";
461                 name = mContext.getString(R.string.unknown);
462             }
463 
464             // TODO(): Handle IRA commands. It's basically
465             // a 7 bit ASCII character set.
466             if (!name.equals("") && mCharacterSet.equals("GSM")) {
467                 byte[] nameByte = GsmAlphabet.stringToGsm8BitPacked(name);
468                 if (nameByte == null) {
469                     name = mContext.getString(R.string.unknown);
470                 } else {
471                     name = new String(nameByte);
472                 }
473             }
474 
475             result.addResponse("+CPBR: " + index + ",\"" + number + "\"," +
476                                regionType + ",\"" + name + "\"");
477             if (!pbr.cursor.moveToNext()) {
478                 break;
479             }
480         }
481         return result;
482     }
483 
484     // Check if the remote device has premission to read our phone book
485     // Return true if it has the permission
486     //        false if not known and we have sent our Intent to check
checkAccessPermission()487     private boolean checkAccessPermission() {
488         BluetoothDevice remoteDevice = mHandsfree.getHeadset().getRemoteDevice();
489 
490         boolean trust = remoteDevice.getTrustState();
491 
492         if (trust) {
493             return true;
494         }
495 
496         Intent intent = new Intent(BluetoothDevice.ACTION_CONNECTION_ACCESS_REQUEST);
497         intent.setClassName(ACCESS_AUTHORITY_PACKAGE, ACCESS_AUTHORITY_CLASS);
498         intent.putExtra(BluetoothDevice.EXTRA_ACCESS_REQUEST_TYPE,
499                         BluetoothDevice.REQUEST_TYPE_PHONEBOOK_ACCESS);
500         intent.putExtra(BluetoothDevice.EXTRA_DEVICE, remoteDevice);
501         // Leave EXTRA_PACKAGE_NAME and EXTRA_CLASS_NAME field empty
502         // BluetoothHandsfree's broadcast receiver is anonymous, cannot be targeted
503         mContext.sendBroadcast(intent, BLUETOOTH_ADMIN_PERM);
504 
505         return false;
506     }
507 
getPhoneType(int type)508     private static String getPhoneType(int type) {
509         switch (type) {
510             case Phone.TYPE_HOME:
511                 return "H";
512             case Phone.TYPE_MOBILE:
513                 return "M";
514             case Phone.TYPE_WORK:
515                 return "W";
516             case Phone.TYPE_FAX_HOME:
517             case Phone.TYPE_FAX_WORK:
518                 return "F";
519             case Phone.TYPE_OTHER:
520             case Phone.TYPE_CUSTOM:
521             default:
522                 return "O";
523         }
524     }
525 
log(String msg)526     private static void log(String msg) {
527         Log.d(TAG, msg);
528     }
529 }
530