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 BluetoothPbapObexServer(Handler callback, Context context)174 public BluetoothPbapObexServer(Handler callback, Context context) { 175 super(); 176 mCallback = callback; 177 mContext = context; 178 mVcardManager = new BluetoothPbapVcardManager(mContext); 179 } 180 181 @Override onConnect(final HeaderSet request, HeaderSet reply)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 onDisconnect(final HeaderSet req, final HeaderSet resp)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 onAbort(HeaderSet request, HeaderSet reply)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 onPut(final Operation op)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 onDelete(final HeaderSet request, final HeaderSet reply)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 onSetPath(final HeaderSet request, final HeaderSet reply, final boolean backup, final boolean create)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 onClose()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 onGet(Operation op)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 isNameMatchTarget(String name, String target)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 */ isLegalPath(final String str)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 AppParamValue()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 dump()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 */ parseApplicationParameter(final byte[] appParam, AppParamValue appParamValue)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 */ sendVcardListingXml(final int type, Operation op, final int maxListCount, final int listStartOffset, final String searchValue, String searchAttr)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 createList(final int maxListCount, final int listStartOffset, final String searchValue, StringBuilder result, String type)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 */ pushHeader(final Operation op, final HeaderSet reply)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 */ pushBytes(Operation op, final String vcardString)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 handleAppParaForResponse(AppParamValue appParamValue, int size, HeaderSet reply, Operation op)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 pullVcardListing(byte[] appParam, AppParamValue appParamValue, HeaderSet reply, Operation op)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 pullVcardEntry(byte[] appParam, AppParamValue appParamValue, Operation op, final String name, final String current_path)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 pullPhonebook(byte[] appParam, AppParamValue appParamValue, HeaderSet reply, Operation op, final String name)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 closeStream(final OutputStream out, final Operation op)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. onAuthenticationFailure(final byte[] userName)1039 public final void onAuthenticationFailure(final byte[] userName) { 1040 } 1041 createSelectionPara(final int type)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 */ xmlEncode(String name, StringBuilder result)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 writeVCardEntry(int vcfIndex, String name, StringBuilder result)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 notifyUpdateWakeLock()1103 private void notifyUpdateWakeLock() { 1104 Message msg = Message.obtain(mCallback); 1105 msg.what = BluetoothPbapService.MSG_ACQUIRE_WAKE_LOCK; 1106 msg.sendToTarget(); 1107 } 1108 logHeader(HeaderSet hs)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