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