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