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