Advertise UTF-8 phonebook support.
[android/platform/packages/apps/Phone.git] / src / com / android / phone / BluetoothAtPhonebook.java
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.Contacts.Phones;
27 import android.provider.Contacts.PhonesColumns;
28 import android.telephony.PhoneNumberUtils;
29 import android.text.TextUtils;
30 import android.util.Log;
31
32 import java.util.HashMap;
33
34 /**
35  * Helper for managing phonebook presentation over AT commands
36  * @hide
37  */
38 public class BluetoothAtPhonebook {
39     private static final String TAG = "BtAtPhonebook";
40     private static final boolean DBG = false;
41     
42     /** The projection to use when querying the call log database in response
43      *  to AT+CPBR for the MC, RC, and DC phone books (missed, received, and
44      *   dialed calls respectively)
45      */
46     private static final String[] CALLS_PROJECTION = new String[] {
47         Calls._ID, Calls.NUMBER
48     };
49
50     /** The projection to use when querying the contacts database in response
51      *   to AT+CPBR for the ME phonebook (saved phone numbers).
52      */
53     private static final String[] PHONES_PROJECTION = new String[] {
54         Phones._ID, Phones.NAME, Phones.NUMBER, Phones.TYPE
55     };
56
57     /** The projection to use when querying the contacts database in response
58      *  to AT+CNUM for the ME phonebook (saved phone numbers).  We need only
59      *   the phone numbers here and the phone type.
60      */
61     private static final String[] PHONES_LITE_PROJECTION = new String[] {
62         Phones._ID, Phones.NUMBER, Phones.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     typeColumn;
77         public int     nameColumn;
78     };
79
80     private final Context mContext;
81     private final BluetoothHandsfree mHandsfree;
82
83     private String mCurrentPhonebook;
84
85     private final HashMap<String, PhonebookResult> mPhonebooks =
86             new HashMap<String, PhonebookResult>(4);
87
88     public BluetoothAtPhonebook(Context context, BluetoothHandsfree handsfree) {
89         mContext = context;
90         mHandsfree = handsfree;
91         mPhonebooks.put("DC", new PhonebookResult());  // dialled calls
92         mPhonebooks.put("RC", new PhonebookResult());  // received calls
93         mPhonebooks.put("MC", new PhonebookResult());  // missed calls
94         mPhonebooks.put("ME", new PhonebookResult());  // mobile phonebook
95
96         mCurrentPhonebook = "ME";  // default to mobile phonebook
97     }
98
99     /** Returns the last dialled number, or null if no numbers have been called */
100     public String getLastDialledNumber() {
101         String[] projection = {Calls.NUMBER};
102         Cursor cursor = mContext.getContentResolver().query(Calls.CONTENT_URI, projection,
103                 Calls.TYPE + "=" + Calls.OUTGOING_TYPE, null, Calls.DEFAULT_SORT_ORDER +
104                 " LIMIT 1");
105         if (cursor.getCount() < 1) {
106             cursor.close();
107             return null;
108         }
109         cursor.moveToNext();
110         int column = cursor.getColumnIndexOrThrow(Calls.NUMBER);
111         String number = cursor.getString(column);
112         cursor.close();
113         return number;
114     }
115     
116     public void register(AtParser parser) {
117         // Select Character Set
118         // Always send UTF-8, but pretend to support IRA and GSM for compatability
119         // TODO: Implement IRA and GSM encoding instead of faking it
120         parser.register("+CSCS", new AtCommandHandler() {
121             @Override
122             public AtCommandResult handleReadCommand() {
123                 return new AtCommandResult("+CSCS: \"UTF-8\"");
124             }
125             @Override
126             public AtCommandResult handleSetCommand(Object[] args) {
127                 if (args.length < 1) {
128                     return new AtCommandResult(AtCommandResult.ERROR);
129                 }
130                 if (((String)args[0]).equals("\"GSM\"") || ((String)args[0]).equals("\"IRA\"") ||
131                         ((String)args[0]).equals("\"UTF-8\"") ||
132                         ((String)args[0]).equals("\"UTF8\"") ) {
133                     return new AtCommandResult(AtCommandResult.OK);
134                 } else {
135                     return mHandsfree.reportCmeError(BluetoothCmeError.OPERATION_NOT_SUPPORTED);
136                 }
137             }
138             @Override
139             public AtCommandResult handleTestCommand() {
140                 return new AtCommandResult( "+CSCS: (\"UTF-8\",\"IRA\",\"GSM\")");
141             }
142         });
143
144         // Select PhoneBook memory Storage
145         parser.register("+CPBS", new AtCommandHandler() {
146             @Override
147             public AtCommandResult handleReadCommand() {
148                 // Return current size and max size
149                 if ("SM".equals(mCurrentPhonebook)) {
150                     return new AtCommandResult("+CPBS: \"SM\",0," + MAX_PHONEBOOK_SIZE);
151                 }
152                     
153                 PhonebookResult pbr = getPhonebookResult(mCurrentPhonebook, true);
154                 if (pbr == null) {
155                     return mHandsfree.reportCmeError(BluetoothCmeError.OPERATION_NOT_ALLOWED);
156                 }
157                 return new AtCommandResult("+CPBS: \"" + mCurrentPhonebook + "\"," +
158                         pbr.cursor.getCount() + "," + MAX_PHONEBOOK_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                 if (index1 <= 0 || index2 < index1 || index2 > pbr.cursor.getCount()) {
219                     return new AtCommandResult(AtCommandResult.ERROR);
220                 }
221
222                 // Process
223                 AtCommandResult result = new AtCommandResult(AtCommandResult.OK);
224                 int errorDetected = -1; // no error
225                 pbr.cursor.moveToPosition(index1 - 1);
226                 for (int index = index1; index <= index2; index++) {
227                     String number = pbr.cursor.getString(pbr.numberColumn);
228                     String name = null;
229                     int type = -1;
230                     if (pbr.nameColumn == -1) {
231                         // try caller id lookup
232                         // TODO: This code is horribly inefficient. I saw it
233                         // take 7 seconds to process 100 missed calls.
234                         Cursor c = mContext.getContentResolver().query(
235                                 Uri.withAppendedPath(Phones.CONTENT_FILTER_URL, number),
236                                 new String[] {Phones.NAME, Phones.TYPE}, null, null, null);
237                         if (c != null) {
238                             if (c.moveToFirst()) {
239                                 name = c.getString(0);
240                                 type = c.getInt(1);
241                             }
242                             c.close();
243                         }
244                         if (DBG && name == null) log("Caller ID lookup failed for " + number);
245
246                     } else {
247                         name = pbr.cursor.getString(pbr.nameColumn);
248                     }
249                     if (name == null) name = "";
250                     name = name.trim();
251                     if (name.length() > 28) name = name.substring(0, 28);
252
253                     if (pbr.typeColumn != -1) {
254                         type = pbr.cursor.getInt(pbr.typeColumn);
255                         name = name + "/" + getPhoneType(type);
256                     }
257
258                     int regionType = PhoneNumberUtils.toaFromString(number);
259
260                     number = number.trim();
261                     number = PhoneNumberUtils.stripSeparators(number);
262                     if (number.length() > 30) number = number.substring(0, 30);
263                     if (number.equals("-1")) {
264                         // unknown numbers are stored as -1 in our database
265                         number = "";
266                         name = "unknown";
267                     }
268
269                     result.addResponse("+CPBR: " + index + ",\"" + number + "\"," +
270                                        regionType + ",\"" + name + "\"");
271                     if (!pbr.cursor.moveToNext()) {
272                         break;
273                     }
274                 }
275                 return result;
276             }
277             @Override
278             public AtCommandResult handleTestCommand() {
279                 /* Ideally we should return the maximum range of valid index's
280                  * for the selected phone book, but this causes problems for the
281                  * Parrot CK3300. So instead send just the range of currently
282                  * valid index's.
283                  */
284                 int size;
285                 if ("SM".equals(mCurrentPhonebook)) {
286                     size = 0;
287                 } else {
288                     PhonebookResult pbr = getPhonebookResult(mCurrentPhonebook, false);
289                     if (pbr == null) {
290                         return mHandsfree.reportCmeError(BluetoothCmeError.OPERATION_NOT_ALLOWED);
291                     }
292                     size = pbr.cursor.getCount();
293                 }
294
295                 if (size == 0) {
296                     /* Sending "+CPBR: (1-0)" can confused some carkits, send "1-1"
297                      * instead */
298                     size = 1;
299                 }
300                 return new AtCommandResult("+CPBR: (1-" + size + "),30,30");
301             }
302         });
303     }
304
305     /** Get the most recent result for the given phone book,
306      *  with the cursor ready to go.
307      *  If force then re-query that phonebook
308      *  Returns null if the cursor is not ready
309      */
310     private synchronized PhonebookResult getPhonebookResult(String pb, boolean force) {
311         if (pb == null) {
312             return null;
313         }
314         PhonebookResult pbr = mPhonebooks.get(pb);
315         if (pbr == null) {
316             pbr = new PhonebookResult();
317         }
318         if (force || pbr.cursor == null) {
319             if (!queryPhonebook(pb, pbr)) {
320                 return null;
321             }
322         }
323
324         if (pbr.cursor == null) {
325             return null;
326         }
327
328         return pbr;
329     }
330
331     private synchronized boolean queryPhonebook(String pb, PhonebookResult pbr) {
332         String where;
333         boolean ancillaryPhonebook = true;
334
335         if (pb.equals("ME")) {
336             ancillaryPhonebook = false;
337             where = null;
338         } else if (pb.equals("DC")) {
339             where = OUTGOING_CALL_WHERE;
340         } else if (pb.equals("RC")) {
341             where = INCOMING_CALL_WHERE;
342         } else if (pb.equals("MC")) {
343             where = MISSED_CALL_WHERE;
344         } else {
345             return false;
346         }
347
348         if (pbr.cursor != null) {
349             pbr.cursor.close();
350             pbr.cursor = null;
351         }
352
353         if (ancillaryPhonebook) {
354             pbr.cursor = mContext.getContentResolver().query(
355                     Calls.CONTENT_URI, CALLS_PROJECTION, where, null,
356                     Calls.DEFAULT_SORT_ORDER + " LIMIT " + MAX_PHONEBOOK_SIZE);
357             pbr.numberColumn = pbr.cursor.getColumnIndexOrThrow(Calls.NUMBER);
358             pbr.typeColumn = -1;
359             pbr.nameColumn = -1;
360         } else {
361             pbr.cursor = mContext.getContentResolver().query(
362                     Phones.CONTENT_URI, PHONES_PROJECTION, where, null,
363                     Phones.DEFAULT_SORT_ORDER + " LIMIT " + MAX_PHONEBOOK_SIZE);
364             pbr.numberColumn = pbr.cursor.getColumnIndex(Phones.NUMBER);
365             pbr.typeColumn = pbr.cursor.getColumnIndex(Phones.TYPE);
366             pbr.nameColumn = pbr.cursor.getColumnIndex(Phones.NAME);
367         }
368         Log.i(TAG, "Refreshed phonebook " + pb + " with " + pbr.cursor.getCount() + " results");
369         return true;
370     }
371
372     private static String getPhoneType(int type) {
373         switch (type) {
374             case PhonesColumns.TYPE_HOME:
375                 return "H";
376             case PhonesColumns.TYPE_MOBILE:
377                 return "M";
378             case PhonesColumns.TYPE_WORK:
379                 return "W";
380             case PhonesColumns.TYPE_FAX_HOME:
381             case PhonesColumns.TYPE_FAX_WORK:
382                 return "F";
383             case PhonesColumns.TYPE_OTHER:
384             case PhonesColumns.TYPE_CUSTOM:
385             default:
386                 return "O";
387         }
388     }
389
390     private static void log(String msg) {
391         Log.d(TAG, msg);
392     }
393 }