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