BluetoothPbapVcardManager.java revision 2b36e1731eb5ed784abc1a374eb69d8523123df1
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 com.android.bluetooth.R;
36
37import android.net.Uri;
38import android.os.Handler;
39import android.text.TextUtils;
40import android.util.Log;
41import android.database.Cursor;
42import android.content.ContentResolver;
43import android.content.Context;
44import android.provider.CallLog;
45import android.provider.CallLog.Calls;
46import android.provider.ContactsContract.RawContacts;
47import android.provider.ContactsContract.PhoneLookup;
48import android.pim.vcard.VCardComposer;
49import android.pim.vcard.VCardConfig;
50import android.pim.vcard.VCardComposer.OneEntryHandler;
51import android.provider.ContactsContract;
52import android.provider.ContactsContract.CommonDataKinds;
53import android.provider.ContactsContract.Contacts;
54import android.provider.ContactsContract.Data;
55import android.provider.ContactsContract.CommonDataKinds.Phone;
56
57import javax.obex.ResponseCodes;
58import javax.obex.Operation;
59import java.io.IOException;
60import java.io.OutputStream;
61import java.io.Writer;
62import java.util.ArrayList;
63
64public class BluetoothPbapVcardManager {
65    private static final String TAG = "BluetoothPbapVcardManager";
66
67    private static final boolean V = BluetoothPbapService.VERBOSE;
68
69    private ContentResolver mResolver;
70
71    private Context mContext;
72
73    private StringBuilder mVcardResults = null;
74
75    static final String[] PHONES_PROJECTION = new String[] {
76            Data._ID, // 0
77            CommonDataKinds.Phone.TYPE, // 1
78            CommonDataKinds.Phone.LABEL, // 2
79            CommonDataKinds.Phone.NUMBER, // 3
80            Contacts.DISPLAY_NAME, // 4
81    };
82
83    private static final int ID_COLUMN_INDEX = 0;
84
85    private static final int PHONE_TYPE_COLUMN_INDEX = 1;
86
87    private static final int PHONE_LABEL_COLUMN_INDEX = 2;
88
89    private static final int PHOEN_NUMBER_COLUMN_INDEX = 3;
90
91    private static final int CONTACTS_DISPLAY_NAME_COLUMN_INDEX = 4;
92
93    static final String SORT_ORDER_PHONE_NUMBER = CommonDataKinds.Phone.NUMBER + " ASC";
94
95    static final String[] CONTACTS_PROJECTION = new String[] {
96            Contacts._ID, // 0
97            Contacts.DISPLAY_NAME, // 1
98    };
99
100    static final int CONTACTS_ID_COLUMN_INDEX = 0;
101
102    static final int CONTACTS_NAME_COLUMN_INDEX = 1;
103
104    // call histories use dynamic handles, and handles should order by date; the
105    // most recently one should be the first handle. In table "calls", _id and
106    // date are consistent in ordering, to implement simply, we sort by _id
107    // here.
108    static final String CALLLOG_SORT_ORDER = Calls._ID + " DESC";
109
110    public BluetoothPbapVcardManager(final Context context) {
111        mContext = context;
112        mResolver = mContext.getContentResolver();
113    }
114
115    public final String getOwnerPhoneNumberVcard(final boolean vcardType21) {
116        VCardComposer composer = new VCardComposer(mContext);
117        String name = BluetoothPbapService.getLocalPhoneName();
118        String number = BluetoothPbapService.getLocalPhoneNum();
119        String vcard = composer.composeVCardForPhoneOwnNumber(Phone.TYPE_MOBILE, name, number,
120                vcardType21);
121        return vcard;
122    }
123
124    public final int getPhonebookSize(final int type) {
125        int size;
126        switch (type) {
127            case BluetoothPbapObexServer.ContentType.PHONEBOOK:
128                size = getContactsSize();
129                break;
130            default:
131                size = getCallHistorySize(type);
132                break;
133        }
134        if (V) Log.v(TAG, "getPhonebookSzie size = " + size + " type = " + type);
135        return size;
136    }
137
138    public final int getContactsSize() {
139        final Uri myUri = Contacts.CONTENT_URI;
140        int size = 0;
141        Cursor contactCursor = null;
142        try {
143            contactCursor = mResolver.query(myUri, null, null, null, null);
144            if (contactCursor != null) {
145                size = contactCursor.getCount() + 1; // always has the 0.vcf
146            }
147        } finally {
148            if (contactCursor != null) {
149                contactCursor.close();
150            }
151        }
152        return size;
153    }
154
155    public final int getCallHistorySize(final int type) {
156        final Uri myUri = CallLog.Calls.CONTENT_URI;
157        String selection = BluetoothPbapObexServer.createSelectionPara(type);
158        int size = 0;
159        Cursor callCursor = null;
160        try {
161            callCursor = mResolver.query(myUri, null, selection, null,
162                    CallLog.Calls.DEFAULT_SORT_ORDER);
163            if (callCursor != null) {
164                size = callCursor.getCount();
165            }
166        } finally {
167            if (callCursor != null) {
168                callCursor.close();
169            }
170        }
171        return size;
172    }
173
174    public final ArrayList<String> loadCallHistoryList(final int type) {
175        final Uri myUri = CallLog.Calls.CONTENT_URI;
176        String selection = BluetoothPbapObexServer.createSelectionPara(type);
177        String[] projection = new String[] {
178                Calls.NUMBER, Calls.CACHED_NAME
179        };
180        final int CALLS_NUMBER_COLUMN_INDEX = 0;
181        final int CALLS_NAME_COLUMN_INDEX = 1;
182
183        Cursor callCursor = null;
184        ArrayList<String> list = new ArrayList<String>();
185        try {
186            callCursor = mResolver.query(myUri, projection, selection, null,
187                    CALLLOG_SORT_ORDER);
188            if (callCursor != null) {
189                for (callCursor.moveToFirst(); !callCursor.isAfterLast();
190                        callCursor.moveToNext()) {
191                    String name = callCursor.getString(CALLS_NAME_COLUMN_INDEX);
192                    if (TextUtils.isEmpty(name)) {
193                        // name not found,use number instead
194                        name = callCursor.getString(CALLS_NUMBER_COLUMN_INDEX);
195                    }
196                    list.add(name);
197                }
198            }
199        } finally {
200            if (callCursor != null) {
201                callCursor.close();
202            }
203        }
204        return list;
205    }
206
207    public final ArrayList<String> getPhonebookNameList(final int orderByWhat) {
208        ArrayList<String> nameList = new ArrayList<String>();
209        nameList.add(BluetoothPbapService.getLocalPhoneName());
210
211        final Uri myUri = Contacts.CONTENT_URI;
212        Cursor contactCursor = null;
213        try {
214            if (orderByWhat == BluetoothPbapObexServer.ORDER_BY_INDEXED) {
215                contactCursor = mResolver.query(myUri, CONTACTS_PROJECTION, null, null,
216                        Contacts._ID);
217            } else if (orderByWhat == BluetoothPbapObexServer.ORDER_BY_ALPHABETICAL) {
218                contactCursor = mResolver.query(myUri, CONTACTS_PROJECTION, null, null,
219                        Contacts.DISPLAY_NAME);
220            }
221            if (contactCursor != null) {
222                for (contactCursor.moveToFirst(); !contactCursor.isAfterLast(); contactCursor
223                        .moveToNext()) {
224                    String name = contactCursor.getString(CONTACTS_NAME_COLUMN_INDEX);
225                    if (TextUtils.isEmpty(name)) {
226                        name = mContext.getString(android.R.string.unknownName);
227                    }
228                    nameList.add(name);
229                }
230            }
231        } finally {
232            if (contactCursor != null) {
233                contactCursor.close();
234            }
235        }
236        return nameList;
237    }
238
239    public final ArrayList<String> getPhonebookNumberList() {
240        ArrayList<String> numberList = new ArrayList<String>();
241        numberList.add(BluetoothPbapService.getLocalPhoneNum());
242
243        final Uri myUri = Phone.CONTENT_URI;
244        Cursor phoneCursor = null;
245        try {
246            phoneCursor = mResolver.query(myUri, PHONES_PROJECTION, null, null,
247                    SORT_ORDER_PHONE_NUMBER);
248            if (phoneCursor != null) {
249                for (phoneCursor.moveToFirst(); !phoneCursor.isAfterLast(); phoneCursor
250                        .moveToNext()) {
251                    String number = phoneCursor.getString(PHOEN_NUMBER_COLUMN_INDEX);
252                    if (TextUtils.isEmpty(number)) {
253                        number = mContext.getString(R.string.defaultnumber);
254                    }
255                    numberList.add(number);
256                }
257            }
258        } finally {
259            if (phoneCursor != null) {
260                phoneCursor.close();
261            }
262        }
263        return numberList;
264    }
265
266    public final int composeAndSendCallLogVcards(final int type, final Operation op,
267            final int startPoint, final int endPoint, final boolean vcardType21) {
268        if (startPoint < 1 || startPoint > endPoint) {
269            Log.e(TAG, "internal error: startPoint or endPoint is not correct.");
270            return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR;
271        }
272        String typeSelection = BluetoothPbapObexServer.createSelectionPara(type);
273
274        final Uri myUri = CallLog.Calls.CONTENT_URI;
275        final String[] CALLLOG_PROJECTION = new String[] {
276            CallLog.Calls._ID, // 0
277        };
278        final int ID_COLUMN_INDEX = 0;
279
280        Cursor callsCursor = null;
281        long startPointId = 0;
282        long endPointId = 0;
283        try {
284            // Need test to see if order by _ID is ok here, or by date?
285            callsCursor = mResolver.query(myUri, CALLLOG_PROJECTION, typeSelection, null,
286                    CALLLOG_SORT_ORDER);
287            if (callsCursor != null) {
288                callsCursor.moveToPosition(startPoint - 1);
289                startPointId = callsCursor.getLong(ID_COLUMN_INDEX);
290                if (V) Log.v(TAG, "Call Log query startPointId = " + startPointId);
291                if (startPoint == endPoint) {
292                    endPointId = startPointId;
293                } else {
294                    callsCursor.moveToPosition(endPoint - 1);
295                    endPointId = callsCursor.getLong(ID_COLUMN_INDEX);
296                }
297                if (V) Log.v(TAG, "Call log query endPointId = " + endPointId);
298            }
299        } finally {
300            if (callsCursor != null) {
301                callsCursor.close();
302            }
303        }
304
305        String recordSelection;
306        if (startPoint == endPoint) {
307            recordSelection = Calls._ID + "=" + startPointId;
308        } else {
309            // The query to call table is by "_id DESC" order, so change
310            // correspondingly.
311            recordSelection = Calls._ID + ">=" + endPointId + " AND " + Calls._ID + "<="
312                    + startPointId;
313        }
314
315        String selection;
316        if (typeSelection == null) {
317            selection = recordSelection;
318        } else {
319            selection = "(" + typeSelection + ") AND (" + recordSelection + ")";
320        }
321
322        if (V) Log.v(TAG, "Call log query selection is: " + selection);
323
324        return composeAndSendVCards(op, selection, vcardType21, null, false);
325    }
326
327    public final int composeAndSendPhonebookVcards(final Operation op, final int startPoint,
328            final int endPoint, final boolean vcardType21, String ownerVCard) {
329        if (startPoint < 1 || startPoint > endPoint) {
330            Log.e(TAG, "internal error: startPoint or endPoint is not correct.");
331            return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR;
332        }
333        final Uri myUri = Contacts.CONTENT_URI;
334
335        Cursor contactCursor = null;
336        long startPointId = 0;
337        long endPointId = 0;
338        try {
339            contactCursor = mResolver.query(myUri, CONTACTS_PROJECTION, null, null,
340                    Contacts._ID);
341            if (contactCursor != null) {
342                contactCursor.moveToPosition(startPoint - 1);
343                startPointId = contactCursor.getLong(CONTACTS_ID_COLUMN_INDEX);
344                if (V) Log.v(TAG, "Query startPointId = " + startPointId);
345                if (startPoint == endPoint) {
346                    endPointId = startPointId;
347                } else {
348                    contactCursor.moveToPosition(endPoint - 1);
349                    endPointId = contactCursor.getLong(CONTACTS_ID_COLUMN_INDEX);
350                }
351                if (V) Log.v(TAG, "Query endPointId = " + endPointId);
352            }
353        } finally {
354            if (contactCursor != null) {
355                contactCursor.close();
356            }
357        }
358
359        final String selection;
360        if (startPoint == endPoint) {
361            selection = Contacts._ID + "=" + startPointId;
362        } else {
363            selection = Contacts._ID + ">=" + startPointId + " AND " + Contacts._ID + "<="
364                    + endPointId;
365        }
366
367        if (V) Log.v(TAG, "Query selection is: " + selection);
368
369        return composeAndSendVCards(op, selection, vcardType21, ownerVCard, true);
370    }
371
372    public final int composeAndSendPhonebookOneVcard(final Operation op, final int offset,
373            final boolean vcardType21, String ownerVCard, int orderByWhat) {
374        if (offset < 1) {
375            Log.e(TAG, "Internal error: offset is not correct.");
376            return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR;
377        }
378        final Uri myUri = Contacts.CONTENT_URI;
379        Cursor contactCursor = null;
380        String selection = null;
381        long contactId = 0;
382        if (orderByWhat == BluetoothPbapObexServer.ORDER_BY_INDEXED) {
383            try {
384                contactCursor = mResolver.query(myUri, CONTACTS_PROJECTION, null, null,
385                        Contacts._ID);
386                if (contactCursor != null) {
387                    contactCursor.moveToPosition(offset - 1);
388                    contactId = contactCursor.getLong(CONTACTS_ID_COLUMN_INDEX);
389                    if (V) Log.v(TAG, "Query startPointId = " + contactId);
390                }
391            } finally {
392                if (contactCursor != null) {
393                    contactCursor.close();
394                }
395            }
396        } else if (orderByWhat == BluetoothPbapObexServer.ORDER_BY_ALPHABETICAL) {
397            try {
398                contactCursor = mResolver.query(myUri, CONTACTS_PROJECTION, null, null,
399                        Contacts.DISPLAY_NAME);
400                if (contactCursor != null) {
401                    contactCursor.moveToPosition(offset - 1);
402                    contactId = contactCursor.getLong(CONTACTS_ID_COLUMN_INDEX);
403                    if (V) Log.v(TAG, "Query startPointId = " + contactId);
404                }
405            } finally {
406                if (contactCursor != null) {
407                    contactCursor.close();
408                }
409            }
410        } else {
411            Log.e(TAG, "Parameter orderByWhat is not supported!");
412            return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR;
413        }
414        selection = Contacts._ID + "=" + contactId;
415
416        if (V) Log.v(TAG, "Query selection is: " + selection);
417
418        return composeAndSendVCards(op, selection, vcardType21, ownerVCard, true);
419    }
420
421    public final int composeAndSendVCards(final Operation op, final String selection,
422            final boolean vcardType21, String ownerVCard, boolean isContacts) {
423        long timestamp = 0;
424        if (V) timestamp = System.currentTimeMillis();
425
426        VCardComposer composer = null;
427        try {
428            // Currently only support Generic Vcard 2.1 and 3.0
429            final int vcardType;
430            if (vcardType21) {
431                vcardType = VCardConfig.VCARD_TYPE_V21_GENERIC;
432            } else {
433                vcardType = VCardConfig.VCARD_TYPE_V30_GENERIC;
434            }
435
436            final boolean careHandlerErrors = true;
437            final boolean needPhoto = false; //We disable photo for the time being
438            final boolean isCallLogComposer = !isContacts;
439
440            composer = new VCardComposer(mContext, vcardType, careHandlerErrors, isCallLogComposer,
441                                         needPhoto);
442
443            composer.addHandler(new HandlerForStringBuffer(op, ownerVCard));
444
445            if (!composer.init(selection, null)) {
446                return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR;
447            }
448
449            while (!composer.isAfterLast()) {
450                if (!composer.createOneEntry()) {
451                    Log.e(TAG, "Failed to read a contact. Error reason: "
452                            + composer.getErrorReason());
453                    return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR;
454                }
455            }
456        } finally {
457            if (composer != null) {
458                composer.terminate();
459            }
460        }
461
462        if (V) Log.v(TAG, "Total vcard composing and sending out takes "
463                    + (System.currentTimeMillis() - timestamp) + " ms");
464
465        return ResponseCodes.OBEX_HTTP_OK;
466    }
467
468    /**
469     * Handler to emit VCard String to PCE once size grow to maxPacketSize.
470     */
471    public class HandlerForStringBuffer implements OneEntryHandler {
472        @SuppressWarnings("hiding")
473        private Operation operation;
474
475        private OutputStream outputStream;
476
477        private int maxPacketSize;
478
479        private String phoneOwnVCard = null;
480
481        public HandlerForStringBuffer(Operation op, String ownerVCard) {
482            operation = op;
483            maxPacketSize = operation.getMaxPacketSize();
484            if (V) Log.v(TAG, "getMaxPacketSize() = " + maxPacketSize);
485            if (ownerVCard != null) {
486                phoneOwnVCard = ownerVCard;
487                if (V) Log.v(TAG, "phone own number vcard:");
488                if (V) Log.v(TAG, phoneOwnVCard);
489            }
490        }
491
492        public boolean onInit(Context context) {
493            try {
494                outputStream = operation.openOutputStream();
495                mVcardResults = new StringBuilder();
496                if (phoneOwnVCard != null) {
497                    mVcardResults.append(phoneOwnVCard);
498                }
499            } catch (IOException e) {
500                Log.e(TAG, "open outputstrem failed" + e.toString());
501                return false;
502            }
503            if (V) Log.v(TAG, "openOutputStream() ok.");
504            return true;
505        }
506
507        public boolean onEntryCreated(String vcard) {
508            int vcardLen = vcard.length();
509            if (V) Log.v(TAG, "The length of this vcard is: " + vcardLen);
510
511            mVcardResults.append(vcard);
512            int vcardStringLen = mVcardResults.toString().length();
513            if (V) Log.v(TAG, "The length of this vcardResults is: " + vcardStringLen);
514
515            if (vcardStringLen >= maxPacketSize) {
516                long timestamp = 0;
517                int position = 0;
518
519                // Need while loop to handle the big vcard case
520                while (position < (vcardStringLen - maxPacketSize)) {
521                    if (V) timestamp = System.currentTimeMillis();
522
523                    String subStr = mVcardResults.toString().substring(position,
524                            position + maxPacketSize);
525                    try {
526                        outputStream.write(subStr.getBytes(), 0, maxPacketSize);
527                    } catch (IOException e) {
528                        Log.e(TAG, "write outputstrem failed" + e.toString());
529                        return false;
530                    }
531                    if (V) Log.v(TAG, "Sending vcard String " + maxPacketSize + " bytes took "
532                            + (System.currentTimeMillis() - timestamp) + " ms");
533
534                    position += maxPacketSize;
535                }
536                mVcardResults.delete(0, position);
537            }
538            return true;
539        }
540
541        public void onTerminate() {
542            // Send out last packet
543            String lastStr = mVcardResults.toString();
544            try {
545                outputStream.write(lastStr.getBytes(), 0, lastStr.length());
546            } catch (IOException e) {
547                Log.e(TAG, "write outputstrem failed" + e.toString());
548            }
549            if (V) Log.v(TAG, "Last packet sent out, sending process complete!");
550
551            if (!BluetoothPbapObexServer.closeStream(outputStream, operation)) {
552                if (V) Log.v(TAG, "CloseStream failed!");
553            } else {
554                if (V) Log.v(TAG, "CloseStream ok!");
555            }
556        }
557    }
558}
559