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