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