BluetoothPbapVcardManager.java revision ce8d51a3a43d113a4a6bad30d595c2a81d0f623c
1/* 2 * Copyright (c) 2008-2009, Motorola, Inc. 3 * 4 * All rights reserved. 5 * 6 * Redistribution and use in source and binary forms, with or without 7 * modification, are permitted provided that the following conditions are met: 8 * 9 * - Redistributions of source code must retain the above copyright notice, 10 * this list of conditions and the following disclaimer. 11 * 12 * - Redistributions in binary form must reproduce the above copyright notice, 13 * this list of conditions and the following disclaimer in the documentation 14 * and/or other materials provided with the distribution. 15 * 16 * - Neither the name of the Motorola, Inc. nor the names of its contributors 17 * may be used to endorse or promote products derived from this software 18 * without specific prior written permission. 19 * 20 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 23 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE 24 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 25 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 26 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 27 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 28 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 29 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 30 * POSSIBILITY OF SUCH DAMAGE. 31 */ 32 33package com.android.bluetooth.pbap; 34 35import android.content.ContentResolver; 36import android.content.Context; 37import android.database.Cursor; 38import android.net.Uri; 39import android.provider.CallLog; 40import android.provider.CallLog.Calls; 41import android.provider.ContactsContract.CommonDataKinds; 42import android.provider.ContactsContract.Contacts; 43import android.provider.ContactsContract.Data; 44import android.provider.ContactsContract.CommonDataKinds.Phone; 45import android.provider.ContactsContract.PhoneLookup; 46import android.text.TextUtils; 47import android.util.Log; 48 49import com.android.bluetooth.R; 50import com.android.vcard.VCardComposer; 51import com.android.vcard.VCardConfig; 52import com.android.internal.telephony.CallerInfo; 53 54import java.io.IOException; 55import java.io.OutputStream; 56import java.util.ArrayList; 57 58import javax.obex.ServerOperation; 59import javax.obex.Operation; 60import javax.obex.ResponseCodes; 61 62public class BluetoothPbapVcardManager { 63 private static final String TAG = "BluetoothPbapVcardManager"; 64 65 private static final boolean V = BluetoothPbapService.VERBOSE; 66 67 private ContentResolver mResolver; 68 69 private Context mContext; 70 71 static final String[] PHONES_PROJECTION = new String[] { 72 Data._ID, // 0 73 CommonDataKinds.Phone.TYPE, // 1 74 CommonDataKinds.Phone.LABEL, // 2 75 CommonDataKinds.Phone.NUMBER, // 3 76 Contacts.DISPLAY_NAME, // 4 77 }; 78 79 private static final int PHONE_NUMBER_COLUMN_INDEX = 3; 80 81 static final String SORT_ORDER_PHONE_NUMBER = CommonDataKinds.Phone.NUMBER + " ASC"; 82 83 static final String[] CONTACTS_PROJECTION = new String[] { 84 Contacts._ID, // 0 85 Contacts.DISPLAY_NAME, // 1 86 }; 87 88 static final int CONTACTS_ID_COLUMN_INDEX = 0; 89 90 static final int CONTACTS_NAME_COLUMN_INDEX = 1; 91 92 // call histories use dynamic handles, and handles should order by date; the 93 // most recently one should be the first handle. In table "calls", _id and 94 // date are consistent in ordering, to implement simply, we sort by _id 95 // here. 96 static final String CALLLOG_SORT_ORDER = Calls._ID + " DESC"; 97 98 private static final String CLAUSE_ONLY_VISIBLE = Contacts.IN_VISIBLE_GROUP + "=1"; 99 100 public BluetoothPbapVcardManager(final Context context) { 101 mContext = context; 102 mResolver = mContext.getContentResolver(); 103 } 104 105 public final String getOwnerPhoneNumberVcard(final boolean vcardType21) { 106 BluetoothPbapCallLogComposer composer = new BluetoothPbapCallLogComposer(mContext); 107 String name = BluetoothPbapService.getLocalPhoneName(); 108 String number = BluetoothPbapService.getLocalPhoneNum(); 109 String vcard = composer.composeVCardForPhoneOwnNumber(Phone.TYPE_MOBILE, name, number, 110 vcardType21); 111 return vcard; 112 } 113 114 public final int getPhonebookSize(final int type) { 115 int size; 116 switch (type) { 117 case BluetoothPbapObexServer.ContentType.PHONEBOOK: 118 size = getContactsSize(); 119 break; 120 default: 121 size = getCallHistorySize(type); 122 break; 123 } 124 if (V) Log.v(TAG, "getPhonebookSzie size = " + size + " type = " + type); 125 return size; 126 } 127 128 public final int getContactsSize() { 129 final Uri myUri = Contacts.CONTENT_URI; 130 int size = 0; 131 Cursor contactCursor = null; 132 try { 133 contactCursor = mResolver.query(myUri, null, CLAUSE_ONLY_VISIBLE, null, null); 134 if (contactCursor != null) { 135 size = contactCursor.getCount() + 1; // always has the 0.vcf 136 } 137 } finally { 138 if (contactCursor != null) { 139 contactCursor.close(); 140 } 141 } 142 return size; 143 } 144 145 public final int getCallHistorySize(final int type) { 146 final Uri myUri = CallLog.Calls.CONTENT_URI; 147 String selection = BluetoothPbapObexServer.createSelectionPara(type); 148 int size = 0; 149 Cursor callCursor = null; 150 try { 151 callCursor = mResolver.query(myUri, null, selection, null, 152 CallLog.Calls.DEFAULT_SORT_ORDER); 153 if (callCursor != null) { 154 size = callCursor.getCount(); 155 } 156 } finally { 157 if (callCursor != null) { 158 callCursor.close(); 159 } 160 } 161 return size; 162 } 163 164 public final ArrayList<String> loadCallHistoryList(final int type) { 165 final Uri myUri = CallLog.Calls.CONTENT_URI; 166 String selection = BluetoothPbapObexServer.createSelectionPara(type); 167 String[] projection = new String[] { 168 Calls.NUMBER, Calls.CACHED_NAME 169 }; 170 final int CALLS_NUMBER_COLUMN_INDEX = 0; 171 final int CALLS_NAME_COLUMN_INDEX = 1; 172 173 Cursor callCursor = null; 174 ArrayList<String> list = new ArrayList<String>(); 175 try { 176 callCursor = mResolver.query(myUri, projection, selection, null, 177 CALLLOG_SORT_ORDER); 178 if (callCursor != null) { 179 for (callCursor.moveToFirst(); !callCursor.isAfterLast(); 180 callCursor.moveToNext()) { 181 String name = callCursor.getString(CALLS_NAME_COLUMN_INDEX); 182 if (TextUtils.isEmpty(name)) { 183 // name not found, use number instead 184 name = callCursor.getString(CALLS_NUMBER_COLUMN_INDEX); 185 if (CallerInfo.UNKNOWN_NUMBER.equals(name) || 186 CallerInfo.PRIVATE_NUMBER.equals(name) || 187 CallerInfo.PAYPHONE_NUMBER.equals(name)) { 188 name = mContext.getString(R.string.unknownNumber); 189 } 190 } 191 list.add(name); 192 } 193 } 194 } finally { 195 if (callCursor != null) { 196 callCursor.close(); 197 } 198 } 199 return list; 200 } 201 202 public final ArrayList<String> getPhonebookNameList(final int orderByWhat) { 203 ArrayList<String> nameList = new ArrayList<String>(); 204 nameList.add(BluetoothPbapService.getLocalPhoneName()); 205 206 final Uri myUri = Contacts.CONTENT_URI; 207 Cursor contactCursor = null; 208 try { 209 if (orderByWhat == BluetoothPbapObexServer.ORDER_BY_INDEXED) { 210 if (V) Log.v(TAG, "getPhonebookNameList, order by index"); 211 contactCursor = mResolver.query(myUri, CONTACTS_PROJECTION, CLAUSE_ONLY_VISIBLE, 212 null, Contacts._ID); 213 } else if (orderByWhat == BluetoothPbapObexServer.ORDER_BY_ALPHABETICAL) { 214 if (V) Log.v(TAG, "getPhonebookNameList, order by alpha"); 215 contactCursor = mResolver.query(myUri, CONTACTS_PROJECTION, CLAUSE_ONLY_VISIBLE, 216 null, Contacts.DISPLAY_NAME); 217 } 218 if (contactCursor != null) { 219 for (contactCursor.moveToFirst(); !contactCursor.isAfterLast(); contactCursor 220 .moveToNext()) { 221 String name = contactCursor.getString(CONTACTS_NAME_COLUMN_INDEX); 222 if (TextUtils.isEmpty(name)) { 223 name = mContext.getString(android.R.string.unknownName); 224 } 225 nameList.add(name); 226 } 227 } 228 } finally { 229 if (contactCursor != null) { 230 contactCursor.close(); 231 } 232 } 233 return nameList; 234 } 235 236 public final ArrayList<String> getContactNamesByNumber(final String phoneNumber) { 237 ArrayList<String> nameList = new ArrayList<String>(); 238 239 Cursor contactCursor = null; 240 Uri uri = null; 241 242 if (phoneNumber != null && phoneNumber.length() == 0) { 243 uri = Contacts.CONTENT_URI; 244 } else { 245 uri = Uri.withAppendedPath(PhoneLookup.CONTENT_FILTER_URI, 246 Uri.encode(phoneNumber)); 247 } 248 249 try { 250 contactCursor = mResolver.query(uri, CONTACTS_PROJECTION, CLAUSE_ONLY_VISIBLE, 251 null, Contacts._ID); 252 253 if (contactCursor != null) { 254 for (contactCursor.moveToFirst(); !contactCursor.isAfterLast(); contactCursor 255 .moveToNext()) { 256 String name = contactCursor.getString(CONTACTS_NAME_COLUMN_INDEX); 257 long id = contactCursor.getLong(CONTACTS_ID_COLUMN_INDEX); 258 if (TextUtils.isEmpty(name)) { 259 name = mContext.getString(android.R.string.unknownName); 260 } 261 if (V) Log.v(TAG, "got name " + name + " by number " + phoneNumber + " @" + id); 262 nameList.add(name); 263 } 264 } 265 } finally { 266 if (contactCursor != null) { 267 contactCursor.close(); 268 } 269 } 270 return nameList; 271 } 272 273 public final int composeAndSendCallLogVcards(final int type, Operation op, 274 final int startPoint, final int endPoint, final boolean vcardType21) { 275 if (startPoint < 1 || startPoint > endPoint) { 276 Log.e(TAG, "internal error: startPoint or endPoint is not correct."); 277 return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR; 278 } 279 String typeSelection = BluetoothPbapObexServer.createSelectionPara(type); 280 281 final Uri myUri = CallLog.Calls.CONTENT_URI; 282 final String[] CALLLOG_PROJECTION = new String[] { 283 CallLog.Calls._ID, // 0 284 }; 285 final int ID_COLUMN_INDEX = 0; 286 287 Cursor callsCursor = null; 288 long startPointId = 0; 289 long endPointId = 0; 290 try { 291 // Need test to see if order by _ID is ok here, or by date? 292 callsCursor = mResolver.query(myUri, CALLLOG_PROJECTION, typeSelection, null, 293 CALLLOG_SORT_ORDER); 294 if (callsCursor != null) { 295 callsCursor.moveToPosition(startPoint - 1); 296 startPointId = callsCursor.getLong(ID_COLUMN_INDEX); 297 if (V) Log.v(TAG, "Call Log query startPointId = " + startPointId); 298 if (startPoint == endPoint) { 299 endPointId = startPointId; 300 } else { 301 callsCursor.moveToPosition(endPoint - 1); 302 endPointId = callsCursor.getLong(ID_COLUMN_INDEX); 303 } 304 if (V) Log.v(TAG, "Call log query endPointId = " + endPointId); 305 } 306 } finally { 307 if (callsCursor != null) { 308 callsCursor.close(); 309 } 310 } 311 312 String recordSelection; 313 if (startPoint == endPoint) { 314 recordSelection = Calls._ID + "=" + startPointId; 315 } else { 316 // The query to call table is by "_id DESC" order, so change 317 // correspondingly. 318 recordSelection = Calls._ID + ">=" + endPointId + " AND " + Calls._ID + "<=" 319 + startPointId; 320 } 321 322 String selection; 323 if (typeSelection == null) { 324 selection = recordSelection; 325 } else { 326 selection = "(" + typeSelection + ") AND (" + recordSelection + ")"; 327 } 328 329 if (V) Log.v(TAG, "Call log query selection is: " + selection); 330 331 return composeAndSendVCards(op, selection, vcardType21, null, false); 332 } 333 334 public final int composeAndSendPhonebookVcards(Operation op, final int startPoint, 335 final int endPoint, final boolean vcardType21, String ownerVCard) { 336 if (startPoint < 1 || startPoint > endPoint) { 337 Log.e(TAG, "internal error: startPoint or endPoint is not correct."); 338 return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR; 339 } 340 final Uri myUri = Contacts.CONTENT_URI; 341 342 Cursor contactCursor = null; 343 long startPointId = 0; 344 long endPointId = 0; 345 try { 346 contactCursor = mResolver.query(myUri, CONTACTS_PROJECTION, CLAUSE_ONLY_VISIBLE, null, 347 Contacts._ID); 348 if (contactCursor != null) { 349 contactCursor.moveToPosition(startPoint - 1); 350 startPointId = contactCursor.getLong(CONTACTS_ID_COLUMN_INDEX); 351 if (V) Log.v(TAG, "Query startPointId = " + startPointId); 352 if (startPoint == endPoint) { 353 endPointId = startPointId; 354 } else { 355 contactCursor.moveToPosition(endPoint - 1); 356 endPointId = contactCursor.getLong(CONTACTS_ID_COLUMN_INDEX); 357 } 358 if (V) Log.v(TAG, "Query endPointId = " + endPointId); 359 } 360 } finally { 361 if (contactCursor != null) { 362 contactCursor.close(); 363 } 364 } 365 366 final String selection; 367 if (startPoint == endPoint) { 368 selection = Contacts._ID + "=" + startPointId + " AND " + CLAUSE_ONLY_VISIBLE; 369 } else { 370 selection = Contacts._ID + ">=" + startPointId + " AND " + Contacts._ID + "<=" 371 + endPointId + " AND " + CLAUSE_ONLY_VISIBLE; 372 } 373 374 if (V) Log.v(TAG, "Query selection is: " + selection); 375 376 return composeAndSendVCards(op, selection, vcardType21, ownerVCard, true); 377 } 378 379 public final int composeAndSendPhonebookOneVcard(Operation op, final int offset, 380 final boolean vcardType21, String ownerVCard, int orderByWhat) { 381 if (offset < 1) { 382 Log.e(TAG, "Internal error: offset is not correct."); 383 return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR; 384 } 385 final Uri myUri = Contacts.CONTENT_URI; 386 Cursor contactCursor = null; 387 String selection = null; 388 long contactId = 0; 389 if (orderByWhat == BluetoothPbapObexServer.ORDER_BY_INDEXED) { 390 try { 391 contactCursor = mResolver.query(myUri, CONTACTS_PROJECTION, CLAUSE_ONLY_VISIBLE, 392 null, Contacts._ID); 393 if (contactCursor != null) { 394 contactCursor.moveToPosition(offset - 1); 395 contactId = contactCursor.getLong(CONTACTS_ID_COLUMN_INDEX); 396 if (V) Log.v(TAG, "Query startPointId = " + contactId); 397 } 398 } finally { 399 if (contactCursor != null) { 400 contactCursor.close(); 401 } 402 } 403 } else if (orderByWhat == BluetoothPbapObexServer.ORDER_BY_ALPHABETICAL) { 404 try { 405 contactCursor = mResolver.query(myUri, CONTACTS_PROJECTION, CLAUSE_ONLY_VISIBLE, 406 null, Contacts.DISPLAY_NAME); 407 if (contactCursor != null) { 408 contactCursor.moveToPosition(offset - 1); 409 contactId = contactCursor.getLong(CONTACTS_ID_COLUMN_INDEX); 410 if (V) Log.v(TAG, "Query startPointId = " + contactId); 411 } 412 } finally { 413 if (contactCursor != null) { 414 contactCursor.close(); 415 } 416 } 417 } else { 418 Log.e(TAG, "Parameter orderByWhat is not supported!"); 419 return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR; 420 } 421 selection = Contacts._ID + "=" + contactId; 422 423 if (V) Log.v(TAG, "Query selection is: " + selection); 424 425 return composeAndSendVCards(op, selection, vcardType21, ownerVCard, true); 426 } 427 428 public final int composeAndSendVCards(Operation op, final String selection, 429 final boolean vcardType21, String ownerVCard, boolean isContacts) { 430 long timestamp = 0; 431 if (V) timestamp = System.currentTimeMillis(); 432 433 if (isContacts) { 434 VCardComposer composer = null; 435 HandlerForStringBuffer buffer = null; 436 try { 437 // Currently only support Generic Vcard 2.1 and 3.0 438 int vcardType; 439 if (vcardType21) { 440 vcardType = VCardConfig.VCARD_TYPE_V21_GENERIC; 441 } else { 442 vcardType = VCardConfig.VCARD_TYPE_V30_GENERIC; 443 } 444 vcardType |= VCardConfig.FLAG_REFRAIN_IMAGE_EXPORT; 445 vcardType |= VCardConfig.FLAG_REFRAIN_PHONE_NUMBER_FORMATTING; 446 447 composer = new VCardComposer(mContext, vcardType, true); 448 buffer = new HandlerForStringBuffer(op, ownerVCard); 449 if (!composer.init(Contacts.CONTENT_URI, selection, null, Contacts._ID) || 450 !buffer.onInit(mContext)) { 451 return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR; 452 } 453 454 while (!composer.isAfterLast()) { 455 if (BluetoothPbapObexServer.sIsAborted) { 456 ((ServerOperation)op).isAborted = true; 457 BluetoothPbapObexServer.sIsAborted = false; 458 break; 459 } 460 String vcard = composer.createOneEntry(); 461 if (vcard == null) { 462 Log.e(TAG, "Failed to read a contact. Error reason: " 463 + composer.getErrorReason()); 464 return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR; 465 } 466 if (!buffer.onEntryCreated(vcard)) { 467 // onEntryCreate() already emits error. 468 return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR; 469 } 470 } 471 } finally { 472 if (composer != null) { 473 composer.terminate(); 474 } 475 if (buffer != null) { 476 buffer.onTerminate(); 477 } 478 } 479 } else { // CallLog 480 BluetoothPbapCallLogComposer composer = null; 481 HandlerForStringBuffer buffer = null; 482 try { 483 484 composer = new BluetoothPbapCallLogComposer(mContext); 485 buffer = new HandlerForStringBuffer(op, ownerVCard); 486 if (!composer.init(CallLog.Calls.CONTENT_URI, selection, null, 487 CALLLOG_SORT_ORDER) || 488 !buffer.onInit(mContext)) { 489 return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR; 490 } 491 492 while (!composer.isAfterLast()) { 493 if (BluetoothPbapObexServer.sIsAborted) { 494 ((ServerOperation)op).isAborted = true; 495 BluetoothPbapObexServer.sIsAborted = false; 496 break; 497 } 498 String vcard = composer.createOneEntry(vcardType21); 499 if (vcard == null) { 500 Log.e(TAG, "Failed to read a contact. Error reason: " 501 + composer.getErrorReason()); 502 return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR; 503 } 504 buffer.onEntryCreated(vcard); 505 } 506 } finally { 507 if (composer != null) { 508 composer.terminate(); 509 } 510 if (buffer != null) { 511 buffer.onTerminate(); 512 } 513 } 514 } 515 516 if (V) Log.v(TAG, "Total vcard composing and sending out takes " 517 + (System.currentTimeMillis() - timestamp) + " ms"); 518 519 return ResponseCodes.OBEX_HTTP_OK; 520 } 521 522 /** 523 * Handler to emit vCards to PCE. 524 */ 525 public class HandlerForStringBuffer { 526 private Operation operation; 527 528 private OutputStream outputStream; 529 530 private String phoneOwnVCard = null; 531 532 public HandlerForStringBuffer(Operation op, String ownerVCard) { 533 operation = op; 534 if (ownerVCard != null) { 535 phoneOwnVCard = ownerVCard; 536 if (V) Log.v(TAG, "phone own number vcard:"); 537 if (V) Log.v(TAG, phoneOwnVCard); 538 } 539 } 540 541 private boolean write(String vCard) { 542 try { 543 if (vCard != null) { 544 outputStream.write(vCard.getBytes()); 545 return true; 546 } 547 } catch (IOException e) { 548 Log.e(TAG, "write outputstrem failed" + e.toString()); 549 } 550 return false; 551 } 552 553 public boolean onInit(Context context) { 554 try { 555 outputStream = operation.openOutputStream(); 556 if (phoneOwnVCard != null) { 557 return write(phoneOwnVCard); 558 } 559 return true; 560 } catch (IOException e) { 561 Log.e(TAG, "open outputstrem failed" + e.toString()); 562 } 563 return false; 564 } 565 566 public boolean onEntryCreated(String vcard) { 567 return write(vcard); 568 } 569 570 public void onTerminate() { 571 if (!BluetoothPbapObexServer.closeStream(outputStream, operation)) { 572 if (V) Log.v(TAG, "CloseStream failed!"); 573 } else { 574 if (V) Log.v(TAG, "CloseStream ok!"); 575 } 576 } 577 } 578} 579