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