/* * Copyright (c) 2008-2009, Motorola, Inc. * * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * - Redistributions of source code must retain the above copyright notice, * this list of conditions and the following disclaimer. * * - Redistributions in binary form must reproduce the above copyright notice, * this list of conditions and the following disclaimer in the documentation * and/or other materials provided with the distribution. * * - Neither the name of the Motorola, Inc. nor the names of its contributors * may be used to endorse or promote products derived from this software * without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */ package com.android.bluetooth.pbap; import android.content.Context; import android.os.Message; import android.os.Handler; import android.text.TextUtils; import android.util.Log; import android.provider.CallLog.Calls; import android.provider.CallLog; import java.io.IOException; import java.io.OutputStream; import java.text.CharacterIterator; import java.text.StringCharacterIterator; import java.util.ArrayList; import java.util.Arrays; import javax.obex.ServerRequestHandler; import javax.obex.ResponseCodes; import javax.obex.ApplicationParameter; import javax.obex.ServerOperation; import javax.obex.Operation; import javax.obex.HeaderSet; public class BluetoothPbapObexServer extends ServerRequestHandler { private static final String TAG = "BluetoothPbapObexServer"; private static final boolean D = BluetoothPbapService.DEBUG; private static final boolean V = BluetoothPbapService.VERBOSE; private static final int UUID_LENGTH = 16; // The length of suffix of vcard name - ".vcf" is 5 private static final int VCARD_NAME_SUFFIX_LENGTH = 5; // 128 bit UUID for PBAP private static final byte[] PBAP_TARGET = new byte[] { 0x79, 0x61, 0x35, (byte)0xf0, (byte)0xf0, (byte)0xc5, 0x11, (byte)0xd8, 0x09, 0x66, 0x08, 0x00, 0x20, 0x0c, (byte)0x9a, 0x66 }; // Currently not support SIM card private static final String[] LEGAL_PATH = { "/telecom", "/telecom/pb", "/telecom/ich", "/telecom/och", "/telecom/mch", "/telecom/cch" }; @SuppressWarnings("unused") private static final String[] LEGAL_PATH_WITH_SIM = { "/telecom", "/telecom/pb", "/telecom/ich", "/telecom/och", "/telecom/mch", "/telecom/cch", "/SIM1", "/SIM1/telecom", "/SIM1/telecom/ich", "/SIM1/telecom/och", "/SIM1/telecom/mch", "/SIM1/telecom/cch", "/SIM1/telecom/pb" }; // SIM card private static final String SIM1 = "SIM1"; // missed call history private static final String MCH = "mch"; // incoming call history private static final String ICH = "ich"; // outgoing call history private static final String OCH = "och"; // combined call history private static final String CCH = "cch"; // phone book private static final String PB = "pb"; private static final String ICH_PATH = "/telecom/ich"; private static final String OCH_PATH = "/telecom/och"; private static final String MCH_PATH = "/telecom/mch"; private static final String CCH_PATH = "/telecom/cch"; private static final String PB_PATH = "/telecom/pb"; // type for list vcard objects private static final String TYPE_LISTING = "x-bt/vcard-listing"; // type for get single vcard object private static final String TYPE_VCARD = "x-bt/vcard"; // to indicate if need send body besides headers private static final int NEED_SEND_BODY = -1; // type for download all vcard objects private static final String TYPE_PB = "x-bt/phonebook"; // The number of indexes in the phone book. private boolean mNeedPhonebookSize = false; // The number of missed calls that have not been checked on the PSE at the // point of the request. Only apply to "mch" case. private boolean mNeedNewMissedCallsNum = false; private int mMissedCallSize = 0; // record current path the client are browsing private String mCurrentPath = ""; private Handler mCallback = null; private Context mContext; private BluetoothPbapVcardManager mVcardManager; private int mOrderBy = ORDER_BY_INDEXED; private static int CALLLOG_NUM_LIMIT = 50; public static int ORDER_BY_INDEXED = 0; public static int ORDER_BY_ALPHABETICAL = 1; public static boolean sIsAborted = false; public static class ContentType { public static final int PHONEBOOK = 1; public static final int INCOMING_CALL_HISTORY = 2; public static final int OUTGOING_CALL_HISTORY = 3; public static final int MISSED_CALL_HISTORY = 4; public static final int COMBINED_CALL_HISTORY = 5; } public BluetoothPbapObexServer(Handler callback, Context context) { super(); mCallback = callback; mContext = context; mVcardManager = new BluetoothPbapVcardManager(mContext); } @Override public int onConnect(final HeaderSet request, HeaderSet reply) { if (V) logHeader(request); notifyUpdateWakeLock(); try { byte[] uuid = (byte[])request.getHeader(HeaderSet.TARGET); if (uuid == null) { return ResponseCodes.OBEX_HTTP_NOT_ACCEPTABLE; } if (D) Log.d(TAG, "onConnect(): uuid=" + Arrays.toString(uuid)); if (uuid.length != UUID_LENGTH) { Log.w(TAG, "Wrong UUID length"); return ResponseCodes.OBEX_HTTP_NOT_ACCEPTABLE; } for (int i = 0; i < UUID_LENGTH; i++) { if (uuid[i] != PBAP_TARGET[i]) { Log.w(TAG, "Wrong UUID"); return ResponseCodes.OBEX_HTTP_NOT_ACCEPTABLE; } } reply.setHeader(HeaderSet.WHO, uuid); } catch (IOException e) { Log.e(TAG, e.toString()); return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR; } try { byte[] remote = (byte[])request.getHeader(HeaderSet.WHO); if (remote != null) { if (D) Log.d(TAG, "onConnect(): remote=" + Arrays.toString(remote)); reply.setHeader(HeaderSet.TARGET, remote); } } catch (IOException e) { Log.e(TAG, e.toString()); return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR; } if (V) Log.v(TAG, "onConnect(): uuid is ok, will send out " + "MSG_SESSION_ESTABLISHED msg."); Message msg = Message.obtain(mCallback); msg.what = BluetoothPbapService.MSG_SESSION_ESTABLISHED; msg.sendToTarget(); return ResponseCodes.OBEX_HTTP_OK; } @Override public void onDisconnect(final HeaderSet req, final HeaderSet resp) { if (D) Log.d(TAG, "onDisconnect(): enter"); if (V) logHeader(req); notifyUpdateWakeLock(); resp.responseCode = ResponseCodes.OBEX_HTTP_OK; if (mCallback != null) { Message msg = Message.obtain(mCallback); msg.what = BluetoothPbapService.MSG_SESSION_DISCONNECTED; msg.sendToTarget(); if (V) Log.v(TAG, "onDisconnect(): msg MSG_SESSION_DISCONNECTED sent out."); } } @Override public int onAbort(HeaderSet request, HeaderSet reply) { if (D) Log.d(TAG, "onAbort(): enter."); notifyUpdateWakeLock(); sIsAborted = true; return ResponseCodes.OBEX_HTTP_OK; } @Override public int onPut(final Operation op) { if (D) Log.d(TAG, "onPut(): not support PUT request."); notifyUpdateWakeLock(); return ResponseCodes.OBEX_HTTP_BAD_REQUEST; } @Override public int onSetPath(final HeaderSet request, final HeaderSet reply, final boolean backup, final boolean create) { if (V) logHeader(request); if (D) Log.d(TAG, "before setPath, mCurrentPath == " + mCurrentPath); notifyUpdateWakeLock(); String current_path_tmp = mCurrentPath; String tmp_path = null; try { tmp_path = (String)request.getHeader(HeaderSet.NAME); } catch (IOException e) { Log.e(TAG, "Get name header fail"); return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR; } if (D) Log.d(TAG, "backup=" + backup + " create=" + create + " name=" + tmp_path); if (backup) { if (current_path_tmp.length() != 0) { current_path_tmp = current_path_tmp.substring(0, current_path_tmp.lastIndexOf("/")); } } else { if (tmp_path == null) { current_path_tmp = ""; } else { current_path_tmp = current_path_tmp + "/" + tmp_path; } } if ((current_path_tmp.length() != 0) && (!isLegalPath(current_path_tmp))) { if (create) { Log.w(TAG, "path create is forbidden!"); return ResponseCodes.OBEX_HTTP_FORBIDDEN; } else { Log.w(TAG, "path is not legal"); return ResponseCodes.OBEX_HTTP_NOT_FOUND; } } mCurrentPath = current_path_tmp; if (V) Log.v(TAG, "after setPath, mCurrentPath == " + mCurrentPath); return ResponseCodes.OBEX_HTTP_OK; } @Override public void onClose() { if (mCallback != null) { Message msg = Message.obtain(mCallback); msg.what = BluetoothPbapService.MSG_SERVERSESSION_CLOSE; msg.sendToTarget(); if (D) Log.d(TAG, "onClose(): msg MSG_SERVERSESSION_CLOSE sent out."); } } @Override public int onGet(Operation op) { notifyUpdateWakeLock(); sIsAborted = false; HeaderSet request = null; HeaderSet reply = new HeaderSet(); String type = ""; String name = ""; byte[] appParam = null; AppParamValue appParamValue = new AppParamValue(); try { request = op.getReceivedHeader(); type = (String)request.getHeader(HeaderSet.TYPE); name = (String)request.getHeader(HeaderSet.NAME); appParam = (byte[])request.getHeader(HeaderSet.APPLICATION_PARAMETER); } catch (IOException e) { Log.e(TAG, "request headers error"); return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR; } if (V) logHeader(request); if (D) Log.d(TAG, "OnGet type is " + type + "; name is " + name); if (type == null) { return ResponseCodes.OBEX_HTTP_NOT_ACCEPTABLE; } // Accroding to specification,the name header could be omitted such as // sony erriccsonHBH-DS980 // For "x-bt/phonebook" and "x-bt/vcard-listing": // if name == null, guess what carkit actually want from current path // For "x-bt/vcard": // We decide which kind of content client would like per current path boolean validName = true; if (TextUtils.isEmpty(name)) { validName = false; } if (!validName || (validName && type.equals(TYPE_VCARD))) { if (D) Log.d(TAG, "Guess what carkit actually want from current path (" + mCurrentPath + ")"); if (mCurrentPath.equals(PB_PATH)) { appParamValue.needTag = ContentType.PHONEBOOK; } else if (mCurrentPath.equals(ICH_PATH)) { appParamValue.needTag = ContentType.INCOMING_CALL_HISTORY; } else if (mCurrentPath.equals(OCH_PATH)) { appParamValue.needTag = ContentType.OUTGOING_CALL_HISTORY; } else if (mCurrentPath.equals(MCH_PATH)) { appParamValue.needTag = ContentType.MISSED_CALL_HISTORY; mNeedNewMissedCallsNum = true; } else if (mCurrentPath.equals(CCH_PATH)) { appParamValue.needTag = ContentType.COMBINED_CALL_HISTORY; } else { Log.w(TAG, "mCurrentpath is not valid path!!!"); return ResponseCodes.OBEX_HTTP_NOT_ACCEPTABLE; } if (D) Log.v(TAG, "onGet(): appParamValue.needTag=" + appParamValue.needTag); } else { // Not support SIM card currently if (name.contains(SIM1.subSequence(0, SIM1.length()))) { Log.w(TAG, "Not support access SIM card info!"); return ResponseCodes.OBEX_HTTP_NOT_ACCEPTABLE; } // we have weak name checking here to provide better // compatibility with other devices,although unique name such as // "pb.vcf" is required by SIG spec. if (name.contains(PB.subSequence(0, PB.length()))) { appParamValue.needTag = ContentType.PHONEBOOK; if (D) Log.v(TAG, "download phonebook request"); } else if (name.contains(ICH.subSequence(0, ICH.length()))) { appParamValue.needTag = ContentType.INCOMING_CALL_HISTORY; if (D) Log.v(TAG, "download incoming calls request"); } else if (name.contains(OCH.subSequence(0, OCH.length()))) { appParamValue.needTag = ContentType.OUTGOING_CALL_HISTORY; if (D) Log.v(TAG, "download outgoing calls request"); } else if (name.contains(MCH.subSequence(0, MCH.length()))) { appParamValue.needTag = ContentType.MISSED_CALL_HISTORY; mNeedNewMissedCallsNum = true; if (D) Log.v(TAG, "download missed calls request"); } else if (name.contains(CCH.subSequence(0, CCH.length()))) { appParamValue.needTag = ContentType.COMBINED_CALL_HISTORY; if (D) Log.v(TAG, "download combined calls request"); } else { Log.w(TAG, "Input name doesn't contain valid info!!!"); return ResponseCodes.OBEX_HTTP_NOT_ACCEPTABLE; } } if ((appParam != null) && !parseApplicationParameter(appParam, appParamValue)) { return ResponseCodes.OBEX_HTTP_BAD_REQUEST; } // listing request if (type.equals(TYPE_LISTING)) { return pullVcardListing(appParam, appParamValue, reply, op); } // pull vcard entry request else if (type.equals(TYPE_VCARD)) { return pullVcardEntry(appParam, appParamValue, op, name, mCurrentPath); } // down load phone book request else if (type.equals(TYPE_PB)) { return pullPhonebook(appParam, appParamValue, reply, op, name); } else { Log.w(TAG, "unknown type request!!!"); return ResponseCodes.OBEX_HTTP_NOT_ACCEPTABLE; } } /** check whether path is legal */ private final boolean isLegalPath(final String str) { if (str.length() == 0) { return true; } for (int i = 0; i < LEGAL_PATH.length; i++) { if (str.equals(LEGAL_PATH[i])) { return true; } } return false; } private class AppParamValue { public int maxListCount; public int listStartOffset; public String searchValue; // Indicate which vCard parameter the search operation shall be carried // out on. Can be "Name | Number | Sound", default value is "Name". public String searchAttr; // Indicate which sorting order shall be used for the // listing object. // Can be "Alphabetical | Indexed | Phonetical", default value is // "Indexed". public String order; public int needTag; public boolean vcard21; public AppParamValue() { maxListCount = 0xFFFF; listStartOffset = 0; searchValue = ""; searchAttr = ""; order = ""; needTag = 0x00; vcard21 = true; } public void dump() { Log.i(TAG, "maxListCount=" + maxListCount + " listStartOffset=" + listStartOffset + " searchValue=" + searchValue + " searchAttr=" + searchAttr + " needTag=" + needTag + " vcard21=" + vcard21 + " order=" + order); } } /** To parse obex application parameter */ private final boolean parseApplicationParameter(final byte[] appParam, AppParamValue appParamValue) { int i = 0; boolean parseOk = true; while (i < appParam.length) { switch (appParam[i]) { case ApplicationParameter.TRIPLET_TAGID.FILTER_TAGID: i += 2; // length and tag field in triplet i += ApplicationParameter.TRIPLET_LENGTH.FILTER_LENGTH; break; case ApplicationParameter.TRIPLET_TAGID.ORDER_TAGID: i += 2; // length and tag field in triplet appParamValue.order = Byte.toString(appParam[i]); i += ApplicationParameter.TRIPLET_LENGTH.ORDER_LENGTH; break; case ApplicationParameter.TRIPLET_TAGID.SEARCH_VALUE_TAGID: i += 1; // length field in triplet // length of search value is variable int length = appParam[i]; if (length == 0) { parseOk = false; break; } if (appParam[i+length] == 0x0) { appParamValue.searchValue = new String(appParam, i + 1, length-1); } else { appParamValue.searchValue = new String(appParam, i + 1, length); } i += length; i += 1; break; case ApplicationParameter.TRIPLET_TAGID.SEARCH_ATTRIBUTE_TAGID: i += 2; appParamValue.searchAttr = Byte.toString(appParam[i]); i += ApplicationParameter.TRIPLET_LENGTH.SEARCH_ATTRIBUTE_LENGTH; break; case ApplicationParameter.TRIPLET_TAGID.MAXLISTCOUNT_TAGID: i += 2; if (appParam[i] == 0 && appParam[i + 1] == 0) { mNeedPhonebookSize = true; } else { int highValue = appParam[i] & 0xff; int lowValue = appParam[i + 1] & 0xff; appParamValue.maxListCount = highValue * 256 + lowValue; } i += ApplicationParameter.TRIPLET_LENGTH.MAXLISTCOUNT_LENGTH; break; case ApplicationParameter.TRIPLET_TAGID.LISTSTARTOFFSET_TAGID: i += 2; int highValue = appParam[i] & 0xff; int lowValue = appParam[i + 1] & 0xff; appParamValue.listStartOffset = highValue * 256 + lowValue; i += ApplicationParameter.TRIPLET_LENGTH.LISTSTARTOFFSET_LENGTH; break; case ApplicationParameter.TRIPLET_TAGID.FORMAT_TAGID: i += 2;// length field in triplet if (appParam[i] != 0) { appParamValue.vcard21 = false; } i += ApplicationParameter.TRIPLET_LENGTH.FORMAT_LENGTH; break; default: parseOk = false; Log.e(TAG, "Parse Application Parameter error"); break; } } if (D) appParamValue.dump(); return parseOk; } /** Form and Send an XML format String to client for Phone book listing */ private final int sendVcardListingXml(final int type, Operation op, final int maxListCount, final int listStartOffset, final String searchValue, String searchAttr) { StringBuilder result = new StringBuilder(); int itemsFound = 0; result.append(""); result.append(""); result.append(""); // Phonebook listing request if (type == ContentType.PHONEBOOK) { if (searchAttr.equals("0")) { // search by name itemsFound = createList(maxListCount, listStartOffset, searchValue, result, "name"); } else if (searchAttr.equals("1")) { // search by number itemsFound = createList(maxListCount, listStartOffset, searchValue, result, "number"); }// end of search by number else { return ResponseCodes.OBEX_HTTP_PRECON_FAILED; } } // Call history listing request else { ArrayList nameList = mVcardManager.loadCallHistoryList(type); int requestSize = nameList.size() >= maxListCount ? maxListCount : nameList.size(); int startPoint = listStartOffset; int endPoint = startPoint + requestSize; if (endPoint > nameList.size()) { endPoint = nameList.size(); } if (D) Log.d(TAG, "call log list, size=" + requestSize + " offset=" + listStartOffset); for (int j = startPoint; j < endPoint; j++) { writeVCardEntry(j+1, nameList.get(j),result); } } result.append(""); if (V) Log.v(TAG, "itemsFound =" + itemsFound); return pushBytes(op, result.toString()); } private int createList(final int maxListCount, final int listStartOffset, final String searchValue, StringBuilder result, String type) { int itemsFound = 0; ArrayList nameList = mVcardManager.getPhonebookNameList(mOrderBy); final int requestSize = nameList.size() >= maxListCount ? maxListCount : nameList.size(); final int listSize = nameList.size(); String compareValue = "", currentValue; if (D) Log.d(TAG, "search by " + type + ", requestSize=" + requestSize + " offset=" + listStartOffset + " searchValue=" + searchValue); if (type.equals("number")) { // query the number, to get the names ArrayList names = mVcardManager.getContactNamesByNumber(searchValue); for (int i = 0; i < names.size(); i++) { compareValue = names.get(i).trim(); if (D) Log.d(TAG, "compareValue=" + compareValue); for (int pos = listStartOffset; pos < listSize && itemsFound < requestSize; pos++) { currentValue = nameList.get(pos); if (D) Log.d(TAG, "currentValue=" + currentValue); if (currentValue.startsWith(compareValue)) { itemsFound++; if (currentValue.contains(",")) currentValue = currentValue.substring(0, currentValue.lastIndexOf(',')); writeVCardEntry(pos, currentValue,result); } } if (itemsFound >= requestSize) { break; } } } else { if (searchValue != null) { compareValue = searchValue.trim(); } for (int pos = listStartOffset; pos < listSize && itemsFound < requestSize; pos++) { currentValue = nameList.get(pos); if (currentValue.contains(",")) currentValue = currentValue.substring(0, currentValue.lastIndexOf(',')); if (searchValue.isEmpty() || ((currentValue.toLowerCase()).equals(compareValue.toLowerCase()))) { itemsFound++; writeVCardEntry(pos, currentValue,result); } } } return itemsFound; } /** * Function to send obex header back to client such as get phonebook size * request */ private final int pushHeader(final Operation op, final HeaderSet reply) { OutputStream outputStream = null; if (D) Log.d(TAG, "Push Header"); if (D) Log.d(TAG, reply.toString()); int pushResult = ResponseCodes.OBEX_HTTP_OK; try { op.sendHeaders(reply); outputStream = op.openOutputStream(); outputStream.flush(); } catch (IOException e) { Log.e(TAG, e.toString()); pushResult = ResponseCodes.OBEX_HTTP_INTERNAL_ERROR; } finally { if (!closeStream(outputStream, op)) { pushResult = ResponseCodes.OBEX_HTTP_INTERNAL_ERROR; } } return pushResult; } /** Function to send vcard data to client */ private final int pushBytes(Operation op, final String vcardString) { if (vcardString == null) { Log.w(TAG, "vcardString is null!"); return ResponseCodes.OBEX_HTTP_OK; } OutputStream outputStream = null; int pushResult = ResponseCodes.OBEX_HTTP_OK; try { outputStream = op.openOutputStream(); outputStream.write(vcardString.getBytes()); if (V) Log.v(TAG, "Send Data complete!"); } catch (IOException e) { Log.e(TAG, "open/write outputstrem failed" + e.toString()); pushResult = ResponseCodes.OBEX_HTTP_INTERNAL_ERROR; } if (!closeStream(outputStream, op)) { pushResult = ResponseCodes.OBEX_HTTP_INTERNAL_ERROR; } return pushResult; } private final int handleAppParaForResponse(AppParamValue appParamValue, int size, HeaderSet reply, Operation op) { byte[] misnum = new byte[1]; ApplicationParameter ap = new ApplicationParameter(); // In such case, PCE only want the number of index. // So response not contain any Body header. if (mNeedPhonebookSize) { if (V) Log.v(TAG, "Need Phonebook size in response header."); mNeedPhonebookSize = false; byte[] pbsize = new byte[2]; pbsize[0] = (byte)((size / 256) & 0xff);// HIGH VALUE pbsize[1] = (byte)((size % 256) & 0xff);// LOW VALUE ap.addAPPHeader(ApplicationParameter.TRIPLET_TAGID.PHONEBOOKSIZE_TAGID, ApplicationParameter.TRIPLET_LENGTH.PHONEBOOKSIZE_LENGTH, pbsize); if (mNeedNewMissedCallsNum) { mNeedNewMissedCallsNum = false; int nmnum = size - mMissedCallSize; mMissedCallSize = size; nmnum = nmnum > 0 ? nmnum : 0; misnum[0] = (byte)nmnum; ap.addAPPHeader(ApplicationParameter.TRIPLET_TAGID.NEWMISSEDCALLS_TAGID, ApplicationParameter.TRIPLET_LENGTH.NEWMISSEDCALLS_LENGTH, misnum); if (D) Log.d(TAG, "handleAppParaForResponse(): mNeedNewMissedCallsNum=true, num= " + nmnum); } reply.setHeader(HeaderSet.APPLICATION_PARAMETER, ap.getAPPparam()); if (D) Log.d(TAG, "Send back Phonebook size only, without body info! Size= " + size); return pushHeader(op, reply); } // Only apply to "mch" download/listing. // NewMissedCalls is used only in the response, together with Body // header. if (mNeedNewMissedCallsNum) { if (V) Log.v(TAG, "Need new missed call num in response header."); mNeedNewMissedCallsNum = false; int nmnum = size - mMissedCallSize; mMissedCallSize = size; nmnum = nmnum > 0 ? nmnum : 0; misnum[0] = (byte)nmnum; ap.addAPPHeader(ApplicationParameter.TRIPLET_TAGID.NEWMISSEDCALLS_TAGID, ApplicationParameter.TRIPLET_LENGTH.NEWMISSEDCALLS_LENGTH, misnum); reply.setHeader(HeaderSet.APPLICATION_PARAMETER, ap.getAPPparam()); if (D) Log.d(TAG, "handleAppParaForResponse(): mNeedNewMissedCallsNum=true, num= " + nmnum); // Only Specifies the headers, not write for now, will write to PCE // together with Body try { op.sendHeaders(reply); } catch (IOException e) { Log.e(TAG, e.toString()); return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR; } } return NEED_SEND_BODY; } private final int pullVcardListing(byte[] appParam, AppParamValue appParamValue, HeaderSet reply, Operation op) { String searchAttr = appParamValue.searchAttr.trim(); if (searchAttr == null || searchAttr.length() == 0) { // If searchAttr is not set by PCE, set default value per spec. appParamValue.searchAttr = "0"; if (D) Log.d(TAG, "searchAttr is not set by PCE, assume search by name by default"); } else if (!searchAttr.equals("0") && !searchAttr.equals("1")) { Log.w(TAG, "search attr not supported"); if (searchAttr.equals("2")) { // search by sound is not supported currently Log.w(TAG, "do not support search by sound"); return ResponseCodes.OBEX_HTTP_NOT_IMPLEMENTED; } return ResponseCodes.OBEX_HTTP_PRECON_FAILED; } else { Log.i(TAG, "searchAttr is valid: " + searchAttr); } int size = mVcardManager.getPhonebookSize(appParamValue.needTag); int needSendBody = handleAppParaForResponse(appParamValue, size, reply, op); if (needSendBody != NEED_SEND_BODY) { return needSendBody; } if (size == 0) { if (V) Log.v(TAG, "PhonebookSize is 0, return."); return ResponseCodes.OBEX_HTTP_OK; } String orderPara = appParamValue.order.trim(); if (TextUtils.isEmpty(orderPara)) { // If order parameter is not set by PCE, set default value per spec. orderPara = "0"; if (D) Log.d(TAG, "Order parameter is not set by PCE. " + "Assume order by 'Indexed' by default"); } else if (!orderPara.equals("0") && !orderPara.equals("1")) { if (V) Log.v(TAG, "Order parameter is not supported: " + appParamValue.order); if (orderPara.equals("2")) { // Order by sound is not supported currently Log.w(TAG, "Do not support order by sound"); return ResponseCodes.OBEX_HTTP_NOT_IMPLEMENTED; } return ResponseCodes.OBEX_HTTP_PRECON_FAILED; } else { Log.i(TAG, "Order parameter is valid: " + orderPara); } if (orderPara.equals("0")) { mOrderBy = ORDER_BY_INDEXED; } else if (orderPara.equals("1")) { mOrderBy = ORDER_BY_ALPHABETICAL; } int sendResult = sendVcardListingXml(appParamValue.needTag, op, appParamValue.maxListCount, appParamValue.listStartOffset, appParamValue.searchValue, appParamValue.searchAttr); return sendResult; } private final int pullVcardEntry(byte[] appParam, AppParamValue appParamValue, Operation op, final String name, final String current_path) { if (name == null || name.length() < VCARD_NAME_SUFFIX_LENGTH) { if (D) Log.d(TAG, "Name is Null, or the length of name < 5 !"); return ResponseCodes.OBEX_HTTP_NOT_ACCEPTABLE; } String strIndex = name.substring(0, name.length() - VCARD_NAME_SUFFIX_LENGTH + 1); int intIndex = 0; if (strIndex.trim().length() != 0) { try { intIndex = Integer.parseInt(strIndex); } catch (NumberFormatException e) { Log.e(TAG, "catch number format exception " + e.toString()); return ResponseCodes.OBEX_HTTP_NOT_ACCEPTABLE; } } int size = mVcardManager.getPhonebookSize(appParamValue.needTag); if (size == 0) { if (V) Log.v(TAG, "PhonebookSize is 0, return."); return ResponseCodes.OBEX_HTTP_NOT_FOUND; } boolean vcard21 = appParamValue.vcard21; if (appParamValue.needTag == 0) { Log.w(TAG, "wrong path!"); return ResponseCodes.OBEX_HTTP_NOT_ACCEPTABLE; } else if (appParamValue.needTag == ContentType.PHONEBOOK) { if (intIndex < 0 || intIndex >= size) { Log.w(TAG, "The requested vcard is not acceptable! name= " + name); return ResponseCodes.OBEX_HTTP_NOT_FOUND; } else if (intIndex == 0) { // For PB_PATH, 0.vcf is the phone number of this phone. String ownerVcard = mVcardManager.getOwnerPhoneNumberVcard(vcard21,null); return pushBytes(op, ownerVcard); } else { return mVcardManager.composeAndSendPhonebookOneVcard(op, intIndex, vcard21, null, mOrderBy ); } } else { if (intIndex <= 0 || intIndex > size) { Log.w(TAG, "The requested vcard is not acceptable! name= " + name); return ResponseCodes.OBEX_HTTP_NOT_FOUND; } // For others (ich/och/cch/mch), 0.vcf is meaningless, and must // begin from 1.vcf if (intIndex >= 1) { return mVcardManager.composeAndSendCallLogVcards(appParamValue.needTag, op, intIndex, intIndex, vcard21); } } return ResponseCodes.OBEX_HTTP_OK; } private final int pullPhonebook(byte[] appParam, AppParamValue appParamValue, HeaderSet reply, Operation op, final String name) { // code start for passing PTS3.2 TC_PSE_PBD_BI_01_C if (name != null) { int dotIndex = name.indexOf("."); String vcf = "vcf"; if (dotIndex >= 0 && dotIndex <= name.length()) { if (name.regionMatches(dotIndex + 1, vcf, 0, vcf.length()) == false) { Log.w(TAG, "name is not .vcf"); return ResponseCodes.OBEX_HTTP_NOT_ACCEPTABLE; } } } // code end for passing PTS3.2 TC_PSE_PBD_BI_01_C int pbSize = mVcardManager.getPhonebookSize(appParamValue.needTag); int needSendBody = handleAppParaForResponse(appParamValue, pbSize, reply, op); if (needSendBody != NEED_SEND_BODY) { return needSendBody; } if (pbSize == 0) { if (V) Log.v(TAG, "PhonebookSize is 0, return."); return ResponseCodes.OBEX_HTTP_OK; } int requestSize = pbSize >= appParamValue.maxListCount ? appParamValue.maxListCount : pbSize; int startPoint = appParamValue.listStartOffset; if (startPoint < 0 || startPoint >= pbSize) { Log.w(TAG, "listStartOffset is not correct! " + startPoint); return ResponseCodes.OBEX_HTTP_OK; } // Limit the number of call log to CALLLOG_NUM_LIMIT if (appParamValue.needTag != BluetoothPbapObexServer.ContentType.PHONEBOOK) { if (requestSize > CALLLOG_NUM_LIMIT) { requestSize = CALLLOG_NUM_LIMIT; } } int endPoint = startPoint + requestSize - 1; if (endPoint > pbSize - 1) { endPoint = pbSize - 1; } if (D) Log.d(TAG, "pullPhonebook(): requestSize=" + requestSize + " startPoint=" + startPoint + " endPoint=" + endPoint); boolean vcard21 = appParamValue.vcard21; if (appParamValue.needTag == BluetoothPbapObexServer.ContentType.PHONEBOOK) { if (startPoint == 0) { String ownerVcard = mVcardManager.getOwnerPhoneNumberVcard(vcard21,null); if (endPoint == 0) { return pushBytes(op, ownerVcard); } else { return mVcardManager.composeAndSendPhonebookVcards(op, 1, endPoint, vcard21, ownerVcard); } } else { return mVcardManager.composeAndSendPhonebookVcards(op, startPoint, endPoint, vcard21, null); } } else { return mVcardManager.composeAndSendCallLogVcards(appParamValue.needTag, op, startPoint + 1, endPoint + 1, vcard21); } } public static boolean closeStream(final OutputStream out, final Operation op) { boolean returnvalue = true; try { if (out != null) { out.close(); } } catch (IOException e) { Log.e(TAG, "outputStream close failed" + e.toString()); returnvalue = false; } try { if (op != null) { op.close(); } } catch (IOException e) { Log.e(TAG, "operation close failed" + e.toString()); returnvalue = false; } return returnvalue; } // Reserved for future use. In case PSE challenge PCE and PCE input wrong // session key. public final void onAuthenticationFailure(final byte[] userName) { } public static final String createSelectionPara(final int type) { String selection = null; switch (type) { case ContentType.INCOMING_CALL_HISTORY: selection = Calls.TYPE + "=" + CallLog.Calls.INCOMING_TYPE; break; case ContentType.OUTGOING_CALL_HISTORY: selection = Calls.TYPE + "=" + CallLog.Calls.OUTGOING_TYPE; break; case ContentType.MISSED_CALL_HISTORY: selection = Calls.TYPE + "=" + CallLog.Calls.MISSED_TYPE; break; default: break; } if (V) Log.v(TAG, "Call log selection: " + selection); return selection; } /** * XML encode special characters in the name field */ private void xmlEncode(String name, StringBuilder result) { if (name == null) { return; } final StringCharacterIterator iterator = new StringCharacterIterator(name); char character = iterator.current(); while (character != CharacterIterator.DONE ){ if (character == '<') { result.append("<"); } else if (character == '>') { result.append(">"); } else if (character == '\"') { result.append("""); } else if (character == '\'') { result.append("'"); } else if (character == '&') { result.append("&"); } else { // The char is not a special one, add it to the result as is result.append(character); } character = iterator.next(); } } private void writeVCardEntry(int vcfIndex, String name, StringBuilder result) { result.append(""); } private void notifyUpdateWakeLock() { Message msg = Message.obtain(mCallback); msg.what = BluetoothPbapService.MSG_ACQUIRE_WAKE_LOCK; msg.sendToTarget(); } public static final void logHeader(HeaderSet hs) { Log.v(TAG, "Dumping HeaderSet " + hs.toString()); try { Log.v(TAG, "COUNT : " + hs.getHeader(HeaderSet.COUNT)); Log.v(TAG, "NAME : " + hs.getHeader(HeaderSet.NAME)); Log.v(TAG, "TYPE : " + hs.getHeader(HeaderSet.TYPE)); Log.v(TAG, "LENGTH : " + hs.getHeader(HeaderSet.LENGTH)); Log.v(TAG, "TIME_ISO_8601 : " + hs.getHeader(HeaderSet.TIME_ISO_8601)); Log.v(TAG, "TIME_4_BYTE : " + hs.getHeader(HeaderSet.TIME_4_BYTE)); Log.v(TAG, "DESCRIPTION : " + hs.getHeader(HeaderSet.DESCRIPTION)); Log.v(TAG, "TARGET : " + hs.getHeader(HeaderSet.TARGET)); Log.v(TAG, "HTTP : " + hs.getHeader(HeaderSet.HTTP)); Log.v(TAG, "WHO : " + hs.getHeader(HeaderSet.WHO)); Log.v(TAG, "OBJECT_CLASS : " + hs.getHeader(HeaderSet.OBJECT_CLASS)); Log.v(TAG, "APPLICATION_PARAMETER : " + hs.getHeader(HeaderSet.APPLICATION_PARAMETER)); } catch (IOException e) { Log.e(TAG, "dump HeaderSet error " + e); } } }