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