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