AtPhonebook.java revision 6c91bc0a163cc7600c40d7fb979777fd911d1ef1
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 17package com.android.bluetooth.hfp; 18 19import com.android.internal.telephony.GsmAlphabet; 20 21import android.bluetooth.BluetoothDevice; 22import android.content.Context; 23import android.content.Intent; 24import android.database.Cursor; 25import android.net.Uri; 26import android.provider.CallLog.Calls; 27import android.provider.ContactsContract.CommonDataKinds.Phone; 28import android.provider.ContactsContract.PhoneLookup; 29import android.telephony.PhoneNumberUtils; 30import android.util.Log; 31 32import java.util.HashMap; 33 34/** 35 * Helper for managing phonebook presentation over AT commands 36 * @hide 37 */ 38public class AtPhonebook { 39 private static final String TAG = "BluetoothAtPhonebook"; 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 Phone._ID, Phone.DISPLAY_NAME, Phone.NUMBER, Phone.TYPE 55 }; 56 57 /** Android supports as many phonebook entries as the flash can hold, but 58 * BT periphals don't. Limit the number we'll report. */ 59 private static final int MAX_PHONEBOOK_SIZE = 16384; 60 61 private static final String OUTGOING_CALL_WHERE = Calls.TYPE + "=" + Calls.OUTGOING_TYPE; 62 private static final String INCOMING_CALL_WHERE = Calls.TYPE + "=" + Calls.INCOMING_TYPE; 63 private static final String MISSED_CALL_WHERE = Calls.TYPE + "=" + Calls.MISSED_TYPE; 64 private static final String VISIBLE_PHONEBOOK_WHERE = Phone.IN_VISIBLE_GROUP + "=1"; 65 66 private class PhonebookResult { 67 public Cursor cursor; // result set of last query 68 public int numberColumn; 69 public int typeColumn; 70 public int nameColumn; 71 }; 72 73 private final Context mContext; 74 75 private String mCurrentPhonebook; 76 private String mCharacterSet = "UTF-8"; 77 78 private int mCpbrIndex1, mCpbrIndex2; 79 private boolean mCheckingAccessPermission; 80 81 // package and class name to which we send intent to check phone book access permission 82 private static final String ACCESS_AUTHORITY_PACKAGE = "com.android.settings"; 83 private static final String ACCESS_AUTHORITY_CLASS = 84 "com.android.settings.bluetooth.BluetoothPermissionRequest"; 85 private static final String BLUETOOTH_ADMIN_PERM = android.Manifest.permission.BLUETOOTH_ADMIN; 86 87 private final HashMap<String, PhonebookResult> mPhonebooks = 88 new HashMap<String, PhonebookResult>(4); 89 90 public AtPhonebook(Context context) { 91 mContext = context; 92 mPhonebooks.put("DC", new PhonebookResult()); // dialled calls 93 mPhonebooks.put("RC", new PhonebookResult()); // received calls 94 mPhonebooks.put("MC", new PhonebookResult()); // missed calls 95 mPhonebooks.put("ME", new PhonebookResult()); // mobile phonebook 96 97 mCurrentPhonebook = "ME"; // default to mobile phonebook 98 99 mCpbrIndex1 = mCpbrIndex2 = -1; 100 mCheckingAccessPermission = false; 101 } 102 103 /** Returns the last dialled number, or null if no numbers have been called */ 104 public String getLastDialledNumber() { 105 String[] projection = {Calls.NUMBER}; 106 Cursor cursor = mContext.getContentResolver().query(Calls.CONTENT_URI, projection, 107 Calls.TYPE + "=" + Calls.OUTGOING_TYPE, null, Calls.DEFAULT_SORT_ORDER + 108 " LIMIT 1"); 109 if (cursor == null) return null; 110 111 if (cursor.getCount() < 1) { 112 cursor.close(); 113 return null; 114 } 115 cursor.moveToNext(); 116 int column = cursor.getColumnIndexOrThrow(Calls.NUMBER); 117 String number = cursor.getString(column); 118 cursor.close(); 119 return number; 120 } 121 122 /* package */ void handleAccessPermissionResult(Intent intent) { 123 if (!mCheckingAccessPermission) { 124 return; 125 } 126 127 //HeadsetBase headset = mHandsfree.getHeadset(); 128 // ASSERT: (headset != null) && headSet.isConnected() 129 // REASON: mCheckingAccessPermission is true, otherwise resetAtState 130 // has set mCheckingAccessPermission to false 131 132 if (intent.getAction().equals(BluetoothDevice.ACTION_CONNECTION_ACCESS_REPLY)) { 133 134 if (intent.getIntExtra(BluetoothDevice.EXTRA_CONNECTION_ACCESS_RESULT, 135 BluetoothDevice.CONNECTION_ACCESS_NO) == 136 BluetoothDevice.CONNECTION_ACCESS_YES) { 137 // BluetoothDevice remoteDevice = headset.getRemoteDevice(); 138 // TODO(BT) when we do CPBR, fix this NullPointerException 139 BluetoothDevice remoteDevice = null; 140 if (intent.getBooleanExtra(BluetoothDevice.EXTRA_ALWAYS_ALLOWED, false)) { 141 remoteDevice.setTrust(true); 142 } 143 144 // AtCommandResult cpbrResult = processCpbrCommand(); 145 // headset.sendURC(cpbrResult.toString()); 146 } else { 147 // headset.sendURC("ERROR"); 148 } 149 } 150 mCpbrIndex1 = mCpbrIndex2 = -1; 151 mCheckingAccessPermission = false; 152 } 153 154 /** Get the most recent result for the given phone book, 155 * with the cursor ready to go. 156 * If force then re-query that phonebook 157 * Returns null if the cursor is not ready 158 */ 159 private synchronized PhonebookResult getPhonebookResult(String pb, boolean force) { 160 if (pb == null) { 161 return null; 162 } 163 PhonebookResult pbr = mPhonebooks.get(pb); 164 if (pbr == null) { 165 pbr = new PhonebookResult(); 166 } 167 if (force || pbr.cursor == null) { 168 if (!queryPhonebook(pb, pbr)) { 169 return null; 170 } 171 } 172 173 return pbr; 174 } 175 176 private synchronized boolean queryPhonebook(String pb, PhonebookResult pbr) { 177 String where; 178 boolean ancillaryPhonebook = true; 179 180 if (pb.equals("ME")) { 181 ancillaryPhonebook = false; 182 where = VISIBLE_PHONEBOOK_WHERE; 183 } else if (pb.equals("DC")) { 184 where = OUTGOING_CALL_WHERE; 185 } else if (pb.equals("RC")) { 186 where = INCOMING_CALL_WHERE; 187 } else if (pb.equals("MC")) { 188 where = MISSED_CALL_WHERE; 189 } else { 190 return false; 191 } 192 193 if (pbr.cursor != null) { 194 pbr.cursor.close(); 195 pbr.cursor = null; 196 } 197 198 if (ancillaryPhonebook) { 199 pbr.cursor = mContext.getContentResolver().query( 200 Calls.CONTENT_URI, CALLS_PROJECTION, where, null, 201 Calls.DEFAULT_SORT_ORDER + " LIMIT " + MAX_PHONEBOOK_SIZE); 202 if (pbr.cursor == null) return false; 203 204 pbr.numberColumn = pbr.cursor.getColumnIndexOrThrow(Calls.NUMBER); 205 pbr.typeColumn = -1; 206 pbr.nameColumn = -1; 207 } else { 208 pbr.cursor = mContext.getContentResolver().query(Phone.CONTENT_URI, PHONES_PROJECTION, 209 where, null, Phone.NUMBER + " LIMIT " + MAX_PHONEBOOK_SIZE); 210 if (pbr.cursor == null) return false; 211 212 pbr.numberColumn = pbr.cursor.getColumnIndex(Phone.NUMBER); 213 pbr.typeColumn = pbr.cursor.getColumnIndex(Phone.TYPE); 214 pbr.nameColumn = pbr.cursor.getColumnIndex(Phone.DISPLAY_NAME); 215 } 216 Log.i(TAG, "Refreshed phonebook " + pb + " with " + pbr.cursor.getCount() + " results"); 217 return true; 218 } 219 220 synchronized void resetAtState() { 221 mCharacterSet = "UTF-8"; 222 mCpbrIndex1 = mCpbrIndex2 = -1; 223 mCheckingAccessPermission = false; 224 } 225 226 private synchronized int getMaxPhoneBookSize(int currSize) { 227 // some car kits ignore the current size and request max phone book 228 // size entries. Thus, it takes a long time to transfer all the 229 // entries. Use a heuristic to calculate the max phone book size 230 // considering future expansion. 231 // maxSize = currSize + currSize / 2 rounded up to nearest power of 2 232 // If currSize < 100, use 100 as the currSize 233 234 int maxSize = (currSize < 100) ? 100 : currSize; 235 maxSize += maxSize / 2; 236 return roundUpToPowerOfTwo(maxSize); 237 } 238 239 private int roundUpToPowerOfTwo(int x) { 240 x |= x >> 1; 241 x |= x >> 2; 242 x |= x >> 4; 243 x |= x >> 8; 244 x |= x >> 16; 245 return x + 1; 246 } 247 248 // process CPBR command after permission check 249 private String processCpbrCommand() 250 { 251 // Shortcut SM phonebook 252 if ("SM".equals(mCurrentPhonebook)) { 253 // return new AtCommandResult(AtCommandResult.OK); 254 } 255 256 // Check phonebook 257 PhonebookResult pbr = getPhonebookResult(mCurrentPhonebook, false); 258 if (pbr == null) { 259 // return mHandsfree.reportCmeError(BluetoothCmeError.OPERATION_NOT_ALLOWED); 260 return null; 261 } 262 263 // More sanity checks 264 // Send OK instead of ERROR if these checks fail. 265 // When we send error, certain kits like BMW disconnect the 266 // Handsfree connection. 267 if (pbr.cursor.getCount() == 0 || mCpbrIndex1 <= 0 || mCpbrIndex2 < mCpbrIndex1 || 268 mCpbrIndex2 > pbr.cursor.getCount() || mCpbrIndex1 > pbr.cursor.getCount()) { 269 // return new AtCommandResult(AtCommandResult.OK); 270 return null; 271 } 272 273 // Process 274 //AtCommandResult result = new AtCommandResult(AtCommandResult.OK); 275 int errorDetected = -1; // no error 276 pbr.cursor.moveToPosition(mCpbrIndex1 - 1); 277 for (int index = mCpbrIndex1; index <= mCpbrIndex2; index++) { 278 String number = pbr.cursor.getString(pbr.numberColumn); 279 String name = null; 280 int type = -1; 281 if (pbr.nameColumn == -1) { 282 // try caller id lookup 283 // TODO: This code is horribly inefficient. I saw it 284 // take 7 seconds to process 100 missed calls. 285 Cursor c = mContext.getContentResolver(). 286 query(Uri.withAppendedPath(PhoneLookup.CONTENT_FILTER_URI, number), 287 new String[] {PhoneLookup.DISPLAY_NAME, PhoneLookup.TYPE}, 288 null, null, null); 289 if (c != null) { 290 if (c.moveToFirst()) { 291 name = c.getString(0); 292 type = c.getInt(1); 293 } 294 c.close(); 295 } 296 if (DBG && name == null) log("Caller ID lookup failed for " + number); 297 298 } else { 299 name = pbr.cursor.getString(pbr.nameColumn); 300 } 301 if (name == null) name = ""; 302 name = name.trim(); 303 if (name.length() > 28) name = name.substring(0, 28); 304 305 if (pbr.typeColumn != -1) { 306 type = pbr.cursor.getInt(pbr.typeColumn); 307 name = name + "/" + getPhoneType(type); 308 } 309 310 if (number == null) number = ""; 311 int regionType = PhoneNumberUtils.toaFromString(number); 312 313 number = number.trim(); 314 number = PhoneNumberUtils.stripSeparators(number); 315 if (number.length() > 30) number = number.substring(0, 30); 316 if (number.equals("-1")) { 317 // unknown numbers are stored as -1 in our database 318 number = ""; 319 // name = mContext.getString(R.string.unknown); 320 } 321 322 // TODO(): Handle IRA commands. It's basically 323 // a 7 bit ASCII character set. 324 if (!name.equals("") && mCharacterSet.equals("GSM")) { 325 byte[] nameByte = GsmAlphabet.stringToGsm8BitPacked(name); 326 if (nameByte == null) { 327 // name = mContext.getString(R.string.unknown); 328 } else { 329 name = new String(nameByte); 330 } 331 } 332 333 // result.addResponse("+CPBR: " + index + ",\"" + number + "\"," + 334 // regionType + ",\"" + name + "\""); 335 if (!pbr.cursor.moveToNext()) { 336 break; 337 } 338 } 339 // return result; 340 return null; 341 } 342 343 private static String getPhoneType(int type) { 344 switch (type) { 345 case Phone.TYPE_HOME: 346 return "H"; 347 case Phone.TYPE_MOBILE: 348 return "M"; 349 case Phone.TYPE_WORK: 350 return "W"; 351 case Phone.TYPE_FAX_HOME: 352 case Phone.TYPE_FAX_WORK: 353 return "F"; 354 case Phone.TYPE_OTHER: 355 case Phone.TYPE_CUSTOM: 356 default: 357 return "O"; 358 } 359 } 360 361 private static void log(String msg) { 362 Log.d(TAG, msg); 363 } 364} 365