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.bluetooth.hfp; 18 19 import com.android.bluetooth.R; 20 21 import com.android.internal.telephony.GsmAlphabet; 22 23 import android.bluetooth.BluetoothDevice; 24 import android.content.ContentResolver; 25 import android.content.Context; 26 import android.content.Intent; 27 import android.database.Cursor; 28 import android.net.Uri; 29 import android.provider.CallLog.Calls; 30 import android.provider.ContactsContract.CommonDataKinds.Phone; 31 import android.provider.ContactsContract.PhoneLookup; 32 import android.telephony.PhoneNumberUtils; 33 import android.util.Log; 34 35 import java.util.HashMap; 36 37 /** 38 * Helper for managing phonebook presentation over AT commands 39 * @hide 40 */ 41 public class AtPhonebook { 42 private static final String TAG = "BluetoothAtPhonebook"; 43 private static final boolean DBG = false; 44 45 /** The projection to use when querying the call log database in response 46 * to AT+CPBR for the MC, RC, and DC phone books (missed, received, and 47 * dialed calls respectively) 48 */ 49 private static final String[] CALLS_PROJECTION = new String[] { 50 Calls._ID, Calls.NUMBER 51 }; 52 53 /** The projection to use when querying the contacts database in response 54 * to AT+CPBR for the ME phonebook (saved phone numbers). 55 */ 56 private static final String[] PHONES_PROJECTION = new String[] { 57 Phone._ID, Phone.DISPLAY_NAME, Phone.NUMBER, Phone.TYPE 58 }; 59 60 /** Android supports as many phonebook entries as the flash can hold, but 61 * BT periphals don't. Limit the number we'll report. */ 62 private static final int MAX_PHONEBOOK_SIZE = 16384; 63 64 private static final String OUTGOING_CALL_WHERE = Calls.TYPE + "=" + Calls.OUTGOING_TYPE; 65 private static final String INCOMING_CALL_WHERE = Calls.TYPE + "=" + Calls.INCOMING_TYPE; 66 private static final String MISSED_CALL_WHERE = Calls.TYPE + "=" + Calls.MISSED_TYPE; 67 private static final String VISIBLE_PHONEBOOK_WHERE = Phone.IN_VISIBLE_GROUP + "=1"; 68 69 private class PhonebookResult { 70 public Cursor cursor; // result set of last query 71 public int numberColumn; 72 public int typeColumn; 73 public int nameColumn; 74 }; 75 76 private Context mContext; 77 private ContentResolver mContentResolver; 78 private HeadsetStateMachine mStateMachine; 79 private String mCurrentPhonebook; 80 private String mCharacterSet = "UTF-8"; 81 82 private int mCpbrIndex1, mCpbrIndex2; 83 private boolean mCheckingAccessPermission; 84 85 // package and class name to which we send intent to check phone book access permission 86 private static final String ACCESS_AUTHORITY_PACKAGE = "com.android.settings"; 87 private static final String ACCESS_AUTHORITY_CLASS = 88 "com.android.settings.bluetooth.BluetoothPermissionRequest"; 89 private static final String BLUETOOTH_ADMIN_PERM = android.Manifest.permission.BLUETOOTH_ADMIN; 90 91 private final HashMap<String, PhonebookResult> mPhonebooks = 92 new HashMap<String, PhonebookResult>(4); 93 94 final int TYPE_UNKNOWN = -1; 95 final int TYPE_READ = 0; 96 final int TYPE_SET = 1; 97 final int TYPE_TEST = 2; 98 99 public AtPhonebook(Context context, HeadsetStateMachine headsetState) { 100 mContext = context; 101 mContentResolver = context.getContentResolver(); 102 mStateMachine = headsetState; 103 mPhonebooks.put("DC", new PhonebookResult()); // dialled calls 104 mPhonebooks.put("RC", new PhonebookResult()); // received calls 105 mPhonebooks.put("MC", new PhonebookResult()); // missed calls 106 mPhonebooks.put("ME", new PhonebookResult()); // mobile phonebook 107 108 mCurrentPhonebook = "ME"; // default to mobile phonebook 109 110 mCpbrIndex1 = mCpbrIndex2 = -1; 111 mCheckingAccessPermission = false; 112 } 113 114 public void cleanup() { 115 mPhonebooks.clear(); 116 } 117 118 /** Returns the last dialled number, or null if no numbers have been called */ 119 public String getLastDialledNumber() { 120 String[] projection = {Calls.NUMBER}; 121 Cursor cursor = mContentResolver.query(Calls.CONTENT_URI, projection, 122 Calls.TYPE + "=" + Calls.OUTGOING_TYPE, null, Calls.DEFAULT_SORT_ORDER + 123 " LIMIT 1"); 124 if (cursor == null) return null; 125 126 if (cursor.getCount() < 1) { 127 cursor.close(); 128 return null; 129 } 130 cursor.moveToNext(); 131 int column = cursor.getColumnIndexOrThrow(Calls.NUMBER); 132 String number = cursor.getString(column); 133 cursor.close(); 134 return number; 135 } 136 137 public boolean getCheckingAccessPermission() { 138 return mCheckingAccessPermission; 139 } 140 141 public void setCheckingAccessPermission(boolean checkAccessPermission) { 142 mCheckingAccessPermission = checkAccessPermission; 143 } 144 145 public void setCpbrIndex(int cpbrIndex) { 146 mCpbrIndex1 = mCpbrIndex2 = cpbrIndex; 147 } 148 149 public void handleCscsCommand(String atString, int type) 150 { 151 log("handleCscsCommand - atString = " +atString); 152 // Select Character Set 153 int atCommandResult = HeadsetHalConstants.AT_RESPONSE_ERROR; 154 int atCommandErrorCode = -1; 155 String atCommandResponse = null; 156 switch (type) { 157 case TYPE_READ: // Read 158 log("handleCscsCommand - Read Command"); 159 atCommandResponse = "+CSCS: \"" + mCharacterSet + "\""; 160 atCommandResult = HeadsetHalConstants.AT_RESPONSE_OK; 161 break; 162 case TYPE_TEST: // Test 163 log("handleCscsCommand - Test Command"); 164 atCommandResponse = ( "+CSCS: (\"UTF-8\",\"IRA\",\"GSM\")"); 165 atCommandResult = HeadsetHalConstants.AT_RESPONSE_OK; 166 break; 167 case TYPE_SET: // Set 168 log("handleCscsCommand - Set Command"); 169 String[] args = atString.split("="); 170 if (args.length < 2 || !(args[1] instanceof String)) { 171 mStateMachine.atResponseCodeNative(atCommandResult, atCommandErrorCode); 172 break; 173 } 174 String characterSet = ((atString.split("="))[1]); 175 characterSet = characterSet.replace("\"", ""); 176 if (characterSet.equals("GSM") || characterSet.equals("IRA") || 177 characterSet.equals("UTF-8") || characterSet.equals("UTF8")) { 178 mCharacterSet = characterSet; 179 atCommandResult = HeadsetHalConstants.AT_RESPONSE_OK; 180 } else { 181 atCommandErrorCode = BluetoothCmeError.OPERATION_NOT_SUPPORTED; 182 } 183 break; 184 case TYPE_UNKNOWN: 185 default: 186 log("handleCscsCommand - Invalid chars"); 187 atCommandErrorCode = BluetoothCmeError.TEXT_HAS_INVALID_CHARS; 188 } 189 if (atCommandResponse != null) 190 mStateMachine.atResponseStringNative(atCommandResponse); 191 mStateMachine.atResponseCodeNative(atCommandResult, atCommandErrorCode); 192 } 193 194 public void handleCpbsCommand(String atString, int type) { 195 // Select PhoneBook memory Storage 196 log("handleCpbsCommand - atString = " +atString); 197 int atCommandResult = HeadsetHalConstants.AT_RESPONSE_ERROR; 198 int atCommandErrorCode = -1; 199 String atCommandResponse = null; 200 switch (type) { 201 case TYPE_READ: // Read 202 log("handleCpbsCommand - read command"); 203 // Return current size and max size 204 if ("SM".equals(mCurrentPhonebook)) { 205 atCommandResponse = "+CPBS: \"SM\",0," + getMaxPhoneBookSize(0); 206 atCommandResult = HeadsetHalConstants.AT_RESPONSE_OK; 207 if (atCommandResponse != null) 208 mStateMachine.atResponseStringNative(atCommandResponse); 209 mStateMachine.atResponseCodeNative(atCommandResult, atCommandErrorCode); 210 break; 211 } 212 PhonebookResult pbr = getPhonebookResult(mCurrentPhonebook, true); 213 if (pbr == null) { 214 atCommandErrorCode = BluetoothCmeError.OPERATION_NOT_SUPPORTED; 215 mStateMachine.atResponseCodeNative(atCommandResult, atCommandErrorCode); 216 break; 217 } 218 int size = pbr.cursor.getCount(); 219 atCommandResponse = "+CPBS: \"" + mCurrentPhonebook + "\"," + size + "," + getMaxPhoneBookSize(size); 220 pbr.cursor.close(); 221 pbr.cursor = null; 222 atCommandResult = HeadsetHalConstants.AT_RESPONSE_OK; 223 break; 224 case TYPE_TEST: // Test 225 log("handleCpbsCommand - test command"); 226 atCommandResponse = ("+CPBS: (\"ME\",\"SM\",\"DC\",\"RC\",\"MC\")"); 227 atCommandResult = HeadsetHalConstants.AT_RESPONSE_OK; 228 break; 229 case TYPE_SET: // Set 230 log("handleCpbsCommand - set command"); 231 String[] args = atString.split("="); 232 // Select phonebook memory 233 if (args.length < 2 || !(args[1] instanceof String)) { 234 mStateMachine.atResponseCodeNative(atCommandResult, atCommandErrorCode); 235 break; 236 } 237 String pb = ((String)args[1]).trim(); 238 while (pb.endsWith("\"")) pb = pb.substring(0, pb.length() - 1); 239 while (pb.startsWith("\"")) pb = pb.substring(1, pb.length()); 240 if (getPhonebookResult(pb, false) == null && !"SM".equals(pb)) { 241 if (DBG) log("Dont know phonebook: '" + pb + "'"); 242 atCommandErrorCode = BluetoothCmeError.OPERATION_NOT_ALLOWED; 243 mStateMachine.atResponseCodeNative(atCommandResult, atCommandErrorCode); 244 break; 245 } 246 mCurrentPhonebook = pb; 247 atCommandResult = HeadsetHalConstants.AT_RESPONSE_OK; 248 break; 249 case TYPE_UNKNOWN: 250 default: 251 log("handleCpbsCommand - invalid chars"); 252 atCommandErrorCode = BluetoothCmeError.TEXT_HAS_INVALID_CHARS; 253 } 254 if (atCommandResponse != null) 255 mStateMachine.atResponseStringNative(atCommandResponse); 256 mStateMachine.atResponseCodeNative(atCommandResult, atCommandErrorCode); 257 } 258 259 public void handleCpbrCommand(String atString, int type, BluetoothDevice remoteDevice) { 260 log("handleCpbrCommand - atString = " +atString); 261 int atCommandResult = HeadsetHalConstants.AT_RESPONSE_ERROR; 262 int atCommandErrorCode = -1; 263 String atCommandResponse = null; 264 switch (type) { 265 case TYPE_TEST: // Test 266 /* Ideally we should return the maximum range of valid index's 267 * for the selected phone book, but this causes problems for the 268 * Parrot CK3300. So instead send just the range of currently 269 * valid index's. 270 */ 271 log("handleCpbrCommand - test command"); 272 int size; 273 if ("SM".equals(mCurrentPhonebook)) { 274 size = 0; 275 } else { 276 PhonebookResult pbr = getPhonebookResult(mCurrentPhonebook, true); //false); 277 if (pbr == null) { 278 atCommandErrorCode = BluetoothCmeError.OPERATION_NOT_ALLOWED; 279 mStateMachine.atResponseCodeNative(atCommandResult, atCommandErrorCode); 280 break; 281 } 282 size = pbr.cursor.getCount(); 283 log("handleCpbrCommand - size = "+size); 284 pbr.cursor.close(); 285 pbr.cursor = null; 286 } 287 if (size == 0) { 288 /* Sending "+CPBR: (1-0)" can confused some carkits, send "1-1" * instead */ 289 size = 1; 290 } 291 atCommandResponse = "+CPBR: (1-" + size + "),30,30"; 292 atCommandResult = HeadsetHalConstants.AT_RESPONSE_OK; 293 if (atCommandResponse != null) 294 mStateMachine.atResponseStringNative(atCommandResponse); 295 mStateMachine.atResponseCodeNative(atCommandResult, atCommandErrorCode); 296 break; 297 // Read PhoneBook Entries 298 case TYPE_READ: 299 case TYPE_SET: // Set & read 300 // Phone Book Read Request 301 // AT+CPBR=<index1>[,<index2>] 302 log("handleCpbrCommand - set/read command"); 303 if (mCpbrIndex1 != -1) { 304 /* handling a CPBR at the moment, reject this CPBR command */ 305 atCommandErrorCode = BluetoothCmeError.OPERATION_NOT_ALLOWED; 306 mStateMachine.atResponseCodeNative(atCommandResult, atCommandErrorCode); 307 break; 308 } 309 // Parse indexes 310 int index1; 311 int index2; 312 if ((atString.split("=")).length < 2) { 313 mStateMachine.atResponseCodeNative(atCommandResult, atCommandErrorCode); 314 break; 315 } 316 String atCommand = (atString.split("="))[1]; 317 String[] indices = atCommand.split(","); 318 for(int i = 0; i < indices.length; i++) 319 //replace AT command separator ';' from the index if any 320 indices[i] = indices[i].replace(';', ' ').trim(); 321 try { 322 index1 = Integer.parseInt(indices[0]); 323 if (indices.length == 1) 324 index2 = index1; 325 else 326 index2 = Integer.parseInt(indices[1]); 327 } 328 catch (Exception e) { 329 log("handleCpbrCommand - exception - invalid chars: " + e.toString()); 330 atCommandErrorCode = BluetoothCmeError.TEXT_HAS_INVALID_CHARS; 331 mStateMachine.atResponseCodeNative(atCommandResult, atCommandErrorCode); 332 break; 333 } 334 mCpbrIndex1 = index1; 335 mCpbrIndex2 = index2; 336 mCheckingAccessPermission = true; 337 338 if (checkAccessPermission(remoteDevice)) { 339 mCheckingAccessPermission = false; 340 atCommandResult = processCpbrCommand(); 341 mCpbrIndex1 = mCpbrIndex2 = -1; 342 mStateMachine.atResponseCodeNative(atCommandResult, atCommandErrorCode); 343 break; 344 } 345 // no reponse here, will continue the process in handleAccessPermissionResult 346 break; 347 case TYPE_UNKNOWN: 348 default: 349 log("handleCpbrCommand - invalid chars"); 350 atCommandErrorCode = BluetoothCmeError.TEXT_HAS_INVALID_CHARS; 351 mStateMachine.atResponseCodeNative(atCommandResult, atCommandErrorCode); 352 } 353 } 354 355 /** Get the most recent result for the given phone book, 356 * with the cursor ready to go. 357 * If force then re-query that phonebook 358 * Returns null if the cursor is not ready 359 */ 360 private synchronized PhonebookResult getPhonebookResult(String pb, boolean force) { 361 if (pb == null) { 362 return null; 363 } 364 PhonebookResult pbr = mPhonebooks.get(pb); 365 if (pbr == null) { 366 pbr = new PhonebookResult(); 367 } 368 if (force || pbr.cursor == null) { 369 if (!queryPhonebook(pb, pbr)) { 370 return null; 371 } 372 } 373 374 return pbr; 375 } 376 377 private synchronized boolean queryPhonebook(String pb, PhonebookResult pbr) { 378 String where; 379 boolean ancillaryPhonebook = true; 380 381 if (pb.equals("ME")) { 382 ancillaryPhonebook = false; 383 where = VISIBLE_PHONEBOOK_WHERE; 384 } else if (pb.equals("DC")) { 385 where = OUTGOING_CALL_WHERE; 386 } else if (pb.equals("RC")) { 387 where = INCOMING_CALL_WHERE; 388 } else if (pb.equals("MC")) { 389 where = MISSED_CALL_WHERE; 390 } else { 391 return false; 392 } 393 394 if (pbr.cursor != null) { 395 pbr.cursor.close(); 396 pbr.cursor = null; 397 } 398 399 if (ancillaryPhonebook) { 400 pbr.cursor = mContentResolver.query( 401 Calls.CONTENT_URI, CALLS_PROJECTION, where, null, 402 Calls.DEFAULT_SORT_ORDER + " LIMIT " + MAX_PHONEBOOK_SIZE); 403 if (pbr.cursor == null) return false; 404 405 pbr.numberColumn = pbr.cursor.getColumnIndexOrThrow(Calls.NUMBER); 406 pbr.typeColumn = -1; 407 pbr.nameColumn = -1; 408 } else { 409 pbr.cursor = mContentResolver.query(Phone.CONTENT_URI, PHONES_PROJECTION, 410 where, null, Phone.NUMBER + " LIMIT " + MAX_PHONEBOOK_SIZE); 411 if (pbr.cursor == null) return false; 412 413 pbr.numberColumn = pbr.cursor.getColumnIndex(Phone.NUMBER); 414 pbr.typeColumn = pbr.cursor.getColumnIndex(Phone.TYPE); 415 pbr.nameColumn = pbr.cursor.getColumnIndex(Phone.DISPLAY_NAME); 416 } 417 Log.i(TAG, "Refreshed phonebook " + pb + " with " + pbr.cursor.getCount() + " results"); 418 return true; 419 } 420 421 synchronized void resetAtState() { 422 mCharacterSet = "UTF-8"; 423 mCpbrIndex1 = mCpbrIndex2 = -1; 424 mCheckingAccessPermission = false; 425 } 426 427 private synchronized int getMaxPhoneBookSize(int currSize) { 428 // some car kits ignore the current size and request max phone book 429 // size entries. Thus, it takes a long time to transfer all the 430 // entries. Use a heuristic to calculate the max phone book size 431 // considering future expansion. 432 // maxSize = currSize + currSize / 2 rounded up to nearest power of 2 433 // If currSize < 100, use 100 as the currSize 434 435 int maxSize = (currSize < 100) ? 100 : currSize; 436 maxSize += maxSize / 2; 437 return roundUpToPowerOfTwo(maxSize); 438 } 439 440 private int roundUpToPowerOfTwo(int x) { 441 x |= x >> 1; 442 x |= x >> 2; 443 x |= x >> 4; 444 x |= x >> 8; 445 x |= x >> 16; 446 return x + 1; 447 } 448 449 // process CPBR command after permission check 450 /*package*/ int processCpbrCommand() 451 { 452 log("processCpbrCommand"); 453 int atCommandResult = HeadsetHalConstants.AT_RESPONSE_ERROR; 454 int atCommandErrorCode = -1; 455 String atCommandResponse = null; 456 StringBuilder response = new StringBuilder(); 457 String record; 458 459 // Shortcut SM phonebook 460 if ("SM".equals(mCurrentPhonebook)) { 461 atCommandResult = HeadsetHalConstants.AT_RESPONSE_OK; 462 return atCommandResult; 463 } 464 465 // Check phonebook 466 PhonebookResult pbr = getPhonebookResult(mCurrentPhonebook, true); //false); 467 if (pbr == null) { 468 atCommandErrorCode = BluetoothCmeError.OPERATION_NOT_ALLOWED; 469 return atCommandResult; 470 } 471 472 // More sanity checks 473 // Send OK instead of ERROR if these checks fail. 474 // When we send error, certain kits like BMW disconnect the 475 // Handsfree connection. 476 if (pbr.cursor.getCount() == 0 || mCpbrIndex1 <= 0 || mCpbrIndex2 < mCpbrIndex1 || 477 mCpbrIndex2 > pbr.cursor.getCount() || mCpbrIndex1 > pbr.cursor.getCount()) { 478 atCommandResult = HeadsetHalConstants.AT_RESPONSE_OK; 479 return atCommandResult; 480 } 481 482 // Process 483 atCommandResult = HeadsetHalConstants.AT_RESPONSE_OK; 484 int errorDetected = -1; // no error 485 pbr.cursor.moveToPosition(mCpbrIndex1 - 1); 486 log("mCpbrIndex1 = "+mCpbrIndex1+ " and mCpbrIndex2 = "+mCpbrIndex2); 487 for (int index = mCpbrIndex1; index <= mCpbrIndex2; index++) { 488 String number = pbr.cursor.getString(pbr.numberColumn); 489 String name = null; 490 int type = -1; 491 if (pbr.nameColumn == -1) { 492 // try caller id lookup 493 // TODO: This code is horribly inefficient. I saw it 494 // take 7 seconds to process 100 missed calls. 495 Cursor c = mContentResolver. 496 query(Uri.withAppendedPath(PhoneLookup.CONTENT_FILTER_URI, number), 497 new String[] {PhoneLookup.DISPLAY_NAME, PhoneLookup.TYPE}, 498 null, null, null); 499 if (c != null) { 500 if (c.moveToFirst()) { 501 name = c.getString(0); 502 type = c.getInt(1); 503 } 504 c.close(); 505 } 506 if (DBG && name == null) log("Caller ID lookup failed for " + number); 507 508 } else { 509 name = pbr.cursor.getString(pbr.nameColumn); 510 } 511 if (name == null) name = ""; 512 name = name.trim(); 513 if (name.length() > 28) name = name.substring(0, 28); 514 515 if (pbr.typeColumn != -1) { 516 type = pbr.cursor.getInt(pbr.typeColumn); 517 name = name + "/" + getPhoneType(type); 518 } 519 520 if (number == null) number = ""; 521 int regionType = PhoneNumberUtils.toaFromString(number); 522 523 number = number.trim(); 524 number = PhoneNumberUtils.stripSeparators(number); 525 if (number.length() > 30) number = number.substring(0, 30); 526 if (number.equals("-1")) { 527 // unknown numbers are stored as -1 in our database 528 number = ""; 529 name = mContext.getString(R.string.unknownNumber); 530 } 531 532 // TODO(): Handle IRA commands. It's basically 533 // a 7 bit ASCII character set. 534 if (!name.equals("") && mCharacterSet.equals("GSM")) { 535 byte[] nameByte = GsmAlphabet.stringToGsm8BitPacked(name); 536 if (nameByte == null) { 537 name = mContext.getString(R.string.unknownNumber); 538 } else { 539 name = new String(nameByte); 540 } 541 } 542 543 record = "+CPBR: " + index + ",\"" + number + "\"," + regionType + ",\"" + name + "\""; 544 record = record + "\r\n\r\n"; 545 atCommandResponse = record; 546 log("processCpbrCommand - atCommandResponse = "+atCommandResponse); 547 mStateMachine.atResponseStringNative(atCommandResponse); 548 if (!pbr.cursor.moveToNext()) { 549 break; 550 } 551 } 552 if(pbr != null && pbr.cursor != null) { 553 pbr.cursor.close(); 554 pbr.cursor = null; 555 } 556 return atCommandResult; 557 } 558 559 // Check if the remote device has premission to read our phone book 560 // Return true if it has the permission 561 // false if not known and we have sent our Intent to check 562 private boolean checkAccessPermission(BluetoothDevice remoteDevice) { 563 log("checkAccessPermission"); 564 boolean trust = remoteDevice.getTrustState(); 565 566 if (trust) { 567 return true; 568 } 569 570 log("checkAccessPermission - ACTION_CONNECTION_ACCESS_REQUEST"); 571 Intent intent = new Intent(BluetoothDevice.ACTION_CONNECTION_ACCESS_REQUEST); 572 intent.setClassName(ACCESS_AUTHORITY_PACKAGE, ACCESS_AUTHORITY_CLASS); 573 intent.putExtra(BluetoothDevice.EXTRA_ACCESS_REQUEST_TYPE, 574 BluetoothDevice.REQUEST_TYPE_PHONEBOOK_ACCESS); 575 intent.putExtra(BluetoothDevice.EXTRA_DEVICE, remoteDevice); 576 // Leave EXTRA_PACKAGE_NAME and EXTRA_CLASS_NAME field empty 577 // BluetoothHandsfree's broadcast receiver is anonymous, cannot be targeted 578 mContext.sendBroadcast(intent, BLUETOOTH_ADMIN_PERM); 579 return false; 580 } 581 582 private static String getPhoneType(int type) { 583 switch (type) { 584 case Phone.TYPE_HOME: 585 return "H"; 586 case Phone.TYPE_MOBILE: 587 return "M"; 588 case Phone.TYPE_WORK: 589 return "W"; 590 case Phone.TYPE_FAX_HOME: 591 case Phone.TYPE_FAX_WORK: 592 return "F"; 593 case Phone.TYPE_OTHER: 594 case Phone.TYPE_CUSTOM: 595 default: 596 return "O"; 597 } 598 } 599 600 private static void log(String msg) { 601 Log.d(TAG, msg); 602 } 603 } 604