Home | History | Annotate | Download | only in phone
      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 
     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 */
     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 
    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      */
    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 
    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 
    394     synchronized void resetAtState() {
    395         mCharacterSet = "UTF-8";
    396     }
    397 
    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 
    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 
    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 
    438     private static void log(String msg) {
    439         Log.d(TAG, msg);
    440     }
    441 }
    442