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 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 
     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 */
    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 
    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 
    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      */
    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 
    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 
    364     synchronized void resetAtState() {
    365         mCharacterSet = "UTF-8";
    366         mCpbrIndex1 = mCpbrIndex2 = -1;
    367         mCheckingAccessPermission = false;
    368     }
    369 
    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 
    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
    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
    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 
    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 
    526     private static void log(String msg) {
    527         Log.d(TAG, msg);
    528     }
    529 }
    530