BluetoothPbapVcardManager.java revision f7df7a88230c525e2973ca5603f54a77fe47445d
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_PHONE_NUMBER_FORMATTING;
436
437                composer = new VCardComposer(mContext, vcardType, true);
438                composer.addHandler(new HandlerForStringBuffer(op, ownerVCard));
439                if (!composer.init(Contacts.CONTENT_URI, selection, null, null)) {
440                    return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR;
441                }
442
443                while (!composer.isAfterLast()) {
444                    if (BluetoothPbapObexServer.sIsAborted) {
445                        ((ServerOperation)op).isAborted = true;
446                        BluetoothPbapObexServer.sIsAborted = false;
447                        break;
448                    }
449                    if (!composer.createOneEntry()) {
450                        Log.e(TAG, "Failed to read a contact. Error reason: "
451                                + composer.getErrorReason());
452                        return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR;
453                    }
454                }
455            } finally {
456                if (composer != null) {
457                    composer.terminate();
458                }
459            }
460        } else { // CallLog
461            BluetoothPbapCallLogComposer composer = null;
462            try {
463                composer = new BluetoothPbapCallLogComposer(mContext, true);
464                composer.addHandler(new HandlerForStringBuffer(op, ownerVCard));
465                if (!composer.init(CallLog.Calls.CONTENT_URI, selection, null, null)) {
466                    return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR;
467                }
468
469                while (!composer.isAfterLast()) {
470                    if (BluetoothPbapObexServer.sIsAborted) {
471                        ((ServerOperation)op).isAborted = true;
472                        BluetoothPbapObexServer.sIsAborted = false;
473                        break;
474                    }
475                    if (!composer.createOneEntry()) {
476                        Log.e(TAG, "Failed to read a contact. Error reason: "
477                                + composer.getErrorReason());
478                        return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR;
479                    }
480                }
481            } finally {
482                if (composer != null) {
483                    composer.terminate();
484                }
485            }
486        }
487
488        if (V) Log.v(TAG, "Total vcard composing and sending out takes "
489                    + (System.currentTimeMillis() - timestamp) + " ms");
490
491        return ResponseCodes.OBEX_HTTP_OK;
492    }
493
494    /**
495     * Handler to emit VCard String to PCE once size grow to maxPacketSize.
496     */
497    public class HandlerForStringBuffer implements OneEntryHandler {
498        @SuppressWarnings("hiding")
499        private Operation operation;
500
501        private OutputStream outputStream;
502
503        private int maxPacketSize;
504
505        private String phoneOwnVCard = null;
506
507        public HandlerForStringBuffer(Operation op, String ownerVCard) {
508            operation = op;
509            maxPacketSize = operation.getMaxPacketSize();
510            if (V) Log.v(TAG, "getMaxPacketSize() = " + maxPacketSize);
511            if (ownerVCard != null) {
512                phoneOwnVCard = ownerVCard;
513                if (V) Log.v(TAG, "phone own number vcard:");
514                if (V) Log.v(TAG, phoneOwnVCard);
515            }
516        }
517
518        public boolean onInit(Context context) {
519            try {
520                outputStream = operation.openOutputStream();
521                mVcardResults = new StringBuilder();
522                if (phoneOwnVCard != null) {
523                    mVcardResults.append(phoneOwnVCard);
524                }
525            } catch (IOException e) {
526                Log.e(TAG, "open outputstrem failed" + e.toString());
527                return false;
528            }
529            if (V) Log.v(TAG, "openOutputStream() ok.");
530            return true;
531        }
532
533        public boolean onEntryCreated(String vcard) {
534            int vcardLen = vcard.length();
535            if (V) Log.v(TAG, "The length of this vcard is: " + vcardLen);
536
537            mVcardResults.append(vcard);
538            int vcardStringLen = mVcardResults.toString().length();
539            if (V) Log.v(TAG, "The length of this vcardResults is: " + vcardStringLen);
540
541            if (vcardStringLen >= maxPacketSize) {
542                long timestamp = 0;
543                int position = 0;
544
545                // Need while loop to handle the big vcard case
546                while (!BluetoothPbapObexServer.sIsAborted
547                        && position < (vcardStringLen - maxPacketSize)) {
548                    if (V) timestamp = System.currentTimeMillis();
549
550                    String subStr = mVcardResults.toString().substring(position,
551                            position + maxPacketSize);
552                    try {
553                        outputStream.write(subStr.getBytes(), 0, maxPacketSize);
554                    } catch (IOException e) {
555                        Log.e(TAG, "write outputstrem failed" + e.toString());
556                        return false;
557                    }
558                    if (V) Log.v(TAG, "Sending vcard String " + maxPacketSize + " bytes took "
559                            + (System.currentTimeMillis() - timestamp) + " ms");
560
561                    position += maxPacketSize;
562                }
563                mVcardResults.delete(0, position);
564            }
565            return true;
566        }
567
568        public void onTerminate() {
569            // Send out last packet
570            String lastStr = mVcardResults.toString();
571            try {
572                outputStream.write(lastStr.getBytes(), 0, lastStr.length());
573            } catch (IOException e) {
574                Log.e(TAG, "write outputstrem failed" + e.toString());
575            }
576            if (V) Log.v(TAG, "Last packet sent out, sending process complete!");
577
578            if (!BluetoothPbapObexServer.closeStream(outputStream, operation)) {
579                if (V) Log.v(TAG, "CloseStream failed!");
580            } else {
581                if (V) Log.v(TAG, "CloseStream ok!");
582            }
583        }
584    }
585}
586