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 33 package com.android.bluetooth.pbap; 34 35 import android.content.Context; 36 import android.content.ContentResolver; 37 import android.database.Cursor; 38 import android.os.Message; 39 import android.os.Handler; 40 import android.provider.CallLog.Calls; 41 import android.provider.CallLog; 42 import android.text.TextUtils; 43 import android.util.Log; 44 45 import java.io.IOException; 46 import java.io.OutputStream; 47 import java.text.CharacterIterator; 48 import java.text.StringCharacterIterator; 49 import java.util.ArrayList; 50 import java.util.Arrays; 51 52 import javax.obex.ServerRequestHandler; 53 import javax.obex.ResponseCodes; 54 import javax.obex.ApplicationParameter; 55 import javax.obex.ServerOperation; 56 import javax.obex.Operation; 57 import javax.obex.HeaderSet; 58 59 public class BluetoothPbapObexServer extends ServerRequestHandler { 60 61 private static final String TAG = "BluetoothPbapObexServer"; 62 63 private static final boolean D = BluetoothPbapService.DEBUG; 64 65 private static final boolean V = BluetoothPbapService.VERBOSE; 66 67 private static final int UUID_LENGTH = 16; 68 69 // The length of suffix of vcard name - ".vcf" is 5 70 private static final int VCARD_NAME_SUFFIX_LENGTH = 5; 71 72 // 128 bit UUID for PBAP 73 private static final byte[] PBAP_TARGET = new byte[] { 74 0x79, 0x61, 0x35, (byte)0xf0, (byte)0xf0, (byte)0xc5, 0x11, (byte)0xd8, 0x09, 0x66, 75 0x08, 0x00, 0x20, 0x0c, (byte)0x9a, 0x66 76 }; 77 78 // Currently not support SIM card 79 private static final String[] LEGAL_PATH = { 80 "/telecom", "/telecom/pb", "/telecom/ich", "/telecom/och", "/telecom/mch", 81 "/telecom/cch" 82 }; 83 84 @SuppressWarnings("unused") 85 private static final String[] LEGAL_PATH_WITH_SIM = { 86 "/telecom", "/telecom/pb", "/telecom/ich", "/telecom/och", "/telecom/mch", 87 "/telecom/cch", "/SIM1", "/SIM1/telecom", "/SIM1/telecom/ich", "/SIM1/telecom/och", 88 "/SIM1/telecom/mch", "/SIM1/telecom/cch", "/SIM1/telecom/pb" 89 90 }; 91 92 // SIM card 93 private static final String SIM1 = "SIM1"; 94 95 // missed call history 96 private static final String MCH = "mch"; 97 98 // incoming call history 99 private static final String ICH = "ich"; 100 101 // outgoing call history 102 private static final String OCH = "och"; 103 104 // combined call history 105 private static final String CCH = "cch"; 106 107 // phone book 108 private static final String PB = "pb"; 109 110 private static final String TELECOM_PATH = "/telecom"; 111 112 private static final String ICH_PATH = "/telecom/ich"; 113 114 private static final String OCH_PATH = "/telecom/och"; 115 116 private static final String MCH_PATH = "/telecom/mch"; 117 118 private static final String CCH_PATH = "/telecom/cch"; 119 120 private static final String PB_PATH = "/telecom/pb"; 121 122 // type for list vcard objects 123 private static final String TYPE_LISTING = "x-bt/vcard-listing"; 124 125 // type for get single vcard object 126 private static final String TYPE_VCARD = "x-bt/vcard"; 127 128 // to indicate if need send body besides headers 129 private static final int NEED_SEND_BODY = -1; 130 131 // type for download all vcard objects 132 private static final String TYPE_PB = "x-bt/phonebook"; 133 134 // The number of indexes in the phone book. 135 private boolean mNeedPhonebookSize = false; 136 137 // The number of missed calls that have not been checked on the PSE at the 138 // point of the request. Only apply to "mch" case. 139 private boolean mNeedNewMissedCallsNum = false; 140 141 private int mMissedCallSize = 0; 142 143 // record current path the client are browsing 144 private String mCurrentPath = ""; 145 146 private Handler mCallback = null; 147 148 private Context mContext; 149 150 private BluetoothPbapVcardManager mVcardManager; 151 152 private int mOrderBy = ORDER_BY_INDEXED; 153 154 private static int CALLLOG_NUM_LIMIT = 50; 155 156 public static int ORDER_BY_INDEXED = 0; 157 158 public static int ORDER_BY_ALPHABETICAL = 1; 159 160 public static boolean sIsAborted = false; 161 162 public static class ContentType { 163 public static final int PHONEBOOK = 1; 164 165 public static final int INCOMING_CALL_HISTORY = 2; 166 167 public static final int OUTGOING_CALL_HISTORY = 3; 168 169 public static final int MISSED_CALL_HISTORY = 4; 170 171 public static final int COMBINED_CALL_HISTORY = 5; 172 } 173 174 public BluetoothPbapObexServer(Handler callback, Context context) { 175 super(); 176 mCallback = callback; 177 mContext = context; 178 mVcardManager = new BluetoothPbapVcardManager(mContext); 179 } 180 181 @Override 182 public int onConnect(final HeaderSet request, HeaderSet reply) { 183 if (V) logHeader(request); 184 notifyUpdateWakeLock(); 185 try { 186 byte[] uuid = (byte[])request.getHeader(HeaderSet.TARGET); 187 if (uuid == null) { 188 return ResponseCodes.OBEX_HTTP_NOT_ACCEPTABLE; 189 } 190 if (D) Log.d(TAG, "onConnect(): uuid=" + Arrays.toString(uuid)); 191 192 if (uuid.length != UUID_LENGTH) { 193 Log.w(TAG, "Wrong UUID length"); 194 return ResponseCodes.OBEX_HTTP_NOT_ACCEPTABLE; 195 } 196 for (int i = 0; i < UUID_LENGTH; i++) { 197 if (uuid[i] != PBAP_TARGET[i]) { 198 Log.w(TAG, "Wrong UUID"); 199 return ResponseCodes.OBEX_HTTP_NOT_ACCEPTABLE; 200 } 201 } 202 reply.setHeader(HeaderSet.WHO, uuid); 203 } catch (IOException e) { 204 Log.e(TAG, e.toString()); 205 return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR; 206 } 207 208 try { 209 byte[] remote = (byte[])request.getHeader(HeaderSet.WHO); 210 if (remote != null) { 211 if (D) Log.d(TAG, "onConnect(): remote=" + Arrays.toString(remote)); 212 reply.setHeader(HeaderSet.TARGET, remote); 213 } 214 } catch (IOException e) { 215 Log.e(TAG, e.toString()); 216 return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR; 217 } 218 219 if (V) Log.v(TAG, "onConnect(): uuid is ok, will send out " + 220 "MSG_SESSION_ESTABLISHED msg."); 221 222 Message msg = Message.obtain(mCallback); 223 msg.what = BluetoothPbapService.MSG_SESSION_ESTABLISHED; 224 msg.sendToTarget(); 225 226 return ResponseCodes.OBEX_HTTP_OK; 227 } 228 229 @Override 230 public void onDisconnect(final HeaderSet req, final HeaderSet resp) { 231 if (D) Log.d(TAG, "onDisconnect(): enter"); 232 if (V) logHeader(req); 233 notifyUpdateWakeLock(); 234 resp.responseCode = ResponseCodes.OBEX_HTTP_OK; 235 if (mCallback != null) { 236 Message msg = Message.obtain(mCallback); 237 msg.what = BluetoothPbapService.MSG_SESSION_DISCONNECTED; 238 msg.sendToTarget(); 239 if (V) Log.v(TAG, "onDisconnect(): msg MSG_SESSION_DISCONNECTED sent out."); 240 } 241 } 242 243 @Override 244 public int onAbort(HeaderSet request, HeaderSet reply) { 245 if (D) Log.d(TAG, "onAbort(): enter."); 246 notifyUpdateWakeLock(); 247 sIsAborted = true; 248 return ResponseCodes.OBEX_HTTP_OK; 249 } 250 251 @Override 252 public int onPut(final Operation op) { 253 if (D) Log.d(TAG, "onPut(): not support PUT request."); 254 notifyUpdateWakeLock(); 255 return ResponseCodes.OBEX_HTTP_BAD_REQUEST; 256 } 257 258 @Override 259 public int onDelete(final HeaderSet request, final HeaderSet reply) { 260 if (D) Log.d(TAG, "onDelete(): not support PUT request."); 261 notifyUpdateWakeLock(); 262 return ResponseCodes.OBEX_HTTP_BAD_REQUEST; 263 } 264 265 @Override 266 public int onSetPath(final HeaderSet request, final HeaderSet reply, final boolean backup, 267 final boolean create) { 268 if (V) logHeader(request); 269 if (D) Log.d(TAG, "before setPath, mCurrentPath == " + mCurrentPath); 270 notifyUpdateWakeLock(); 271 String current_path_tmp = mCurrentPath; 272 String tmp_path = null; 273 try { 274 tmp_path = (String)request.getHeader(HeaderSet.NAME); 275 } catch (IOException e) { 276 Log.e(TAG, "Get name header fail"); 277 return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR; 278 } 279 if (D) Log.d(TAG, "backup=" + backup + " create=" + create + " name=" + tmp_path); 280 281 if (backup) { 282 if (current_path_tmp.length() != 0) { 283 current_path_tmp = current_path_tmp.substring(0, 284 current_path_tmp.lastIndexOf("/")); 285 } 286 } else { 287 if (tmp_path == null) { 288 current_path_tmp = ""; 289 } else { 290 current_path_tmp = current_path_tmp + "/" + tmp_path; 291 } 292 } 293 294 if ((current_path_tmp.length() != 0) && (!isLegalPath(current_path_tmp))) { 295 if (create) { 296 Log.w(TAG, "path create is forbidden!"); 297 return ResponseCodes.OBEX_HTTP_FORBIDDEN; 298 } else { 299 Log.w(TAG, "path is not legal"); 300 return ResponseCodes.OBEX_HTTP_NOT_FOUND; 301 } 302 } 303 mCurrentPath = current_path_tmp; 304 if (V) Log.v(TAG, "after setPath, mCurrentPath == " + mCurrentPath); 305 306 return ResponseCodes.OBEX_HTTP_OK; 307 } 308 309 @Override 310 public void onClose() { 311 if (mCallback != null) { 312 Message msg = Message.obtain(mCallback); 313 msg.what = BluetoothPbapService.MSG_SERVERSESSION_CLOSE; 314 msg.sendToTarget(); 315 if (D) Log.d(TAG, "onClose(): msg MSG_SERVERSESSION_CLOSE sent out."); 316 } 317 } 318 319 @Override 320 public int onGet(Operation op) { 321 notifyUpdateWakeLock(); 322 sIsAborted = false; 323 HeaderSet request = null; 324 HeaderSet reply = new HeaderSet(); 325 String type = ""; 326 String name = ""; 327 byte[] appParam = null; 328 AppParamValue appParamValue = new AppParamValue(); 329 try { 330 request = op.getReceivedHeader(); 331 type = (String)request.getHeader(HeaderSet.TYPE); 332 name = (String)request.getHeader(HeaderSet.NAME); 333 appParam = (byte[])request.getHeader(HeaderSet.APPLICATION_PARAMETER); 334 } catch (IOException e) { 335 Log.e(TAG, "request headers error"); 336 return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR; 337 } 338 339 if (V) logHeader(request); 340 if (D) Log.d(TAG, "OnGet type is " + type + "; name is " + name); 341 342 if (type == null) { 343 return ResponseCodes.OBEX_HTTP_NOT_ACCEPTABLE; 344 } 345 // Accroding to specification,the name header could be omitted such as 346 // sony erriccsonHBH-DS980 347 348 // For "x-bt/phonebook" and "x-bt/vcard-listing": 349 // if name == null, guess what carkit actually want from current path 350 // For "x-bt/vcard": 351 // We decide which kind of content client would like per current path 352 353 boolean validName = true; 354 if (TextUtils.isEmpty(name)) { 355 validName = false; 356 } 357 358 if (!validName || (validName && type.equals(TYPE_VCARD))) { 359 if (D) Log.d(TAG, "Guess what carkit actually want from current path (" + 360 mCurrentPath + ")"); 361 362 if (mCurrentPath.equals(PB_PATH)) { 363 appParamValue.needTag = ContentType.PHONEBOOK; 364 } else if (mCurrentPath.equals(ICH_PATH)) { 365 appParamValue.needTag = ContentType.INCOMING_CALL_HISTORY; 366 } else if (mCurrentPath.equals(OCH_PATH)) { 367 appParamValue.needTag = ContentType.OUTGOING_CALL_HISTORY; 368 } else if (mCurrentPath.equals(MCH_PATH)) { 369 appParamValue.needTag = ContentType.MISSED_CALL_HISTORY; 370 mNeedNewMissedCallsNum = true; 371 } else if (mCurrentPath.equals(CCH_PATH)) { 372 appParamValue.needTag = ContentType.COMBINED_CALL_HISTORY; 373 } else if (mCurrentPath.equals(TELECOM_PATH)) { 374 /* PBAP 1.1.1 change */ 375 if (!validName && type.equals(TYPE_LISTING)) { 376 Log.e(TAG, "invalid vcard listing request in default folder"); 377 return ResponseCodes.OBEX_HTTP_NOT_FOUND; 378 } 379 } else { 380 Log.w(TAG, "mCurrentpath is not valid path!!!"); 381 return ResponseCodes.OBEX_HTTP_NOT_ACCEPTABLE; 382 } 383 if (D) Log.v(TAG, "onGet(): appParamValue.needTag=" + appParamValue.needTag); 384 } else { 385 // Not support SIM card currently 386 if (name.contains(SIM1.subSequence(0, SIM1.length()))) { 387 Log.w(TAG, "Not support access SIM card info!"); 388 return ResponseCodes.OBEX_HTTP_NOT_ACCEPTABLE; 389 } 390 391 // we have weak name checking here to provide better 392 // compatibility with other devices,although unique name such as 393 // "pb.vcf" is required by SIG spec. 394 if (isNameMatchTarget(name, PB)) { 395 appParamValue.needTag = ContentType.PHONEBOOK; 396 if (D) Log.v(TAG, "download phonebook request"); 397 } else if (isNameMatchTarget(name, ICH)) { 398 appParamValue.needTag = ContentType.INCOMING_CALL_HISTORY; 399 if (D) Log.v(TAG, "download incoming calls request"); 400 } else if (isNameMatchTarget(name, OCH)) { 401 appParamValue.needTag = ContentType.OUTGOING_CALL_HISTORY; 402 if (D) Log.v(TAG, "download outgoing calls request"); 403 } else if (isNameMatchTarget(name, MCH)) { 404 appParamValue.needTag = ContentType.MISSED_CALL_HISTORY; 405 mNeedNewMissedCallsNum = true; 406 if (D) Log.v(TAG, "download missed calls request"); 407 } else if (isNameMatchTarget(name, CCH)) { 408 appParamValue.needTag = ContentType.COMBINED_CALL_HISTORY; 409 if (D) Log.v(TAG, "download combined calls request"); 410 } else { 411 Log.w(TAG, "Input name doesn't contain valid info!!!"); 412 return ResponseCodes.OBEX_HTTP_NOT_FOUND; 413 } 414 } 415 416 if ((appParam != null) && !parseApplicationParameter(appParam, appParamValue)) { 417 return ResponseCodes.OBEX_HTTP_BAD_REQUEST; 418 } 419 420 // listing request 421 if (type.equals(TYPE_LISTING)) { 422 return pullVcardListing(appParam, appParamValue, reply, op); 423 } 424 // pull vcard entry request 425 else if (type.equals(TYPE_VCARD)) { 426 return pullVcardEntry(appParam, appParamValue, op, name, mCurrentPath); 427 } 428 // down load phone book request 429 else if (type.equals(TYPE_PB)) { 430 return pullPhonebook(appParam, appParamValue, reply, op, name); 431 } else { 432 Log.w(TAG, "unknown type request!!!"); 433 return ResponseCodes.OBEX_HTTP_NOT_ACCEPTABLE; 434 } 435 } 436 437 private boolean isNameMatchTarget(String name, String target) { 438 String contentTypeName = name; 439 if (contentTypeName.endsWith(".vcf")) { 440 contentTypeName = contentTypeName 441 .substring(0, contentTypeName.length() - ".vcf".length()); 442 } 443 // There is a test case: Client will send a wrong name "/telecom/pbpb". 444 // So we must use the String between '/' and '/' as a indivisible part 445 // for comparing. 446 String[] nameList = contentTypeName.split("/"); 447 for (String subName : nameList) { 448 if (subName.equals(target)) { 449 return true; 450 } 451 } 452 return false; 453 } 454 455 /** check whether path is legal */ 456 private final boolean isLegalPath(final String str) { 457 if (str.length() == 0) { 458 return true; 459 } 460 for (int i = 0; i < LEGAL_PATH.length; i++) { 461 if (str.equals(LEGAL_PATH[i])) { 462 return true; 463 } 464 } 465 return false; 466 } 467 468 private class AppParamValue { 469 public int maxListCount; 470 471 public int listStartOffset; 472 473 public String searchValue; 474 475 // Indicate which vCard parameter the search operation shall be carried 476 // out on. Can be "Name | Number | Sound", default value is "Name". 477 public String searchAttr; 478 479 // Indicate which sorting order shall be used for the 480 // <x-bt/vcard-listing> listing object. 481 // Can be "Alphabetical | Indexed | Phonetical", default value is 482 // "Indexed". 483 public String order; 484 485 public int needTag; 486 487 public boolean vcard21; 488 489 public byte[] filter; 490 491 public boolean ignorefilter; 492 493 public AppParamValue() { 494 maxListCount = 0xFFFF; 495 listStartOffset = 0; 496 searchValue = ""; 497 searchAttr = ""; 498 order = ""; 499 needTag = 0x00; 500 vcard21 = true; 501 //Filter is not set by default 502 ignorefilter = true; 503 filter = new byte[] {0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00} ; 504 } 505 506 public void dump() { 507 Log.i(TAG, "maxListCount=" + maxListCount + " listStartOffset=" + listStartOffset 508 + " searchValue=" + searchValue + " searchAttr=" + searchAttr + " needTag=" 509 + needTag + " vcard21=" + vcard21 + " order=" + order); 510 } 511 } 512 513 /** To parse obex application parameter */ 514 private final boolean parseApplicationParameter(final byte[] appParam, 515 AppParamValue appParamValue) { 516 int i = 0; 517 boolean parseOk = true; 518 while ((i < appParam.length) && (parseOk == true)) { 519 switch (appParam[i]) { 520 case ApplicationParameter.TRIPLET_TAGID.FILTER_TAGID: 521 i += 2; // length and tag field in triplet 522 for (int index=0; index < ApplicationParameter.TRIPLET_LENGTH.FILTER_LENGTH; 523 index++) { 524 if (appParam[i+index] != 0){ 525 appParamValue.ignorefilter = false; 526 appParamValue.filter[index] = appParam[i+index]; 527 } 528 } 529 i += ApplicationParameter.TRIPLET_LENGTH.FILTER_LENGTH; 530 break; 531 case ApplicationParameter.TRIPLET_TAGID.ORDER_TAGID: 532 i += 2; // length and tag field in triplet 533 appParamValue.order = Byte.toString(appParam[i]); 534 i += ApplicationParameter.TRIPLET_LENGTH.ORDER_LENGTH; 535 break; 536 case ApplicationParameter.TRIPLET_TAGID.SEARCH_VALUE_TAGID: 537 i += 1; // length field in triplet 538 // length of search value is variable 539 int length = appParam[i]; 540 if (length == 0) { 541 parseOk = false; 542 break; 543 } 544 if (appParam[i+length] == 0x0) { 545 appParamValue.searchValue = new String(appParam, i + 1, length-1); 546 } else { 547 appParamValue.searchValue = new String(appParam, i + 1, length); 548 } 549 i += length; 550 i += 1; 551 break; 552 case ApplicationParameter.TRIPLET_TAGID.SEARCH_ATTRIBUTE_TAGID: 553 i += 2; 554 appParamValue.searchAttr = Byte.toString(appParam[i]); 555 i += ApplicationParameter.TRIPLET_LENGTH.SEARCH_ATTRIBUTE_LENGTH; 556 break; 557 case ApplicationParameter.TRIPLET_TAGID.MAXLISTCOUNT_TAGID: 558 i += 2; 559 if (appParam[i] == 0 && appParam[i + 1] == 0) { 560 mNeedPhonebookSize = true; 561 } else { 562 int highValue = appParam[i] & 0xff; 563 int lowValue = appParam[i + 1] & 0xff; 564 appParamValue.maxListCount = highValue * 256 + lowValue; 565 } 566 i += ApplicationParameter.TRIPLET_LENGTH.MAXLISTCOUNT_LENGTH; 567 break; 568 case ApplicationParameter.TRIPLET_TAGID.LISTSTARTOFFSET_TAGID: 569 i += 2; 570 int highValue = appParam[i] & 0xff; 571 int lowValue = appParam[i + 1] & 0xff; 572 appParamValue.listStartOffset = highValue * 256 + lowValue; 573 i += ApplicationParameter.TRIPLET_LENGTH.LISTSTARTOFFSET_LENGTH; 574 break; 575 case ApplicationParameter.TRIPLET_TAGID.FORMAT_TAGID: 576 i += 2;// length field in triplet 577 if (appParam[i] != 0) { 578 appParamValue.vcard21 = false; 579 } 580 i += ApplicationParameter.TRIPLET_LENGTH.FORMAT_LENGTH; 581 break; 582 default: 583 parseOk = false; 584 Log.e(TAG, "Parse Application Parameter error"); 585 break; 586 } 587 } 588 589 if (D) appParamValue.dump(); 590 591 return parseOk; 592 } 593 594 /** Form and Send an XML format String to client for Phone book listing */ 595 private final int sendVcardListingXml(final int type, Operation op, 596 final int maxListCount, final int listStartOffset, final String searchValue, 597 String searchAttr) { 598 StringBuilder result = new StringBuilder(); 599 int itemsFound = 0; 600 result.append("<?xml version=\"1.0\"?>"); 601 result.append("<!DOCTYPE vcard-listing SYSTEM \"vcard-listing.dtd\">"); 602 result.append("<vCard-listing version=\"1.0\">"); 603 604 // Phonebook listing request 605 if (type == ContentType.PHONEBOOK) { 606 if (searchAttr.equals("0")) { // search by name 607 itemsFound = createList(maxListCount, listStartOffset, searchValue, result, 608 "name"); 609 } else if (searchAttr.equals("1")) { // search by number 610 itemsFound = createList(maxListCount, listStartOffset, searchValue, result, 611 "number"); 612 }// end of search by number 613 else { 614 return ResponseCodes.OBEX_HTTP_PRECON_FAILED; 615 } 616 } 617 // Call history listing request 618 else { 619 ArrayList<String> nameList = mVcardManager.loadCallHistoryList(type); 620 int requestSize = nameList.size() >= maxListCount ? maxListCount : nameList.size(); 621 int startPoint = listStartOffset; 622 int endPoint = startPoint + requestSize; 623 if (endPoint > nameList.size()) { 624 endPoint = nameList.size(); 625 } 626 if (D) Log.d(TAG, "call log list, size=" + requestSize + " offset=" + listStartOffset); 627 628 for (int j = startPoint; j < endPoint; j++) { 629 writeVCardEntry(j+1, nameList.get(j),result); 630 } 631 } 632 result.append("</vCard-listing>"); 633 634 if (V) Log.v(TAG, "itemsFound =" + itemsFound); 635 636 return pushBytes(op, result.toString()); 637 } 638 639 private int createList(final int maxListCount, final int listStartOffset, 640 final String searchValue, StringBuilder result, String type) { 641 int itemsFound = 0; 642 ArrayList<String> nameList = mVcardManager.getPhonebookNameList(mOrderBy); 643 final int requestSize = nameList.size() >= maxListCount ? maxListCount : nameList.size(); 644 final int listSize = nameList.size(); 645 String compareValue = "", currentValue; 646 647 if (D) Log.d(TAG, "search by " + type + ", requestSize=" + requestSize + " offset=" 648 + listStartOffset + " searchValue=" + searchValue); 649 650 if (type.equals("number")) { 651 // query the number, to get the names 652 ArrayList<String> names = mVcardManager.getContactNamesByNumber(searchValue); 653 for (int i = 0; i < names.size(); i++) { 654 compareValue = names.get(i).trim(); 655 if (D) Log.d(TAG, "compareValue=" + compareValue); 656 for (int pos = listStartOffset; pos < listSize && 657 itemsFound < requestSize; pos++) { 658 currentValue = nameList.get(pos); 659 if (D) Log.d(TAG, "currentValue=" + currentValue); 660 if (currentValue.equals(compareValue)) { 661 itemsFound++; 662 if (currentValue.contains(",")) 663 currentValue = currentValue.substring(0, currentValue.lastIndexOf(',')); 664 writeVCardEntry(pos, currentValue,result); 665 } 666 } 667 if (itemsFound >= requestSize) { 668 break; 669 } 670 } 671 } else { 672 if (searchValue != null) { 673 compareValue = searchValue.trim(); 674 } 675 for (int pos = listStartOffset; pos < listSize && 676 itemsFound < requestSize; pos++) { 677 currentValue = nameList.get(pos); 678 if (currentValue.contains(",")) 679 currentValue = currentValue.substring(0, currentValue.lastIndexOf(',')); 680 681 if (searchValue.isEmpty() || ((currentValue.toLowerCase()).equals(compareValue.toLowerCase()))) { 682 itemsFound++; 683 writeVCardEntry(pos, currentValue,result); 684 } 685 } 686 } 687 return itemsFound; 688 } 689 690 /** 691 * Function to send obex header back to client such as get phonebook size 692 * request 693 */ 694 private final int pushHeader(final Operation op, final HeaderSet reply) { 695 OutputStream outputStream = null; 696 697 if (D) Log.d(TAG, "Push Header"); 698 if (D) Log.d(TAG, reply.toString()); 699 700 int pushResult = ResponseCodes.OBEX_HTTP_OK; 701 try { 702 op.sendHeaders(reply); 703 outputStream = op.openOutputStream(); 704 outputStream.flush(); 705 } catch (IOException e) { 706 Log.e(TAG, e.toString()); 707 pushResult = ResponseCodes.OBEX_HTTP_INTERNAL_ERROR; 708 } finally { 709 if (!closeStream(outputStream, op)) { 710 pushResult = ResponseCodes.OBEX_HTTP_INTERNAL_ERROR; 711 } 712 } 713 return pushResult; 714 } 715 716 /** Function to send vcard data to client */ 717 private final int pushBytes(Operation op, final String vcardString) { 718 if (vcardString == null) { 719 Log.w(TAG, "vcardString is null!"); 720 return ResponseCodes.OBEX_HTTP_OK; 721 } 722 723 OutputStream outputStream = null; 724 int pushResult = ResponseCodes.OBEX_HTTP_OK; 725 try { 726 outputStream = op.openOutputStream(); 727 outputStream.write(vcardString.getBytes()); 728 if (V) Log.v(TAG, "Send Data complete!"); 729 } catch (IOException e) { 730 Log.e(TAG, "open/write outputstrem failed" + e.toString()); 731 pushResult = ResponseCodes.OBEX_HTTP_INTERNAL_ERROR; 732 } 733 734 if (!closeStream(outputStream, op)) { 735 pushResult = ResponseCodes.OBEX_HTTP_INTERNAL_ERROR; 736 } 737 738 return pushResult; 739 } 740 741 private final int handleAppParaForResponse(AppParamValue appParamValue, int size, 742 HeaderSet reply, Operation op) { 743 byte[] misnum = new byte[1]; 744 ApplicationParameter ap = new ApplicationParameter(); 745 746 // In such case, PCE only want the number of index. 747 // So response not contain any Body header. 748 if (mNeedPhonebookSize) { 749 if (V) Log.v(TAG, "Need Phonebook size in response header."); 750 mNeedPhonebookSize = false; 751 752 byte[] pbsize = new byte[2]; 753 754 pbsize[0] = (byte)((size / 256) & 0xff);// HIGH VALUE 755 pbsize[1] = (byte)((size % 256) & 0xff);// LOW VALUE 756 ap.addAPPHeader(ApplicationParameter.TRIPLET_TAGID.PHONEBOOKSIZE_TAGID, 757 ApplicationParameter.TRIPLET_LENGTH.PHONEBOOKSIZE_LENGTH, pbsize); 758 759 if (mNeedNewMissedCallsNum) { 760 mNeedNewMissedCallsNum = false; 761 int nmnum = 0; 762 ContentResolver contentResolver; 763 contentResolver = mContext.getContentResolver(); 764 765 Cursor c = contentResolver.query( 766 Calls.CONTENT_URI, 767 null, 768 Calls.TYPE + " = " + Calls.MISSED_TYPE + " AND " + android.provider.CallLog.Calls.NEW + " = 1", 769 null, 770 Calls.DEFAULT_SORT_ORDER); 771 772 if (c != null) { 773 nmnum = c.getCount(); 774 c.close(); 775 } 776 777 nmnum = nmnum > 0 ? nmnum : 0; 778 misnum[0] = (byte)nmnum; 779 if (D) Log.d(TAG, "handleAppParaForResponse(): mNeedNewMissedCallsNum=true, num= " + nmnum); 780 } 781 reply.setHeader(HeaderSet.APPLICATION_PARAMETER, ap.getAPPparam()); 782 783 if (D) Log.d(TAG, "Send back Phonebook size only, without body info! Size= " + size); 784 785 return pushHeader(op, reply); 786 } 787 788 // Only apply to "mch" download/listing. 789 // NewMissedCalls is used only in the response, together with Body 790 // header. 791 if (mNeedNewMissedCallsNum) { 792 if (V) Log.v(TAG, "Need new missed call num in response header."); 793 mNeedNewMissedCallsNum = false; 794 int nmnum = 0; 795 ContentResolver contentResolver; 796 contentResolver = mContext.getContentResolver(); 797 798 Cursor c = contentResolver.query( 799 Calls.CONTENT_URI, 800 null, 801 Calls.TYPE + " = " + Calls.MISSED_TYPE + " AND " + android.provider.CallLog.Calls.NEW + " = 1", 802 null, 803 Calls.DEFAULT_SORT_ORDER); 804 805 if (c != null) { 806 nmnum = c.getCount(); 807 c.close(); 808 } 809 810 nmnum = nmnum > 0 ? nmnum : 0; 811 misnum[0] = (byte)nmnum; 812 if (D) Log.d(TAG, "handleAppParaForResponse(): mNeedNewMissedCallsNum=true, num= " + nmnum); 813 814 ap.addAPPHeader(ApplicationParameter.TRIPLET_TAGID.NEWMISSEDCALLS_TAGID, 815 ApplicationParameter.TRIPLET_LENGTH.NEWMISSEDCALLS_LENGTH, misnum); 816 reply.setHeader(HeaderSet.APPLICATION_PARAMETER, ap.getAPPparam()); 817 if (D) Log.d(TAG, "handleAppParaForResponse(): mNeedNewMissedCallsNum=true, num= " 818 + nmnum); 819 820 // Only Specifies the headers, not write for now, will write to PCE 821 // together with Body 822 try { 823 op.sendHeaders(reply); 824 } catch (IOException e) { 825 Log.e(TAG, e.toString()); 826 return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR; 827 } 828 } 829 return NEED_SEND_BODY; 830 } 831 832 private final int pullVcardListing(byte[] appParam, AppParamValue appParamValue, 833 HeaderSet reply, Operation op) { 834 String searchAttr = appParamValue.searchAttr.trim(); 835 836 if (searchAttr == null || searchAttr.length() == 0) { 837 // If searchAttr is not set by PCE, set default value per spec. 838 appParamValue.searchAttr = "0"; 839 if (D) Log.d(TAG, "searchAttr is not set by PCE, assume search by name by default"); 840 } else if (!searchAttr.equals("0") && !searchAttr.equals("1")) { 841 Log.w(TAG, "search attr not supported"); 842 if (searchAttr.equals("2")) { 843 // search by sound is not supported currently 844 Log.w(TAG, "do not support search by sound"); 845 return ResponseCodes.OBEX_HTTP_NOT_IMPLEMENTED; 846 } 847 return ResponseCodes.OBEX_HTTP_PRECON_FAILED; 848 } else { 849 Log.i(TAG, "searchAttr is valid: " + searchAttr); 850 } 851 852 int size = mVcardManager.getPhonebookSize(appParamValue.needTag); 853 int needSendBody = handleAppParaForResponse(appParamValue, size, reply, op); 854 if (needSendBody != NEED_SEND_BODY) { 855 return needSendBody; 856 } 857 858 if (size == 0) { 859 if (V) Log.v(TAG, "PhonebookSize is 0, return."); 860 return ResponseCodes.OBEX_HTTP_OK; 861 } 862 863 String orderPara = appParamValue.order.trim(); 864 if (TextUtils.isEmpty(orderPara)) { 865 // If order parameter is not set by PCE, set default value per spec. 866 orderPara = "0"; 867 if (D) Log.d(TAG, "Order parameter is not set by PCE. " + 868 "Assume order by 'Indexed' by default"); 869 } else if (!orderPara.equals("0") && !orderPara.equals("1")) { 870 if (V) Log.v(TAG, "Order parameter is not supported: " + appParamValue.order); 871 if (orderPara.equals("2")) { 872 // Order by sound is not supported currently 873 Log.w(TAG, "Do not support order by sound"); 874 return ResponseCodes.OBEX_HTTP_NOT_IMPLEMENTED; 875 } 876 return ResponseCodes.OBEX_HTTP_PRECON_FAILED; 877 } else { 878 Log.i(TAG, "Order parameter is valid: " + orderPara); 879 } 880 881 if (orderPara.equals("0")) { 882 mOrderBy = ORDER_BY_INDEXED; 883 } else if (orderPara.equals("1")) { 884 mOrderBy = ORDER_BY_ALPHABETICAL; 885 } 886 887 int sendResult = sendVcardListingXml(appParamValue.needTag, op, appParamValue.maxListCount, 888 appParamValue.listStartOffset, appParamValue.searchValue, 889 appParamValue.searchAttr); 890 return sendResult; 891 } 892 893 private final int pullVcardEntry(byte[] appParam, AppParamValue appParamValue, 894 Operation op, final String name, final String current_path) { 895 if (name == null || name.length() < VCARD_NAME_SUFFIX_LENGTH) { 896 if (D) Log.d(TAG, "Name is Null, or the length of name < 5 !"); 897 return ResponseCodes.OBEX_HTTP_NOT_ACCEPTABLE; 898 } 899 String strIndex = name.substring(0, name.length() - VCARD_NAME_SUFFIX_LENGTH + 1); 900 int intIndex = 0; 901 if (strIndex.trim().length() != 0) { 902 try { 903 intIndex = Integer.parseInt(strIndex); 904 } catch (NumberFormatException e) { 905 Log.e(TAG, "catch number format exception " + e.toString()); 906 return ResponseCodes.OBEX_HTTP_NOT_ACCEPTABLE; 907 } 908 } 909 910 int size = mVcardManager.getPhonebookSize(appParamValue.needTag); 911 if (size == 0) { 912 if (V) Log.v(TAG, "PhonebookSize is 0, return."); 913 return ResponseCodes.OBEX_HTTP_NOT_FOUND; 914 } 915 916 boolean vcard21 = appParamValue.vcard21; 917 if (appParamValue.needTag == 0) { 918 Log.w(TAG, "wrong path!"); 919 return ResponseCodes.OBEX_HTTP_NOT_ACCEPTABLE; 920 } else if (appParamValue.needTag == ContentType.PHONEBOOK) { 921 if (intIndex < 0 || intIndex >= size) { 922 Log.w(TAG, "The requested vcard is not acceptable! name= " + name); 923 return ResponseCodes.OBEX_HTTP_NOT_FOUND; 924 } else if (intIndex == 0) { 925 // For PB_PATH, 0.vcf is the phone number of this phone. 926 String ownerVcard = mVcardManager.getOwnerPhoneNumberVcard(vcard21,null); 927 return pushBytes(op, ownerVcard); 928 } else { 929 return mVcardManager.composeAndSendPhonebookOneVcard(op, intIndex, vcard21, null, 930 mOrderBy, appParamValue.ignorefilter, appParamValue.filter); 931 } 932 } else { 933 if (intIndex <= 0 || intIndex > size) { 934 Log.w(TAG, "The requested vcard is not acceptable! name= " + name); 935 return ResponseCodes.OBEX_HTTP_NOT_FOUND; 936 } 937 // For others (ich/och/cch/mch), 0.vcf is meaningless, and must 938 // begin from 1.vcf 939 if (intIndex >= 1) { 940 return mVcardManager.composeAndSendCallLogVcards(appParamValue.needTag, op, 941 intIndex, intIndex, vcard21, appParamValue.ignorefilter, 942 appParamValue.filter); 943 } 944 } 945 return ResponseCodes.OBEX_HTTP_OK; 946 } 947 948 private final int pullPhonebook(byte[] appParam, AppParamValue appParamValue, HeaderSet reply, 949 Operation op, final String name) { 950 // code start for passing PTS3.2 TC_PSE_PBD_BI_01_C 951 if (name != null) { 952 int dotIndex = name.indexOf("."); 953 String vcf = "vcf"; 954 if (dotIndex >= 0 && dotIndex <= name.length()) { 955 if (name.regionMatches(dotIndex + 1, vcf, 0, vcf.length()) == false) { 956 Log.w(TAG, "name is not .vcf"); 957 return ResponseCodes.OBEX_HTTP_NOT_ACCEPTABLE; 958 } 959 } 960 } // code end for passing PTS3.2 TC_PSE_PBD_BI_01_C 961 962 int pbSize = mVcardManager.getPhonebookSize(appParamValue.needTag); 963 int needSendBody = handleAppParaForResponse(appParamValue, pbSize, reply, op); 964 if (needSendBody != NEED_SEND_BODY) { 965 return needSendBody; 966 } 967 968 if (pbSize == 0) { 969 if (V) Log.v(TAG, "PhonebookSize is 0, return."); 970 return ResponseCodes.OBEX_HTTP_OK; 971 } 972 973 int requestSize = pbSize >= appParamValue.maxListCount ? appParamValue.maxListCount 974 : pbSize; 975 int startPoint = appParamValue.listStartOffset; 976 if (startPoint < 0 || startPoint >= pbSize) { 977 Log.w(TAG, "listStartOffset is not correct! " + startPoint); 978 return ResponseCodes.OBEX_HTTP_OK; 979 } 980 981 // Limit the number of call log to CALLLOG_NUM_LIMIT 982 if (appParamValue.needTag != BluetoothPbapObexServer.ContentType.PHONEBOOK) { 983 if (requestSize > CALLLOG_NUM_LIMIT) { 984 requestSize = CALLLOG_NUM_LIMIT; 985 } 986 } 987 988 int endPoint = startPoint + requestSize - 1; 989 if (endPoint > pbSize - 1) { 990 endPoint = pbSize - 1; 991 } 992 if (D) Log.d(TAG, "pullPhonebook(): requestSize=" + requestSize + " startPoint=" + 993 startPoint + " endPoint=" + endPoint); 994 995 boolean vcard21 = appParamValue.vcard21; 996 if (appParamValue.needTag == BluetoothPbapObexServer.ContentType.PHONEBOOK) { 997 if (startPoint == 0) { 998 String ownerVcard = mVcardManager.getOwnerPhoneNumberVcard(vcard21,null); 999 if (endPoint == 0) { 1000 return pushBytes(op, ownerVcard); 1001 } else { 1002 return mVcardManager.composeAndSendPhonebookVcards(op, 1, endPoint, vcard21, 1003 ownerVcard, appParamValue.ignorefilter, appParamValue.filter); 1004 } 1005 } else { 1006 return mVcardManager.composeAndSendPhonebookVcards(op, startPoint, endPoint, 1007 vcard21, null, appParamValue.ignorefilter, appParamValue.filter); 1008 } 1009 } else { 1010 return mVcardManager.composeAndSendCallLogVcards(appParamValue.needTag, op, 1011 startPoint + 1, endPoint + 1, vcard21, appParamValue.ignorefilter, 1012 appParamValue.filter); 1013 } 1014 } 1015 1016 public static boolean closeStream(final OutputStream out, final Operation op) { 1017 boolean returnvalue = true; 1018 try { 1019 if (out != null) { 1020 out.close(); 1021 } 1022 } catch (IOException e) { 1023 Log.e(TAG, "outputStream close failed" + e.toString()); 1024 returnvalue = false; 1025 } 1026 try { 1027 if (op != null) { 1028 op.close(); 1029 } 1030 } catch (IOException e) { 1031 Log.e(TAG, "operation close failed" + e.toString()); 1032 returnvalue = false; 1033 } 1034 return returnvalue; 1035 } 1036 1037 // Reserved for future use. In case PSE challenge PCE and PCE input wrong 1038 // session key. 1039 public final void onAuthenticationFailure(final byte[] userName) { 1040 } 1041 1042 public static final String createSelectionPara(final int type) { 1043 String selection = null; 1044 switch (type) { 1045 case ContentType.INCOMING_CALL_HISTORY: 1046 selection = Calls.TYPE + "=" + CallLog.Calls.INCOMING_TYPE; 1047 break; 1048 case ContentType.OUTGOING_CALL_HISTORY: 1049 selection = Calls.TYPE + "=" + CallLog.Calls.OUTGOING_TYPE; 1050 break; 1051 case ContentType.MISSED_CALL_HISTORY: 1052 selection = Calls.TYPE + "=" + CallLog.Calls.MISSED_TYPE; 1053 break; 1054 default: 1055 break; 1056 } 1057 if (V) Log.v(TAG, "Call log selection: " + selection); 1058 return selection; 1059 } 1060 1061 /** 1062 * XML encode special characters in the name field 1063 */ 1064 private void xmlEncode(String name, StringBuilder result) { 1065 if (name == null) { 1066 return; 1067 } 1068 1069 final StringCharacterIterator iterator = new StringCharacterIterator(name); 1070 char character = iterator.current(); 1071 while (character != CharacterIterator.DONE ){ 1072 if (character == '<') { 1073 result.append("<"); 1074 } 1075 else if (character == '>') { 1076 result.append(">"); 1077 } 1078 else if (character == '\"') { 1079 result.append("""); 1080 } 1081 else if (character == '\'') { 1082 result.append("'"); 1083 } 1084 else if (character == '&') { 1085 result.append("&"); 1086 } 1087 else { 1088 // The char is not a special one, add it to the result as is 1089 result.append(character); 1090 } 1091 character = iterator.next(); 1092 } 1093 } 1094 1095 private void writeVCardEntry(int vcfIndex, String name, StringBuilder result) { 1096 result.append("<card handle=\""); 1097 result.append(vcfIndex); 1098 result.append(".vcf\" name=\""); 1099 xmlEncode(name, result); 1100 result.append("\"/>"); 1101 } 1102 1103 private void notifyUpdateWakeLock() { 1104 Message msg = Message.obtain(mCallback); 1105 msg.what = BluetoothPbapService.MSG_ACQUIRE_WAKE_LOCK; 1106 msg.sendToTarget(); 1107 } 1108 1109 public static final void logHeader(HeaderSet hs) { 1110 Log.v(TAG, "Dumping HeaderSet " + hs.toString()); 1111 try { 1112 1113 Log.v(TAG, "COUNT : " + hs.getHeader(HeaderSet.COUNT)); 1114 Log.v(TAG, "NAME : " + hs.getHeader(HeaderSet.NAME)); 1115 Log.v(TAG, "TYPE : " + hs.getHeader(HeaderSet.TYPE)); 1116 Log.v(TAG, "LENGTH : " + hs.getHeader(HeaderSet.LENGTH)); 1117 Log.v(TAG, "TIME_ISO_8601 : " + hs.getHeader(HeaderSet.TIME_ISO_8601)); 1118 Log.v(TAG, "TIME_4_BYTE : " + hs.getHeader(HeaderSet.TIME_4_BYTE)); 1119 Log.v(TAG, "DESCRIPTION : " + hs.getHeader(HeaderSet.DESCRIPTION)); 1120 Log.v(TAG, "TARGET : " + hs.getHeader(HeaderSet.TARGET)); 1121 Log.v(TAG, "HTTP : " + hs.getHeader(HeaderSet.HTTP)); 1122 Log.v(TAG, "WHO : " + hs.getHeader(HeaderSet.WHO)); 1123 Log.v(TAG, "OBJECT_CLASS : " + hs.getHeader(HeaderSet.OBJECT_CLASS)); 1124 Log.v(TAG, "APPLICATION_PARAMETER : " + hs.getHeader(HeaderSet.APPLICATION_PARAMETER)); 1125 } catch (IOException e) { 1126 Log.e(TAG, "dump HeaderSet error " + e); 1127 } 1128 } 1129 } 1130