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