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