• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2 * Copyright (c) 2015, The Linux Foundation. All rights reserved.
3 * Copyright (C) 2014 Samsung System LSI
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16 package com.android.bluetooth.pbap;
17 
18 import android.content.ContentResolver;
19 import android.content.ContentValues;
20 import android.content.Context;
21 import android.database.Cursor;
22 import android.database.sqlite.SQLiteException;
23 import android.net.Uri;
24 import android.provider.ContactsContract.CommonDataKinds;
25 import android.provider.ContactsContract.CommonDataKinds.Phone;
26 import android.provider.ContactsContract.CommonDataKinds.StructuredName;
27 import android.provider.ContactsContract.Contacts;
28 import android.text.TextUtils;
29 import android.util.Log;
30 
31 import com.android.bluetooth.BluetoothMethodProxy;
32 import com.android.bluetooth.R;
33 import com.android.internal.annotations.VisibleForTesting;
34 import com.android.obex.Operation;
35 import com.android.obex.ResponseCodes;
36 import com.android.obex.ServerOperation;
37 import com.android.vcard.VCardBuilder;
38 import com.android.vcard.VCardConfig;
39 import com.android.vcard.VCardUtils;
40 
41 import java.util.ArrayList;
42 import java.util.Collections;
43 import java.util.Comparator;
44 import java.util.List;
45 
46 /**
47  * VCard composer especially for Call Log used in Bluetooth.
48  */
49 public class BluetoothPbapSimVcardManager {
50     private static final String TAG = "PbapSIMvCardComposer";
51 
52     private static final boolean V = BluetoothPbapService.VERBOSE;
53 
54     @VisibleForTesting
55     public static final String FAILURE_REASON_FAILED_TO_GET_DATABASE_INFO =
56         "Failed to get database information";
57 
58     @VisibleForTesting
59     public static final String FAILURE_REASON_NO_ENTRY =
60         "There's no exportable in the database";
61 
62     @VisibleForTesting
63     public static final String FAILURE_REASON_NOT_INITIALIZED =
64         "The vCard composer object is not correctly initialized";
65 
66     /** Should be visible only from developers... (no need to translate, hopefully) */
67     @VisibleForTesting
68     public static final String FAILURE_REASON_UNSUPPORTED_URI =
69         "The Uri vCard composer received is not supported by the composer.";
70 
71     @VisibleForTesting
72     public static final String NO_ERROR = "No error";
73 
74     @VisibleForTesting
75     public static final Uri SIM_URI = Uri.parse("content://icc/adn");
76 
77     @VisibleForTesting
78     public static final String SIM_PATH = "/SIM1/telecom";
79 
80     private static final String[] SIM_PROJECTION = new String[] {
81         Contacts.DISPLAY_NAME,
82         CommonDataKinds.Phone.NUMBER,
83         CommonDataKinds.Phone.TYPE,
84         CommonDataKinds.Phone.LABEL
85     };
86 
87     @VisibleForTesting
88     public static final int NAME_COLUMN_INDEX = 0;
89     @VisibleForTesting
90     public static final int NUMBER_COLUMN_INDEX = 1;
91     private static final int NUMBERTYPE_COLUMN_INDEX = 2;
92     private static final int NUMBERLABEL_COLUMN_INDEX = 3;
93 
94 
95     private final Context mContext;
96     private ContentResolver mContentResolver;
97     private Cursor mCursor;
98     private boolean mTerminateIsCalled;
99     private String mErrorReason = NO_ERROR;
100 
BluetoothPbapSimVcardManager(final Context context)101     public BluetoothPbapSimVcardManager(final Context context) {
102         mContext = context;
103         mContentResolver = context.getContentResolver();
104     }
105 
init(final Uri contentUri, final String selection, final String[] selectionArgs, final String sortOrder)106     public boolean init(final Uri contentUri, final String selection,
107             final String[] selectionArgs, final String sortOrder) {
108         if (!SIM_URI.equals(contentUri)) {
109             mErrorReason = FAILURE_REASON_UNSUPPORTED_URI;
110             return false;
111         }
112 
113         //checkpoint Figure out if we can apply selection, projection and sort order.
114         mCursor = BluetoothMethodProxy.getInstance().contentResolverQuery(mContentResolver,
115                 contentUri, SIM_PROJECTION, null, null, sortOrder);
116 
117         if (mCursor == null) {
118             mErrorReason = FAILURE_REASON_FAILED_TO_GET_DATABASE_INFO;
119             return false;
120         }
121         if (mCursor.getCount() == 0 || !mCursor.moveToFirst()) {
122             try {
123                 mCursor.close();
124             } catch (SQLiteException e) {
125                 Log.e(TAG, "SQLiteException on Cursor#close(): " + e.getMessage());
126             } finally {
127                 mErrorReason = FAILURE_REASON_NO_ENTRY;
128                 mCursor = null;
129             }
130             return false;
131         }
132         return true;
133     }
134 
createOneEntry(boolean vcardVer21)135     public String createOneEntry(boolean vcardVer21) {
136         if (mCursor == null || mCursor.isAfterLast()) {
137             mErrorReason = FAILURE_REASON_NOT_INITIALIZED;
138             return null;
139         }
140         try {
141             return createOnevCardEntryInternal(vcardVer21);
142         } finally {
143             mCursor.moveToNext();
144         }
145     }
146 
createOnevCardEntryInternal(boolean vcardVer21)147     private String createOnevCardEntryInternal(boolean vcardVer21) {
148         final int vcardType = (vcardVer21 ? VCardConfig.VCARD_TYPE_V21_GENERIC :
149                 VCardConfig.VCARD_TYPE_V30_GENERIC) |
150                 VCardConfig.FLAG_REFRAIN_PHONE_NUMBER_FORMATTING;
151         final VCardBuilder builder = new VCardBuilder(vcardType);
152         String name = mCursor.getString(NAME_COLUMN_INDEX);
153         if (TextUtils.isEmpty(name)) {
154             name = mCursor.getString(NUMBER_COLUMN_INDEX);
155         }
156         final boolean needCharset = !(VCardUtils.containsOnlyPrintableAscii(name));
157         // Create ContentValues for making name as Structured name
158         List<ContentValues> contentValuesList = new ArrayList<ContentValues>();
159         ContentValues nameContentValues = new ContentValues();
160         nameContentValues.put(StructuredName.DISPLAY_NAME, name);
161         contentValuesList.add(nameContentValues);
162         builder.appendNameProperties(contentValuesList);
163 
164         String number = mCursor.getString(NUMBER_COLUMN_INDEX);
165         if (TextUtils.isEmpty(number)) {
166             // To avoid Spec violation and IOT issues, initialize with invalid number
167             number = "000000";
168         }
169         if (number.equals("-1")) {
170             number = mContext.getString(R.string.unknownNumber);
171         }
172 
173         // checkpoint Figure out what are the type and label
174         int type = mCursor.getInt(NUMBERTYPE_COLUMN_INDEX);
175         String label = mCursor.getString(NUMBERLABEL_COLUMN_INDEX);
176         if (type == 0) { // value for type is not present in db
177             type = Phone.TYPE_MOBILE;
178         }
179         if (TextUtils.isEmpty(label)) {
180             label = Integer.toString(type);
181         }
182         builder.appendTelLine(type, label, number, false);
183         return builder.toString();
184     }
185 
terminate()186     public void terminate() {
187         if (mCursor != null) {
188             try {
189                 mCursor.close();
190             } catch (SQLiteException e) {
191                 Log.e(TAG, "SQLiteException on Cursor#close(): " + e.getMessage());
192             }
193             mCursor = null;
194         }
195 
196         mTerminateIsCalled = true;
197     }
198 
199     @Override
finalize()200     public void finalize() {
201         if (!mTerminateIsCalled) {
202             terminate();
203         }
204     }
205 
getCount()206     public int getCount() {
207         if (mCursor == null) {
208             return 0;
209         }
210         return mCursor.getCount();
211     }
212 
isAfterLast()213     public boolean isAfterLast() {
214         if (mCursor == null) {
215             return false;
216         }
217         return mCursor.isAfterLast();
218     }
219 
moveToPosition(final int position, boolean sortalpha)220     public void moveToPosition(final int position, boolean sortalpha) {
221         if(mCursor == null) {
222             return;
223         }
224         if(sortalpha) {
225             setPositionByAlpha(position);
226             return;
227         }
228         mCursor.moveToPosition(position);
229     }
230 
getErrorReason()231     public String getErrorReason() {
232         return mErrorReason;
233     }
234 
setPositionByAlpha(int position)235     private void setPositionByAlpha(int position) {
236         if(mCursor == null) {
237             return;
238         }
239         ArrayList<String> nameList = new ArrayList<String>();
240         for (mCursor.moveToFirst(); !mCursor.isAfterLast(); mCursor
241                         .moveToNext()) {
242             String name = mCursor.getString(NAME_COLUMN_INDEX);
243             if (TextUtils.isEmpty(name)) {
244                 name = mContext.getString(android.R.string.unknownName);
245             }
246             nameList.add(name);
247         }
248 
249         Collections.sort(nameList, new Comparator <String> () {
250             @Override
251             public int compare(String str1, String str2){
252                 return str1.compareToIgnoreCase(str2);
253             }
254         });
255 
256         for (mCursor.moveToFirst(); !mCursor.isAfterLast(); mCursor.moveToNext()) {
257             if(mCursor.getString(NAME_COLUMN_INDEX).equals(nameList.get(position))) {
258               break;
259             }
260 
261         }
262     }
263 
getSIMContactsSize()264     public final int getSIMContactsSize() {
265         int size = 0;
266         Cursor contactCursor = null;
267         try {
268             contactCursor = BluetoothMethodProxy.getInstance().contentResolverQuery(
269                     mContentResolver, SIM_URI, SIM_PROJECTION, null,null, null);
270             if (contactCursor != null) {
271                 size = contactCursor.getCount();
272             }
273         } finally {
274             if (contactCursor != null) {
275                 contactCursor.close();
276             }
277         }
278         return size;
279     }
280 
getSIMPhonebookNameList(final int orderByWhat)281     public final ArrayList<String> getSIMPhonebookNameList(final int orderByWhat) {
282         ArrayList<String> nameList = new ArrayList<String>();
283         nameList.add(BluetoothPbapService.getLocalPhoneName());
284         //Since owner card should always be 0.vcf, maintain a separate list to avoid sorting
285         ArrayList<String> allnames = new ArrayList<String>();
286         Cursor contactCursor = null;
287         try {
288             contactCursor = BluetoothMethodProxy.getInstance().contentResolverQuery(
289                     mContentResolver, SIM_URI, SIM_PROJECTION, null,null,null);
290             if (contactCursor != null) {
291                 for (contactCursor.moveToFirst(); !contactCursor.isAfterLast(); contactCursor
292                         .moveToNext()) {
293                     String name = contactCursor.getString(NAME_COLUMN_INDEX);
294                     if (TextUtils.isEmpty(name)) {
295                         name = mContext.getString(android.R.string.unknownName);
296                     }
297                     allnames.add(name);
298                 }
299             }
300         } finally {
301             if (contactCursor != null) {
302                 contactCursor.close();
303             }
304         }
305         if (orderByWhat == BluetoothPbapObexServer.ORDER_BY_INDEXED) {
306             if (V) Log.v(TAG, "getPhonebookNameList, order by index");
307         } else if (orderByWhat == BluetoothPbapObexServer.ORDER_BY_ALPHABETICAL) {
308             if (V) Log.v(TAG, "getPhonebookNameList, order by alpha");
309             Collections.sort(allnames, new Comparator <String> () {
310                 @Override
311                 public int compare(String str1, String str2) {
312                     return str1.compareToIgnoreCase(str2);
313                 }
314             });
315         }
316 
317         nameList.addAll(allnames);
318         return nameList;
319 
320     }
321 
getSIMContactNamesByNumber( final String phoneNumber)322     public final ArrayList<String> getSIMContactNamesByNumber(
323             final String phoneNumber) {
324         ArrayList<String> nameList = new ArrayList<String>();
325         ArrayList<String> startNameList = new ArrayList<String>();
326         Cursor contactCursor = null;
327 
328         try {
329             contactCursor = BluetoothMethodProxy.getInstance().contentResolverQuery(
330                     mContentResolver, SIM_URI, SIM_PROJECTION, null, null, null);
331 
332             if (contactCursor != null) {
333                 for (contactCursor.moveToFirst(); !contactCursor.isAfterLast(); contactCursor
334                         .moveToNext()) {
335                     String number = contactCursor.getString(NUMBER_COLUMN_INDEX);
336                     if (number == null) {
337                         if (V) Log.v(TAG, "number is null");
338                         continue;
339                     }
340 
341                     if (V) Log.v(TAG, "number: " + number + " phoneNumber:" + phoneNumber);
342                     if ((number.endsWith(phoneNumber)) || (number.startsWith(phoneNumber))) {
343                         String name = contactCursor.getString(NAME_COLUMN_INDEX);
344                         if (TextUtils.isEmpty(name)) {
345                             name = mContext.getString(android.R.string.unknownName);
346                         }
347                         if (V) Log.v(TAG, "got name " + name + " by number " + phoneNumber);
348 
349                         if (number.endsWith(phoneNumber)) {
350                             if (V) Log.v(TAG, "Adding to end name list");
351                             nameList.add(name);
352                         } else {
353                             if (V) Log.v(TAG, "Adding to start name list");
354                             startNameList.add(name);
355                         }
356                     }
357                 }
358             }
359         } finally {
360             if (contactCursor != null) {
361                 contactCursor.close();
362             }
363         }
364         int startListSize = startNameList.size();
365         for (int index = 0; index < startListSize; index++) {
366             String object = startNameList.get(index);
367             if (!nameList.contains(object))
368                 nameList.add(object);
369         }
370 
371         return nameList;
372     }
373 
composeAndSendSIMPhonebookVcards(Context context, Operation op, final int startPoint, final int endPoint, final boolean vcardType21, String ownerVCard)374     public static final int composeAndSendSIMPhonebookVcards(Context context, Operation op,
375             final int startPoint, final int endPoint, final boolean vcardType21,
376             String ownerVCard) {
377         if (startPoint < 1 || startPoint > endPoint) {
378             Log.e(TAG, "internal error: startPoint or endPoint is not correct.");
379             return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR;
380         }
381         BluetoothPbapSimVcardManager composer = null;
382         HandlerForStringBuffer buffer = null;
383         try {
384             composer = new BluetoothPbapSimVcardManager(context);
385             buffer = new HandlerForStringBuffer(op, ownerVCard);
386 
387             if (!composer.init(SIM_URI, null, null, null) || !buffer.init()) {
388                 return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR;
389             }
390             composer.moveToPosition(startPoint -1, false);
391             for (int count =startPoint -1; count < endPoint; count++) {
392                 if (BluetoothPbapObexServer.sIsAborted) {
393                     ((ServerOperation)op).setAborted(true);
394                     BluetoothPbapObexServer.sIsAborted = false;
395                     break;
396                 }
397                 String vcard = composer.createOneEntry(vcardType21);
398                 if (vcard == null) {
399                     Log.e(TAG, "Failed to read a contact. Error reason: "
400                             + composer.getErrorReason() + ", count:" + count);
401                     return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR;
402                 }
403                 buffer.writeVCard(vcard);
404             }
405         } finally {
406             if (composer != null) {
407                 composer.terminate();
408             }
409             if (buffer != null) {
410                 buffer.terminate();
411             }
412         }
413         return ResponseCodes.OBEX_HTTP_OK;
414     }
415 
composeAndSendSIMPhonebookOneVcard(Context context, Operation op, final int offset, final boolean vcardType21, String ownerVCard, int orderByWhat)416     public static final int composeAndSendSIMPhonebookOneVcard(Context context, Operation op,
417             final int offset, final boolean vcardType21, String ownerVCard,
418             int orderByWhat) {
419         if (offset < 1) {
420             Log.e(TAG, "Internal error: offset is not correct.");
421             return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR;
422         }
423         if (V) Log.v(TAG, "composeAndSendSIMPhonebookOneVcard orderByWhat " + orderByWhat);
424         BluetoothPbapSimVcardManager composer = null;
425         HandlerForStringBuffer buffer = null;
426         try {
427             composer = new BluetoothPbapSimVcardManager(context);
428             buffer = new HandlerForStringBuffer(op, ownerVCard);
429             if (!composer.init(SIM_URI, null, null, null) || !buffer.init()) {
430                 return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR;
431             }
432             if (orderByWhat == BluetoothPbapObexServer.ORDER_BY_INDEXED) {
433                 composer.moveToPosition(offset -1, false);
434             } else if (orderByWhat == BluetoothPbapObexServer.ORDER_BY_ALPHABETICAL) {
435                 composer.moveToPosition(offset -1, true);
436             }
437             if (BluetoothPbapObexServer.sIsAborted) {
438                 ((ServerOperation)op).setAborted(true);
439                  BluetoothPbapObexServer.sIsAborted = false;
440             }
441             String vcard = composer.createOneEntry(vcardType21);
442             if (vcard == null) {
443                 Log.e(TAG, "Failed to read a contact. Error reason: "
444                             + composer.getErrorReason());
445                 return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR;
446             }
447             buffer.writeVCard(vcard);
448         } finally {
449             if (composer != null) {
450                 composer.terminate();
451             }
452             if (buffer != null) {
453                 buffer.terminate();
454             }
455         }
456 
457         return ResponseCodes.OBEX_HTTP_OK;
458     }
459 
isSimPhoneBook(String name, String type, String PB, String SIM1, String TYPE_PB, String TYPE_LISTING, String mCurrentPath)460     protected boolean isSimPhoneBook(String name, String type, String PB,
461             String SIM1, String TYPE_PB, String TYPE_LISTING, String  mCurrentPath) {
462 
463         return ((name.contains(PB.subSequence(0, PB.length()))
464                 && name.contains(SIM1.subSequence(0,
465                         SIM1.length())))
466                 && (type.equals(TYPE_PB)))
467                 || (((name.contains(
468                         PB.subSequence(0, PB.length())))
469                         && (mCurrentPath.equals(SIM_PATH)))
470                         && (type.equals(TYPE_LISTING)));
471     }
472 
getType(String searchAttr)473     protected String getType(String searchAttr) {
474         String type = "";
475         if (searchAttr.equals("0")) {
476             type = "name";
477         } else if (searchAttr.equals("1")) {
478             type = "number";
479         }
480         return type;
481     }
482 }
483