| /* |
| * 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.ContentResolver; |
| import android.content.Context; |
| import android.database.Cursor; |
| import android.os.Handler; |
| import android.os.Message; |
| import android.os.UserManager; |
| import android.provider.CallLog; |
| import android.provider.CallLog.Calls; |
| import android.text.TextUtils; |
| import android.util.Log; |
| |
| import java.io.IOException; |
| import java.io.OutputStream; |
| import java.nio.ByteBuffer; |
| import java.text.CharacterIterator; |
| import java.text.StringCharacterIterator; |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.Collections; |
| |
| import javax.obex.ApplicationParameter; |
| import javax.obex.HeaderSet; |
| import javax.obex.Operation; |
| import javax.obex.ResponseCodes; |
| import javax.obex.ServerRequestHandler; |
| |
| 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; |
| |
| public static final long INVALID_VALUE_PARAMETER = -1; |
| |
| // 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/fav", |
| "/telecom/ich", |
| "/telecom/och", |
| "/telecom/mch", |
| "/telecom/cch" |
| }; |
| |
| @SuppressWarnings("unused") private static final String[] LEGAL_PATH_WITH_SIM = { |
| "/telecom", |
| "/telecom/pb", |
| "/telecom/fav", |
| "/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"; |
| |
| // favorites |
| private static final String FAV = "fav"; |
| |
| private static final String TELECOM_PATH = "/telecom"; |
| |
| 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"; |
| |
| private static final String FAV_PATH = "/telecom/fav"; |
| |
| // 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 boolean mVcardSelector = false; |
| |
| // 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 final int CALLLOG_NUM_LIMIT = 50; |
| |
| public static final int ORDER_BY_INDEXED = 0; |
| |
| public static final int ORDER_BY_ALPHABETICAL = 1; |
| |
| public static boolean sIsAborted = false; |
| |
| private long mDatabaseIdentifierLow = INVALID_VALUE_PARAMETER; |
| |
| private long mDatabaseIdentifierHigh = INVALID_VALUE_PARAMETER; |
| |
| private long mFolderVersionCounterbitMask = 0x0008; |
| |
| private long mDatabaseIdentifierBitMask = 0x0004; |
| |
| private AppParamValue mConnAppParamValue; |
| |
| private PbapStateMachine mStateMachine; |
| |
| 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 static final int FAVORITES = 6; |
| } |
| |
| public BluetoothPbapObexServer(Handler callback, Context context, |
| PbapStateMachine stateMachine) { |
| super(); |
| mCallback = callback; |
| mContext = context; |
| mVcardManager = new BluetoothPbapVcardManager(mContext); |
| mStateMachine = stateMachine; |
| } |
| |
| @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; |
| } |
| |
| try { |
| byte[] appParam = null; |
| mConnAppParamValue = new AppParamValue(); |
| appParam = (byte[]) request.getHeader(HeaderSet.APPLICATION_PARAMETER); |
| if ((appParam != null) && !parseApplicationParameter(appParam, mConnAppParamValue)) { |
| return ResponseCodes.OBEX_HTTP_BAD_REQUEST; |
| } |
| } 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."); |
| } |
| |
| 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; |
| } |
| |
| @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 onDelete(final HeaderSet request, final HeaderSet reply) { |
| if (D) { |
| Log.d(TAG, "onDelete(): 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 currentPathTmp = mCurrentPath; |
| String tmpPath = null; |
| try { |
| tmpPath = (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=" + tmpPath); |
| } |
| |
| if (backup) { |
| if (currentPathTmp.length() != 0) { |
| currentPathTmp = currentPathTmp.substring(0, currentPathTmp.lastIndexOf("/")); |
| } |
| } else { |
| if (tmpPath == null) { |
| currentPathTmp = ""; |
| } else { |
| currentPathTmp = currentPathTmp + "/" + tmpPath; |
| } |
| } |
| |
| if ((currentPathTmp.length() != 0) && (!isLegalPath(currentPathTmp))) { |
| 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 = currentPathTmp; |
| if (V) { |
| Log.v(TAG, "after setPath, mCurrentPath == " + mCurrentPath); |
| } |
| |
| return ResponseCodes.OBEX_HTTP_OK; |
| } |
| |
| @Override |
| public void onClose() { |
| mStateMachine.sendMessage(PbapStateMachine.DISCONNECT); |
| } |
| |
| @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; |
| } |
| |
| /* TODO: block Get request if contacts are not completely loaded locally */ |
| |
| 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; |
| } |
| |
| if (!UserManager.get(mContext).isUserUnlocked()) { |
| Log.e(TAG, "Storage locked, " + type + " failed"); |
| return ResponseCodes.OBEX_HTTP_UNAVAILABLE; |
| } |
| |
| // 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(FAV_PATH)) { |
| appParamValue.needTag = ContentType.FAVORITES; |
| } 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 if (mCurrentPath.equals(TELECOM_PATH)) { |
| /* PBAP 1.1.1 change */ |
| if (!validName && type.equals(TYPE_LISTING)) { |
| Log.e(TAG, "invalid vcard listing request in default folder"); |
| return ResponseCodes.OBEX_HTTP_NOT_FOUND; |
| } |
| } 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 (isNameMatchTarget(name, PB)) { |
| appParamValue.needTag = ContentType.PHONEBOOK; |
| if (D) { |
| Log.v(TAG, "download phonebook request"); |
| } |
| } else if (isNameMatchTarget(name, FAV)) { |
| appParamValue.needTag = ContentType.FAVORITES; |
| if (D) { |
| Log.v(TAG, "download favorites request"); |
| } |
| } else if (isNameMatchTarget(name, ICH)) { |
| appParamValue.needTag = ContentType.INCOMING_CALL_HISTORY; |
| appParamValue.callHistoryVersionCounter = |
| mVcardManager.getCallHistoryPrimaryFolderVersion( |
| ContentType.INCOMING_CALL_HISTORY); |
| if (D) { |
| Log.v(TAG, "download incoming calls request"); |
| } |
| } else if (isNameMatchTarget(name, OCH)) { |
| appParamValue.needTag = ContentType.OUTGOING_CALL_HISTORY; |
| appParamValue.callHistoryVersionCounter = |
| mVcardManager.getCallHistoryPrimaryFolderVersion( |
| ContentType.OUTGOING_CALL_HISTORY); |
| if (D) { |
| Log.v(TAG, "download outgoing calls request"); |
| } |
| } else if (isNameMatchTarget(name, MCH)) { |
| appParamValue.needTag = ContentType.MISSED_CALL_HISTORY; |
| appParamValue.callHistoryVersionCounter = |
| mVcardManager.getCallHistoryPrimaryFolderVersion( |
| ContentType.MISSED_CALL_HISTORY); |
| mNeedNewMissedCallsNum = true; |
| if (D) { |
| Log.v(TAG, "download missed calls request"); |
| } |
| } else if (isNameMatchTarget(name, CCH)) { |
| appParamValue.needTag = ContentType.COMBINED_CALL_HISTORY; |
| appParamValue.callHistoryVersionCounter = |
| mVcardManager.getCallHistoryPrimaryFolderVersion( |
| 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_FOUND; |
| } |
| } |
| |
| 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, name); |
| } else if (type.equals(TYPE_VCARD)) { |
| // pull vcard entry request |
| return pullVcardEntry(appParam, appParamValue, op, reply, name, mCurrentPath); |
| } else if (type.equals(TYPE_PB)) { |
| // down load phone book request |
| return pullPhonebook(appParam, appParamValue, reply, op, name); |
| } else { |
| Log.w(TAG, "unknown type request!!!"); |
| return ResponseCodes.OBEX_HTTP_NOT_ACCEPTABLE; |
| } |
| } |
| |
| private boolean isNameMatchTarget(String name, String target) { |
| if (name == null) { |
| return false; |
| } |
| String contentTypeName = name; |
| if (contentTypeName.endsWith(".vcf")) { |
| contentTypeName = |
| contentTypeName.substring(0, contentTypeName.length() - ".vcf".length()); |
| } |
| // There is a test case: Client will send a wrong name "/telecom/pbpb". |
| // So we must use the String between '/' and '/' as a indivisible part |
| // for comparing. |
| String[] nameList = contentTypeName.split("/"); |
| for (String subName : nameList) { |
| if (subName.equals(target)) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| /** check whether path is legal */ |
| private 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 |
| // <x-bt/vcard-listing> listing object. |
| // Can be "Alphabetical | Indexed | Phonetical", default value is |
| // "Indexed". |
| public String order; |
| |
| public int needTag; |
| |
| public boolean vcard21; |
| |
| public byte[] propertySelector; |
| |
| public byte[] supportedFeature; |
| |
| public boolean ignorefilter; |
| |
| public byte[] vCardSelector; |
| |
| public String vCardSelectorOperator; |
| |
| public byte[] callHistoryVersionCounter; |
| |
| AppParamValue() { |
| maxListCount = 0xFFFF; |
| listStartOffset = 0; |
| searchValue = ""; |
| searchAttr = ""; |
| order = ""; |
| needTag = 0x00; |
| vcard21 = true; |
| //Filter is not set by default |
| ignorefilter = true; |
| vCardSelectorOperator = "0"; |
| propertySelector = new byte[]{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; |
| vCardSelector = new byte[]{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; |
| supportedFeature = new byte[]{0x00, 0x00, 0x00, 0x00}; |
| } |
| |
| public void dump() { |
| Log.i(TAG, "maxListCount=" + maxListCount + " listStartOffset=" + listStartOffset |
| + " searchValue=" + searchValue + " searchAttr=" + searchAttr + " needTag=" |
| + needTag + " vcard21=" + vcard21 + " order=" + order + "vcardselector=" |
| + vCardSelector + "vcardselop=" + vCardSelectorOperator); |
| } |
| } |
| |
| /** To parse obex application parameter */ |
| private boolean parseApplicationParameter(final byte[] appParam, AppParamValue appParamValue) { |
| int i = 0; |
| boolean parseOk = true; |
| while ((i < appParam.length) && (parseOk)) { |
| switch (appParam[i]) { |
| case ApplicationParameter.TRIPLET_TAGID.PROPERTY_SELECTOR_TAGID: |
| i += 2; // length and tag field in triplet |
| for (int index = 0; |
| index < ApplicationParameter.TRIPLET_LENGTH.PROPERTY_SELECTOR_LENGTH; |
| index++) { |
| if (appParam[i + index] != 0) { |
| appParamValue.ignorefilter = false; |
| appParamValue.propertySelector[index] = appParam[i + index]; |
| } |
| } |
| i += ApplicationParameter.TRIPLET_LENGTH.PROPERTY_SELECTOR_LENGTH; |
| break; |
| case ApplicationParameter.TRIPLET_TAGID.SUPPORTEDFEATURE_TAGID: |
| i += 2; // length and tag field in triplet |
| for (int index = 0; |
| index < ApplicationParameter.TRIPLET_LENGTH.SUPPORTEDFEATURE_LENGTH; |
| index++) { |
| if (appParam[i + index] != 0) { |
| appParamValue.supportedFeature[index] = appParam[i + index]; |
| } |
| } |
| |
| i += ApplicationParameter.TRIPLET_LENGTH.SUPPORTEDFEATURE_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; |
| |
| case ApplicationParameter.TRIPLET_TAGID.VCARDSELECTOR_TAGID: |
| i += 2; |
| for (int index = 0; |
| index < ApplicationParameter.TRIPLET_LENGTH.VCARDSELECTOR_LENGTH; |
| index++) { |
| if (appParam[i + index] != 0) { |
| mVcardSelector = true; |
| appParamValue.vCardSelector[index] = appParam[i + index]; |
| } |
| } |
| i += ApplicationParameter.TRIPLET_LENGTH.VCARDSELECTOR_LENGTH; |
| break; |
| case ApplicationParameter.TRIPLET_TAGID.VCARDSELECTOROPERATOR_TAGID: |
| i += 2; |
| appParamValue.vCardSelectorOperator = Byte.toString(appParam[i]); |
| i += ApplicationParameter.TRIPLET_LENGTH.VCARDSELECTOROPERATOR_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 int sendVcardListingXml(AppParamValue appParamValue, Operation op, int needSendBody, |
| int size) { |
| StringBuilder result = new StringBuilder(); |
| int itemsFound = 0; |
| result.append("<?xml version=\"1.0\"?>"); |
| result.append("<!DOCTYPE vcard-listing SYSTEM \"vcard-listing.dtd\">"); |
| result.append("<vCard-listing version=\"1.0\">"); |
| |
| // Phonebook listing request |
| if ((appParamValue.needTag == ContentType.PHONEBOOK) |
| || (appParamValue.needTag == ContentType.FAVORITES)) { |
| String type = ""; |
| if (appParamValue.searchAttr.equals("0")) { |
| type = "name"; |
| } else if (appParamValue.searchAttr.equals("1")) { |
| type = "number"; |
| } |
| if (type.length() > 0) { |
| itemsFound = createList(appParamValue, needSendBody, size, result, type); |
| } else { |
| return ResponseCodes.OBEX_HTTP_PRECON_FAILED; |
| } |
| } else { |
| // Call history listing request |
| ArrayList<String> nameList = mVcardManager.loadCallHistoryList(appParamValue.needTag); |
| int requestSize = |
| nameList.size() >= appParamValue.maxListCount ? appParamValue.maxListCount |
| : nameList.size(); |
| int startPoint = appParamValue.listStartOffset; |
| int endPoint = startPoint + requestSize; |
| if (endPoint > nameList.size()) { |
| endPoint = nameList.size(); |
| } |
| if (D) { |
| Log.d(TAG, "call log list, size=" + requestSize + " offset=" |
| + appParamValue.listStartOffset); |
| } |
| |
| for (int j = startPoint; j < endPoint; j++) { |
| writeVCardEntry(j + 1, nameList.get(j), result); |
| } |
| } |
| result.append("</vCard-listing>"); |
| |
| if (D) { |
| Log.d(TAG, "itemsFound =" + itemsFound); |
| } |
| |
| return pushBytes(op, result.toString()); |
| } |
| |
| private int createList(AppParamValue appParamValue, int needSendBody, int size, |
| StringBuilder result, String type) { |
| int itemsFound = 0; |
| |
| ArrayList<String> nameList = null; |
| if (mVcardSelector) { |
| nameList = mVcardManager.getSelectedPhonebookNameList(mOrderBy, appParamValue.vcard21, |
| needSendBody, size, appParamValue.vCardSelector, |
| appParamValue.vCardSelectorOperator); |
| } else { |
| nameList = mVcardManager.getPhonebookNameList(mOrderBy); |
| } |
| |
| final int requestSize = |
| nameList.size() >= appParamValue.maxListCount ? appParamValue.maxListCount |
| : nameList.size(); |
| final int listSize = nameList.size(); |
| String compareValue = "", currentValue; |
| |
| if (D) { |
| Log.d(TAG, "search by " + type + ", requestSize=" + requestSize + " offset=" |
| + appParamValue.listStartOffset + " searchValue=" + appParamValue.searchValue); |
| } |
| |
| if (type.equals("number")) { |
| ArrayList<Integer> savedPosList = new ArrayList<>(); |
| ArrayList<String> selectedNameList = new ArrayList<String>(); |
| // query the number, to get the names |
| ArrayList<String> names = |
| mVcardManager.getContactNamesByNumber(appParamValue.searchValue); |
| if (mOrderBy == ORDER_BY_ALPHABETICAL) Collections.sort(names); |
| for (int i = 0; i < names.size(); i++) { |
| compareValue = names.get(i).trim(); |
| if (D) Log.d(TAG, "compareValue=" + compareValue); |
| for (int pos = 0; pos < listSize; pos++) { |
| currentValue = nameList.get(pos); |
| if (V) { |
| Log.d(TAG, "currentValue=" + currentValue); |
| } |
| if (currentValue.equals(compareValue)) { |
| if (currentValue.contains(",")) { |
| currentValue = currentValue.substring(0, currentValue.lastIndexOf(',')); |
| } |
| selectedNameList.add(currentValue); |
| savedPosList.add(pos); |
| } |
| } |
| } |
| |
| for (int j = appParamValue.listStartOffset; |
| j < selectedNameList.size() && itemsFound < requestSize; j++) { |
| itemsFound++; |
| writeVCardEntry(savedPosList.get(j), selectedNameList.get(j), result); |
| } |
| |
| } else { |
| ArrayList<Integer> savedPosList = new ArrayList<>(); |
| ArrayList<String> selectedNameList = new ArrayList<String>(); |
| if (appParamValue.searchValue != null) { |
| compareValue = appParamValue.searchValue.trim().toLowerCase(); |
| } |
| |
| for (int pos = 0; pos < listSize; pos++) { |
| currentValue = nameList.get(pos); |
| |
| if (currentValue.contains(",")) { |
| currentValue = currentValue.substring(0, currentValue.lastIndexOf(',')); |
| } |
| |
| if (appParamValue.searchValue != null) { |
| if (appParamValue.searchValue.isEmpty() |
| || ((currentValue.toLowerCase()) |
| .startsWith(compareValue.toLowerCase()))) { |
| selectedNameList.add(currentValue); |
| savedPosList.add(pos); |
| } |
| } |
| } |
| |
| for (int i = appParamValue.listStartOffset; |
| i < selectedNameList.size() && itemsFound < requestSize; i++) { |
| itemsFound++; |
| writeVCardEntry(savedPosList.get(i), selectedNameList.get(i), result); |
| } |
| } |
| return itemsFound; |
| } |
| |
| /** |
| * Function to send obex header back to client such as get phonebook size |
| * request |
| */ |
| private 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 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 int handleAppParaForResponse(AppParamValue appParamValue, int size, HeaderSet reply, |
| Operation op, String name) { |
| byte[] misnum = new byte[1]; |
| ApplicationParameter ap = new ApplicationParameter(); |
| boolean needSendCallHistoryVersionCounters = false; |
| if (isNameMatchTarget(name, MCH) || isNameMatchTarget(name, ICH) || isNameMatchTarget(name, |
| OCH) || isNameMatchTarget(name, CCH)) { |
| needSendCallHistoryVersionCounters = |
| checkPbapFeatureSupport(mFolderVersionCounterbitMask); |
| } |
| boolean needSendPhonebookVersionCounters = false; |
| if (isNameMatchTarget(name, PB) || isNameMatchTarget(name, FAV)) { |
| needSendPhonebookVersionCounters = |
| checkPbapFeatureSupport(mFolderVersionCounterbitMask); |
| } |
| |
| // In such case, PCE only want the number of index. |
| // So response not contain any Body header. |
| if (mNeedPhonebookSize) { |
| if (D) { |
| Log.d(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 = 0; |
| ContentResolver contentResolver; |
| contentResolver = mContext.getContentResolver(); |
| |
| Cursor c = contentResolver.query(Calls.CONTENT_URI, null, |
| Calls.TYPE + " = " + Calls.MISSED_TYPE + " AND " |
| + android.provider.CallLog.Calls.NEW + " = 1", null, |
| Calls.DEFAULT_SORT_ORDER); |
| |
| if (c != null) { |
| nmnum = c.getCount(); |
| c.close(); |
| } |
| |
| 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); |
| } |
| } |
| |
| if (checkPbapFeatureSupport(mDatabaseIdentifierBitMask)) { |
| setDbCounters(ap); |
| } |
| if (needSendPhonebookVersionCounters) { |
| setFolderVersionCounters(ap); |
| } |
| if (needSendCallHistoryVersionCounters) { |
| setCallversionCounters(ap, appParamValue); |
| } |
| 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 (D) { |
| Log.d(TAG, "Need new missed call num in response header."); |
| } |
| mNeedNewMissedCallsNum = false; |
| int nmnum = 0; |
| ContentResolver contentResolver; |
| contentResolver = mContext.getContentResolver(); |
| |
| Cursor c = contentResolver.query(Calls.CONTENT_URI, null, |
| Calls.TYPE + " = " + Calls.MISSED_TYPE + " AND " |
| + android.provider.CallLog.Calls.NEW + " = 1", null, |
| Calls.DEFAULT_SORT_ORDER); |
| |
| if (c != null) { |
| nmnum = c.getCount(); |
| c.close(); |
| } |
| |
| nmnum = nmnum > 0 ? nmnum : 0; |
| misnum[0] = (byte) nmnum; |
| if (D) { |
| Log.d(TAG, |
| "handleAppParaForResponse(): mNeedNewMissedCallsNum=true, num= " + 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; |
| } |
| } |
| |
| if (checkPbapFeatureSupport(mDatabaseIdentifierBitMask)) { |
| setDbCounters(ap); |
| reply.setHeader(HeaderSet.APPLICATION_PARAMETER, ap.getAPPparam()); |
| try { |
| op.sendHeaders(reply); |
| } catch (IOException e) { |
| Log.e(TAG, e.toString()); |
| return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR; |
| } |
| } |
| |
| if (needSendPhonebookVersionCounters) { |
| setFolderVersionCounters(ap); |
| reply.setHeader(HeaderSet.APPLICATION_PARAMETER, ap.getAPPparam()); |
| try { |
| op.sendHeaders(reply); |
| } catch (IOException e) { |
| Log.e(TAG, e.toString()); |
| return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR; |
| } |
| } |
| |
| if (needSendCallHistoryVersionCounters) { |
| setCallversionCounters(ap, appParamValue); |
| reply.setHeader(HeaderSet.APPLICATION_PARAMETER, ap.getAPPparam()); |
| try { |
| op.sendHeaders(reply); |
| } catch (IOException e) { |
| Log.e(TAG, e.toString()); |
| return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR; |
| } |
| } |
| |
| return NEED_SEND_BODY; |
| } |
| |
| private int pullVcardListing(byte[] appParam, AppParamValue appParamValue, HeaderSet reply, |
| Operation op, String name) { |
| 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, name); |
| if (needSendBody != NEED_SEND_BODY) { |
| op.noBodyHeader(); |
| return needSendBody; |
| } |
| |
| if (size == 0) { |
| if (D) { |
| Log.d(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 (D) { |
| Log.d(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; |
| } |
| |
| return sendVcardListingXml(appParamValue, op, needSendBody, size); |
| } |
| |
| private int pullVcardEntry(byte[] appParam, AppParamValue appParamValue, Operation op, |
| HeaderSet reply, final String name, final String currentPath) { |
| 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); |
| int needSendBody = handleAppParaForResponse(appParamValue, size, reply, op, name); |
| if (size == 0) { |
| if (D) { |
| Log.d(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) |
| || (appParamValue.needTag == ContentType.FAVORITES)) { |
| 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) && (appParamValue.needTag == ContentType.PHONEBOOK)) { |
| // For PB_PATH, 0.vcf is the phone number of this phone. |
| String ownerVcard = mVcardManager.getOwnerPhoneNumberVcard(vcard21, |
| appParamValue.ignorefilter ? null : appParamValue.propertySelector); |
| return pushBytes(op, ownerVcard); |
| } else { |
| return mVcardManager.composeAndSendPhonebookOneVcard(op, intIndex, vcard21, null, |
| mOrderBy, appParamValue.ignorefilter, appParamValue.propertySelector); |
| } |
| } 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.composeAndSendSelectedCallLogVcards(appParamValue.needTag, op, |
| intIndex, intIndex, vcard21, needSendBody, size, appParamValue.ignorefilter, |
| appParamValue.propertySelector, appParamValue.vCardSelector, |
| appParamValue.vCardSelectorOperator, mVcardSelector); |
| } |
| } |
| return ResponseCodes.OBEX_HTTP_OK; |
| } |
| |
| private 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())) { |
| 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, name); |
| if (needSendBody != NEED_SEND_BODY) { |
| op.noBodyHeader(); |
| return needSendBody; |
| } |
| |
| if (pbSize == 0) { |
| if (D) { |
| Log.d(TAG, "PhonebookSize is 0, return."); |
| } |
| return ResponseCodes.OBEX_HTTP_OK; |
| } |
| |
| int requestSize = |
| pbSize >= appParamValue.maxListCount ? appParamValue.maxListCount : pbSize; |
| /** |
| * startIndex (resp., lastIndex) corresponds to the index of the first (resp., last) |
| * vcard entry in the phonebook object. |
| * PBAP v1.2.3: only pb starts indexing at 0.vcf (owner card), the other phonebook |
| * objects (e.g., fav) start at 1.vcf. Additionally, the owner card is included in |
| * pb's pbSize. This means pbSize corresponds to the index of the last vcf in the fav |
| * phonebook object, but does not for the pb phonebook object. |
| */ |
| int startIndex = 1; |
| int lastIndex = pbSize; |
| if (appParamValue.needTag == BluetoothPbapObexServer.ContentType.PHONEBOOK) { |
| startIndex = 0; |
| lastIndex = pbSize - 1; |
| } |
| // [startPoint, endPoint] denote the range of vcf indices to send, inclusive. |
| int startPoint = startIndex + appParamValue.listStartOffset; |
| int endPoint = startPoint + requestSize - 1; |
| if (appParamValue.listStartOffset < 0 || startPoint > lastIndex) { |
| Log.w(TAG, "listStartOffset is not correct! " + startPoint); |
| return ResponseCodes.OBEX_HTTP_OK; |
| } |
| if (endPoint > lastIndex) { |
| endPoint = lastIndex; |
| } |
| |
| // Limit the number of call log to CALLLOG_NUM_LIMIT |
| if ((appParamValue.needTag != BluetoothPbapObexServer.ContentType.PHONEBOOK) |
| && (appParamValue.needTag != BluetoothPbapObexServer.ContentType.FAVORITES)) { |
| if (requestSize > CALLLOG_NUM_LIMIT) { |
| requestSize = CALLLOG_NUM_LIMIT; |
| } |
| } |
| |
| if (D) { |
| Log.d(TAG, "pullPhonebook(): requestSize=" + requestSize + " startPoint=" + startPoint |
| + " endPoint=" + endPoint); |
| } |
| |
| boolean vcard21 = appParamValue.vcard21; |
| boolean favorites = |
| (appParamValue.needTag == BluetoothPbapObexServer.ContentType.FAVORITES); |
| if ((appParamValue.needTag == BluetoothPbapObexServer.ContentType.PHONEBOOK) |
| || favorites) { |
| if (startPoint == 0) { |
| String ownerVcard = mVcardManager.getOwnerPhoneNumberVcard(vcard21, |
| appParamValue.ignorefilter ? null : appParamValue.propertySelector); |
| if (endPoint == 0) { |
| return pushBytes(op, ownerVcard); |
| } else { |
| return mVcardManager.composeAndSendPhonebookVcards(op, 1, endPoint, vcard21, |
| ownerVcard, needSendBody, pbSize, appParamValue.ignorefilter, |
| appParamValue.propertySelector, appParamValue.vCardSelector, |
| appParamValue.vCardSelectorOperator, mVcardSelector, favorites); |
| } |
| } else { |
| return mVcardManager.composeAndSendPhonebookVcards(op, startPoint, endPoint, |
| vcard21, null, needSendBody, pbSize, appParamValue.ignorefilter, |
| appParamValue.propertySelector, appParamValue.vCardSelector, |
| appParamValue.vCardSelectorOperator, mVcardSelector, favorites); |
| } |
| } else { |
| return mVcardManager.composeAndSendSelectedCallLogVcards(appParamValue.needTag, op, |
| startPoint, endPoint, vcard21, needSendBody, pbSize, |
| appParamValue.ignorefilter, appParamValue.propertySelector, |
| appParamValue.vCardSelector, appParamValue.vCardSelectorOperator, |
| mVcardSelector); |
| } |
| } |
| |
| 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. |
| @Override |
| 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 + " OR " + Calls.TYPE |
| + "=" + CallLog.Calls.REJECTED_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("<card handle=\""); |
| result.append(vcfIndex); |
| result.append(".vcf\" name=\""); |
| xmlEncode(name, 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); |
| } |
| } |
| |
| private void setDbCounters(ApplicationParameter ap) { |
| ap.addAPPHeader(ApplicationParameter.TRIPLET_TAGID.DATABASEIDENTIFIER_TAGID, |
| ApplicationParameter.TRIPLET_LENGTH.DATABASEIDENTIFIER_LENGTH, |
| getDatabaseIdentifier()); |
| } |
| |
| private void setFolderVersionCounters(ApplicationParameter ap) { |
| ap.addAPPHeader(ApplicationParameter.TRIPLET_TAGID.PRIMARYVERSIONCOUNTER_TAGID, |
| ApplicationParameter.TRIPLET_LENGTH.PRIMARYVERSIONCOUNTER_LENGTH, |
| getPBPrimaryFolderVersion()); |
| ap.addAPPHeader(ApplicationParameter.TRIPLET_TAGID.SECONDARYVERSIONCOUNTER_TAGID, |
| ApplicationParameter.TRIPLET_LENGTH.SECONDARYVERSIONCOUNTER_LENGTH, |
| getPBSecondaryFolderVersion()); |
| } |
| |
| private void setCallversionCounters(ApplicationParameter ap, AppParamValue appParamValue) { |
| ap.addAPPHeader(ApplicationParameter.TRIPLET_TAGID.PRIMARYVERSIONCOUNTER_TAGID, |
| ApplicationParameter.TRIPLET_LENGTH.PRIMARYVERSIONCOUNTER_LENGTH, |
| appParamValue.callHistoryVersionCounter); |
| |
| ap.addAPPHeader(ApplicationParameter.TRIPLET_TAGID.SECONDARYVERSIONCOUNTER_TAGID, |
| ApplicationParameter.TRIPLET_LENGTH.SECONDARYVERSIONCOUNTER_LENGTH, |
| appParamValue.callHistoryVersionCounter); |
| } |
| |
| private byte[] getDatabaseIdentifier() { |
| mDatabaseIdentifierHigh = 0; |
| mDatabaseIdentifierLow = BluetoothPbapUtils.sDbIdentifier.get(); |
| if (mDatabaseIdentifierLow != INVALID_VALUE_PARAMETER |
| && mDatabaseIdentifierHigh != INVALID_VALUE_PARAMETER) { |
| ByteBuffer ret = ByteBuffer.allocate(16); |
| ret.putLong(mDatabaseIdentifierHigh); |
| ret.putLong(mDatabaseIdentifierLow); |
| return ret.array(); |
| } else { |
| return null; |
| } |
| } |
| |
| private byte[] getPBPrimaryFolderVersion() { |
| long primaryVcMsb = 0; |
| ByteBuffer pvc = ByteBuffer.allocate(16); |
| pvc.putLong(primaryVcMsb); |
| |
| Log.d(TAG, "primaryVersionCounter is " + BluetoothPbapUtils.sPrimaryVersionCounter); |
| pvc.putLong(BluetoothPbapUtils.sPrimaryVersionCounter); |
| return pvc.array(); |
| } |
| |
| private byte[] getPBSecondaryFolderVersion() { |
| long secondaryVcMsb = 0; |
| ByteBuffer svc = ByteBuffer.allocate(16); |
| svc.putLong(secondaryVcMsb); |
| |
| Log.d(TAG, "secondaryVersionCounter is " + BluetoothPbapUtils.sSecondaryVersionCounter); |
| svc.putLong(BluetoothPbapUtils.sSecondaryVersionCounter); |
| return svc.array(); |
| } |
| |
| private boolean checkPbapFeatureSupport(long featureBitMask) { |
| Log.d(TAG, "checkPbapFeatureSupport featureBitMask is " + featureBitMask); |
| return ((ByteBuffer.wrap(mConnAppParamValue.supportedFeature).getInt() & featureBitMask) |
| != 0); |
| } |
| } |