BluetoothPbapVcardManager.java revision 4ceaa3e03dc7a68d4e7c0c8e14a461cfd7c4c19c
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 com.android.bluetooth.R; 36 37import android.net.Uri; 38import android.os.Handler; 39import android.text.TextUtils; 40import android.util.Log; 41import android.database.Cursor; 42import android.content.ContentResolver; 43import android.content.Context; 44import android.provider.CallLog; 45import android.provider.CallLog.Calls; 46import android.provider.ContactsContract.RawContacts; 47import android.provider.ContactsContract.PhoneLookup; 48import android.pim.vcard.VCardComposer; 49import android.pim.vcard.VCardConfig; 50import android.pim.vcard.VCardComposer.OneEntryHandler; 51import android.provider.ContactsContract; 52import android.provider.ContactsContract.CommonDataKinds; 53import android.provider.ContactsContract.Contacts; 54import android.provider.ContactsContract.Data; 55import android.provider.ContactsContract.CommonDataKinds.Phone; 56 57import javax.obex.ResponseCodes; 58import javax.obex.Operation; 59import java.io.IOException; 60import java.io.OutputStream; 61import java.io.Writer; 62import java.util.ArrayList; 63 64public class BluetoothPbapVcardManager { 65 private static final String TAG = "BluetoothPbapVcardManager"; 66 67 private static final boolean V = BluetoothPbapService.VERBOSE; 68 69 private ContentResolver mResolver; 70 71 private Context mContext; 72 73 private StringBuilder mVcardResults = null; 74 75 static final String[] PHONES_PROJECTION = new String[] { 76 Data._ID, // 0 77 CommonDataKinds.Phone.TYPE, // 1 78 CommonDataKinds.Phone.LABEL, // 2 79 CommonDataKinds.Phone.NUMBER, // 3 80 Contacts.DISPLAY_NAME, // 4 81 }; 82 83 private static final int ID_COLUMN_INDEX = 0; 84 85 private static final int PHONE_TYPE_COLUMN_INDEX = 1; 86 87 private static final int PHONE_LABEL_COLUMN_INDEX = 2; 88 89 private static final int PHOEN_NUMBER_COLUMN_INDEX = 3; 90 91 private static final int CONTACTS_DISPLAY_NAME_COLUMN_INDEX = 4; 92 93 static final String SORT_ORDER_NAME = Contacts.DISPLAY_NAME + " ASC"; 94 95 static final String SORT_ORDER_PHONE_NUMBER = CommonDataKinds.Phone.NUMBER + " ASC"; 96 97 public BluetoothPbapVcardManager(final Context context) { 98 mContext = context; 99 mResolver = mContext.getContentResolver(); 100 } 101 102 public final String getOwnerPhoneNumberVcard(final boolean vcardType21) { 103 VCardComposer composer = new VCardComposer(mContext); 104 String name = BluetoothPbapService.getLocalPhoneName(); 105 String number = BluetoothPbapService.getLocalPhoneNum(); 106 String vcard = composer.composeVCardForPhoneOwnNumber(Phone.TYPE_MOBILE, name, number, 107 vcardType21); 108 return vcard; 109 } 110 111 public final int getPhonebookSize(final int type) { 112 int size; 113 switch (type) { 114 case BluetoothPbapObexServer.ContentType.PHONEBOOK: 115 size = getContactsSize(); 116 break; 117 default: 118 size = getCallHistorySize(type); 119 break; 120 } 121 if (V) Log.v(TAG, "getPhonebookSzie size = " + size + " type = " + type); 122 return size; 123 } 124 125 public final int getContactsSize() { 126 Uri myUri = RawContacts.CONTENT_URI; 127 int size = 0; 128 Cursor contactCursor = null; 129 try { 130 contactCursor = mResolver.query(myUri, null, null, null, null); 131 if (contactCursor != null) { 132 size = contactCursor.getCount() + 1; // always has the 0.vcf 133 } 134 } finally { 135 if (contactCursor != null) { 136 contactCursor.close(); 137 } 138 } 139 return size; 140 } 141 142 public final int getCallHistorySize(final int type) { 143 Uri myUri = CallLog.Calls.CONTENT_URI; 144 String selection = BluetoothPbapObexServer.createSelectionPara(type); 145 int size = 0; 146 Cursor callCursor = null; 147 try { 148 callCursor = mResolver.query(myUri, null, selection, null, 149 CallLog.Calls.DEFAULT_SORT_ORDER); 150 if (callCursor != null) { 151 size = callCursor.getCount(); 152 } 153 } finally { 154 if (callCursor != null) { 155 callCursor.close(); 156 } 157 } 158 return size; 159 } 160 161 public final ArrayList<String> loadCallHistoryList(final int type) { 162 Uri myUri = CallLog.Calls.CONTENT_URI; 163 String selection = BluetoothPbapObexServer.createSelectionPara(type); 164 String[] projection = new String[] { 165 Calls.NUMBER, Calls.CACHED_NAME 166 }; 167 final int CALLS_NUMBER_COLUMN_INDEX = 0; 168 final int CALLS_NAME_COLUMN_INDEX = 1; 169 170 Cursor callCursor = null; 171 ArrayList<String> list = new ArrayList<String>(); 172 try { 173 callCursor = mResolver.query(myUri, projection, selection, null, 174 CallLog.Calls.DEFAULT_SORT_ORDER); 175 if (callCursor != null) { 176 for (callCursor.moveToFirst(); !callCursor.isAfterLast(); 177 callCursor.moveToNext()) { 178 String name = callCursor.getString(CALLS_NAME_COLUMN_INDEX); 179 if (TextUtils.isEmpty(name)) { 180 // name not found,use number instead 181 name = callCursor.getString(CALLS_NUMBER_COLUMN_INDEX); 182 } 183 list.add(name); 184 } 185 } 186 } finally { 187 if (callCursor != null) { 188 callCursor.close(); 189 } 190 } 191 return list; 192 } 193 194 public final ArrayList<String> getPhonebookNameList() { 195 ArrayList<String> nameList = new ArrayList<String>(); 196 nameList.add(BluetoothPbapService.getLocalPhoneName()); 197 198 Uri myUri = Phone.CONTENT_URI; 199 Cursor phoneCursor = null; 200 try { 201 phoneCursor = mResolver.query(myUri, PHONES_PROJECTION, null, null, SORT_ORDER_NAME); 202 if (phoneCursor != null) { 203 for (phoneCursor.moveToFirst(); !phoneCursor.isAfterLast(); phoneCursor 204 .moveToNext()) { 205 String name = phoneCursor.getString(CONTACTS_DISPLAY_NAME_COLUMN_INDEX); 206 if (TextUtils.isEmpty(name)) { 207 name = mContext.getString(android.R.string.unknownName); 208 } 209 nameList.add(name); 210 } 211 } 212 } finally { 213 if (phoneCursor != null) { 214 phoneCursor.close(); 215 } 216 } 217 return nameList; 218 } 219 220 public final ArrayList<String> getPhonebookNumberList() { 221 ArrayList<String> numberList = new ArrayList<String>(); 222 numberList.add(BluetoothPbapService.getLocalPhoneNum()); 223 224 Uri myUri = Phone.CONTENT_URI; 225 Cursor phoneCursor = null; 226 try { 227 phoneCursor = mResolver.query(myUri, PHONES_PROJECTION, null, null, 228 SORT_ORDER_PHONE_NUMBER); 229 if (phoneCursor != null) { 230 for (phoneCursor.moveToFirst(); !phoneCursor.isAfterLast(); phoneCursor 231 .moveToNext()) { 232 String number = phoneCursor.getString(PHOEN_NUMBER_COLUMN_INDEX); 233 if (TextUtils.isEmpty(number)) { 234 number = mContext.getString(R.string.defaultnumber); 235 } 236 numberList.add(number); 237 } 238 } 239 } finally { 240 if (phoneCursor != null) { 241 phoneCursor.close(); 242 } 243 } 244 return numberList; 245 } 246 247 public final int composeAndSendCallLogVcards(final int type, final Operation op, 248 final int startPoint, final int endPoint, final boolean vcardType21) { 249 if (startPoint < 1 || startPoint > endPoint) { 250 Log.e(TAG, "internal error: startPoint or endPoint is not correct."); 251 return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR; 252 } 253 String typeSelection = BluetoothPbapObexServer.createSelectionPara(type); 254 255 Uri myUri = CallLog.Calls.CONTENT_URI; 256 final String[] CALLLOG_PROJECTION = new String[] { 257 CallLog.Calls._ID, // 0 258 }; 259 final int ID_COLUMN_INDEX = 0; 260 261 Cursor callsCursor = null; 262 long startPointId = 0; 263 long endPointId = 0; 264 try { 265 // Need test to see if order by _ID is ok here, or by date? 266 callsCursor = mResolver.query(myUri, CALLLOG_PROJECTION, typeSelection, null, 267 CallLog.Calls._ID); 268 if (callsCursor != null) { 269 callsCursor.moveToPosition(startPoint - 1); 270 startPointId = callsCursor.getLong(ID_COLUMN_INDEX); 271 if (V) Log.v(TAG, "Call Log query startPointId = " + startPointId); 272 if (startPoint == endPoint) { 273 endPointId = startPointId; 274 } else { 275 callsCursor.moveToPosition(endPoint - 1); 276 endPointId = callsCursor.getLong(ID_COLUMN_INDEX); 277 } 278 if (V) Log.v(TAG, "Call log query endPointId = " + endPointId); 279 } 280 } finally { 281 if (callsCursor != null) { 282 callsCursor.close(); 283 } 284 } 285 286 String recordSelection; 287 if (startPoint == endPoint) { 288 recordSelection = Calls._ID + "=" + startPointId; 289 } else { 290 recordSelection = Calls._ID + ">=" + startPointId + " AND " + Calls._ID + "<=" 291 + endPointId; 292 } 293 294 String selection; 295 if (typeSelection == null) { 296 selection = recordSelection; 297 } else { 298 selection = "(" + typeSelection + ") AND (" + recordSelection + ")"; 299 } 300 301 if (V) Log.v(TAG, "Call log query selection is: " + selection); 302 303 return composeAndSendVCards(op, selection, vcardType21, null, false); 304 } 305 306 public final int composeAndSendPhonebookVcards(final Operation op, final int startPoint, 307 final int endPoint, final boolean vcardType21, String ownerVCard) { 308 if (startPoint < 1 || startPoint > endPoint) { 309 Log.e(TAG, "internal error: startPoint or endPoint is not correct."); 310 return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR; 311 } 312 Uri myUri = RawContacts.CONTENT_URI; 313 final String[] RAW_CONTACTS_PROJECTION = new String[] { 314 RawContacts._ID, // 0 315 }; 316 final int ID_COLUMN_INDEX = 0; 317 318 Cursor contactCursor = null; 319 long startPointId = 0; 320 long endPointId = 0; 321 try { 322 contactCursor = mResolver.query(myUri, RAW_CONTACTS_PROJECTION, null, null, 323 RawContacts._ID); 324 if (contactCursor != null) { 325 contactCursor.moveToPosition(startPoint - 1); 326 startPointId = contactCursor.getLong(ID_COLUMN_INDEX); 327 if (V) Log.v(TAG, "Query startPointId = " + startPointId); 328 if (startPoint == endPoint) { 329 endPointId = startPointId; 330 } else { 331 contactCursor.moveToPosition(endPoint - 1); 332 endPointId = contactCursor.getLong(ID_COLUMN_INDEX); 333 } 334 if (V) Log.v(TAG, "Query endPointId = " + endPointId); 335 } 336 } finally { 337 if (contactCursor != null) { 338 contactCursor.close(); 339 } 340 } 341 342 String selection; 343 if (startPoint == endPoint) { 344 selection = RawContacts._ID + "=" + startPointId; 345 } else { 346 selection = RawContacts._ID + ">=" + startPointId + " AND " + RawContacts._ID + "<=" 347 + endPointId; 348 } 349 350 if (V) Log.v(TAG, "Query selection is: " + selection); 351 352 return composeAndSendVCards(op, selection, vcardType21, ownerVCard, true); 353 } 354 355 public final int composeAndSendVCards(final Operation op, final String selection, 356 final boolean vcardType21, String ownerVCard, boolean isContacts) { 357 long timestamp = 0; 358 if (V) timestamp = System.currentTimeMillis(); 359 360 VCardComposer composer = null; 361 try { 362 // Currently only support Generic Vcard 2.1 and 3.0 363 int vcardType; 364 if (vcardType21) { 365 vcardType = VCardConfig.VCARD_TYPE_V21_GENERIC; 366 } else { 367 vcardType = VCardConfig.VCARD_TYPE_V30_GENERIC; 368 } 369 370 composer = new VCardComposer(mContext, vcardType, true); 371 composer.addHandler(new HandlerForStringBuffer(op, ownerVCard)); 372 final Uri contentUri = (isContacts ? Contacts.CONTENT_URI : CallLog.Calls.CONTENT_URI); 373 if (!composer.init(contentUri, selection, null, null)) { 374 return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR; 375 } 376 377 while (!composer.isAfterLast()) { 378 if (!composer.createOneEntry()) { 379 Log.e(TAG, "Failed to read a contact. Error reason: " 380 + composer.getErrorReason()); 381 return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR; 382 } 383 } 384 } finally { 385 if (composer != null) { 386 composer.terminate(); 387 } 388 } 389 390 if (V) Log.v(TAG, "Total vcard composing and sending out takes " 391 + (System.currentTimeMillis() - timestamp) + " ms"); 392 393 return ResponseCodes.OBEX_HTTP_OK; 394 } 395 396 /** 397 * Handler to emit VCard String to PCE once size grow to maxPacketSize. 398 */ 399 public class HandlerForStringBuffer implements OneEntryHandler { 400 @SuppressWarnings("hiding") 401 private Operation operation; 402 403 private OutputStream outputStream; 404 405 private int maxPacketSize; 406 407 private String phoneOwnVCard = null; 408 409 public HandlerForStringBuffer(Operation op, String ownerVCard) { 410 operation = op; 411 maxPacketSize = operation.getMaxPacketSize(); 412 if (V) Log.v(TAG, "getMaxPacketSize() = " + maxPacketSize); 413 if (ownerVCard != null) { 414 phoneOwnVCard = ownerVCard; 415 if (V) Log.v(TAG, "phone own number vcard:"); 416 if (V) Log.v(TAG, phoneOwnVCard); 417 } 418 } 419 420 public boolean onInit(Context context) { 421 try { 422 outputStream = operation.openOutputStream(); 423 mVcardResults = new StringBuilder(); 424 if (phoneOwnVCard != null) { 425 mVcardResults.append(phoneOwnVCard); 426 } 427 } catch (IOException e) { 428 Log.e(TAG, "open outputstrem failed" + e.toString()); 429 return false; 430 } 431 if (V) Log.v(TAG, "openOutputStream() ok."); 432 return true; 433 } 434 435 public boolean onEntryCreated(String vcard) { 436 int vcardLen = vcard.length(); 437 if (V) Log.v(TAG, "The length of this vcard is: " + vcardLen); 438 439 mVcardResults.append(vcard); 440 int vcardStringLen = mVcardResults.toString().length(); 441 if (V) Log.v(TAG, "The length of this vcardResults is: " + vcardStringLen); 442 443 if (vcardStringLen >= maxPacketSize) { 444 long timestamp = 0; 445 int position = 0; 446 447 // Need while loop to handle the big vcard case 448 while (position < (vcardStringLen - maxPacketSize)) { 449 if (V) timestamp = System.currentTimeMillis(); 450 451 String subStr = mVcardResults.toString().substring(position, 452 position + maxPacketSize); 453 try { 454 outputStream.write(subStr.getBytes(), 0, maxPacketSize); 455 } catch (IOException e) { 456 Log.e(TAG, "write outputstrem failed" + e.toString()); 457 return false; 458 } 459 if (V) Log.v(TAG, "Sending vcard String " + maxPacketSize + " bytes took " 460 + (System.currentTimeMillis() - timestamp) + " ms"); 461 462 position += maxPacketSize; 463 } 464 mVcardResults.delete(0, position); 465 } 466 return true; 467 } 468 469 public void onTerminate() { 470 // Send out last packet 471 String lastStr = mVcardResults.toString(); 472 try { 473 outputStream.write(lastStr.getBytes(), 0, lastStr.length()); 474 } catch (IOException e) { 475 Log.e(TAG, "write outputstrem failed" + e.toString()); 476 } 477 if (V) Log.v(TAG, "Last packet sent out, sending process complete!"); 478 479 if (!BluetoothPbapObexServer.closeStream(outputStream, operation)) { 480 if (V) Log.v(TAG, "CloseStream failed!"); 481 } else { 482 if (V) Log.v(TAG, "CloseStream ok!"); 483 } 484 } 485 } 486} 487