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