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