• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
33 package com.android.bluetooth.pbap;
34 
35 import com.android.bluetooth.R;
36 
37 import android.net.Uri;
38 import android.os.Handler;
39 import android.text.TextUtils;
40 import android.util.Log;
41 import android.database.Cursor;
42 import android.content.ContentResolver;
43 import android.content.Context;
44 import android.provider.CallLog;
45 import android.provider.CallLog.Calls;
46 import android.provider.ContactsContract.RawContacts;
47 import android.provider.ContactsContract.PhoneLookup;
48 import android.pim.vcard.VCardComposer;
49 import android.pim.vcard.VCardConfig;
50 import android.pim.vcard.VCardComposer.OneEntryHandler;
51 import android.provider.ContactsContract;
52 import android.provider.ContactsContract.CommonDataKinds;
53 import android.provider.ContactsContract.Contacts;
54 import android.provider.ContactsContract.Data;
55 import android.provider.ContactsContract.CommonDataKinds.Phone;
56 
57 import javax.obex.ResponseCodes;
58 import javax.obex.Operation;
59 import java.io.IOException;
60 import java.io.OutputStream;
61 import java.io.Writer;
62 import java.util.ArrayList;
63 
64 public 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 
BluetoothPbapVcardManager(final Context context)112     public BluetoothPbapVcardManager(final Context context) {
113         mContext = context;
114         mResolver = mContext.getContentResolver();
115     }
116 
getOwnerPhoneNumberVcard(final boolean vcardType21)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 
getPhonebookSize(final int type)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 
getContactsSize()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 
getCallHistorySize(final int type)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 
loadCallHistoryList(final int type)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 
getPhonebookNameList(final int orderByWhat)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 
getPhonebookNumberList()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 
composeAndSendCallLogVcards(final int type, final Operation op, final int startPoint, final int endPoint, final boolean vcardType21)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 
composeAndSendPhonebookVcards(final Operation op, final int startPoint, final int endPoint, final boolean vcardType21, String ownerVCard)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 
composeAndSendPhonebookOneVcard(final Operation op, final int offset, final boolean vcardType21, String ownerVCard, int orderByWhat)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 
composeAndSendVCards(final Operation op, final String selection, final boolean vcardType21, String ownerVCard, boolean isContacts)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 
HandlerForStringBuffer(Operation op, String ownerVCard)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 
onInit(Context context)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 
onEntryCreated(String vcard)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 
onTerminate()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