1 /* 2 * Copyright (C) 2006 The Android Open Source Project 3 * 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 17 package com.android.providers.telephony; 18 19 import android.annotation.NonNull; 20 import android.app.AppOpsManager; 21 import android.content.ContentProvider; 22 import android.content.ContentResolver; 23 import android.content.ContentValues; 24 import android.content.Context; 25 import android.content.UriMatcher; 26 import android.database.Cursor; 27 import android.database.DatabaseUtils; 28 import android.database.MatrixCursor; 29 import android.database.sqlite.SQLiteDatabase; 30 import android.database.sqlite.SQLiteOpenHelper; 31 import android.database.sqlite.SQLiteQueryBuilder; 32 import android.net.Uri; 33 import android.os.Binder; 34 import android.os.UserHandle; 35 import android.provider.Contacts; 36 import android.provider.Telephony; 37 import android.provider.Telephony.MmsSms; 38 import android.provider.Telephony.Sms; 39 import android.provider.Telephony.Threads; 40 import android.telephony.SmsManager; 41 import android.telephony.SmsMessage; 42 import android.telephony.SubscriptionManager; 43 import android.text.TextUtils; 44 import android.util.Log; 45 46 import com.android.internal.annotations.VisibleForTesting; 47 48 import java.util.HashMap; 49 import java.util.List; 50 51 public class SmsProvider extends ContentProvider { 52 /* No response constant from SmsResponse */ 53 static final int NO_ERROR_CODE = -1; 54 55 private static final Uri NOTIFICATION_URI = Uri.parse("content://sms"); 56 private static final Uri ICC_URI = Uri.parse("content://sms/icc"); 57 private static final Uri ICC_SUBID_URI = Uri.parse("content://sms/icc_subId"); 58 static final String TABLE_SMS = "sms"; 59 static final String TABLE_RAW = "raw"; 60 private static final String TABLE_SR_PENDING = "sr_pending"; 61 private static final String TABLE_WORDS = "words"; 62 static final String VIEW_SMS_RESTRICTED = "sms_restricted"; 63 64 private static final Integer ONE = Integer.valueOf(1); 65 66 private static final String[] CONTACT_QUERY_PROJECTION = 67 new String[] { Contacts.Phones.PERSON_ID }; 68 private static final int PERSON_ID_COLUMN = 0; 69 70 /** Delete any raw messages or message segments marked deleted that are older than an hour. */ 71 static final long RAW_MESSAGE_EXPIRE_AGE_MS = (long) (60 * 60 * 1000); 72 73 /** 74 * These are the columns that are available when reading SMS 75 * messages from the ICC. Columns whose names begin with "is_" 76 * have either "true" or "false" as their values. 77 */ 78 private final static String[] ICC_COLUMNS = new String[] { 79 // N.B.: These columns must appear in the same order as the 80 // calls to add appear in convertIccToSms. 81 "service_center_address", // getServiceCenterAddress 82 "address", // getDisplayOriginatingAddress or getRecipientAddress 83 "message_class", // getMessageClass 84 "body", // getDisplayMessageBody 85 "date", // getTimestampMillis 86 "status", // getStatusOnIcc 87 "index_on_icc", // getIndexOnIcc (1-based index) 88 "is_status_report", // isStatusReportMessage 89 "transport_type", // Always "sms". 90 "type", // depend on getStatusOnIcc 91 "locked", // Always 0 (false). 92 "error_code", // Always -1 (NO_ERROR_CODE), previously it was 0 always. 93 "_id" 94 }; 95 96 @Override onCreate()97 public boolean onCreate() { 98 setAppOps(AppOpsManager.OP_READ_SMS, AppOpsManager.OP_WRITE_SMS); 99 // So we have two database files. One in de, one in ce. Here only "raw" table is in 100 // mDeOpenHelper, other tables are all in mCeOpenHelper. 101 mDeOpenHelper = MmsSmsDatabaseHelper.getInstanceForDe(getContext()); 102 mCeOpenHelper = MmsSmsDatabaseHelper.getInstanceForCe(getContext()); 103 TelephonyBackupAgent.DeferredSmsMmsRestoreService.startIfFilesExist(getContext()); 104 return true; 105 } 106 107 /** 108 * Return the proper view of "sms" table for the current access status. 109 * 110 * @param accessRestricted If the access is restricted 111 * @return the table/view name of the "sms" data 112 */ getSmsTable(boolean accessRestricted)113 public static String getSmsTable(boolean accessRestricted) { 114 return accessRestricted ? VIEW_SMS_RESTRICTED : TABLE_SMS; 115 } 116 117 @Override query(Uri url, String[] projectionIn, String selection, String[] selectionArgs, String sort)118 public Cursor query(Uri url, String[] projectionIn, String selection, 119 String[] selectionArgs, String sort) { 120 // First check if a restricted view of the "sms" table should be used based on the 121 // caller's identity. Only system, phone or the default sms app can have full access 122 // of sms data. For other apps, we present a restricted view which only contains sent 123 // or received messages. 124 final boolean accessRestricted = ProviderUtil.isAccessRestricted( 125 getContext(), getCallingPackage(), Binder.getCallingUid()); 126 final String smsTable = getSmsTable(accessRestricted); 127 SQLiteQueryBuilder qb = new SQLiteQueryBuilder(); 128 129 // If access is restricted, we don't allow subqueries in the query. 130 if (accessRestricted) { 131 try { 132 SqlQueryChecker.checkQueryParametersForSubqueries(projectionIn, selection, sort); 133 } catch (IllegalArgumentException e) { 134 Log.w(TAG, "Query rejected: " + e.getMessage()); 135 return null; 136 } 137 } 138 139 // Generate the body of the query. 140 int match = sURLMatcher.match(url); 141 SQLiteDatabase db = getReadableDatabase(match); 142 switch (match) { 143 case SMS_ALL: 144 constructQueryForBox(qb, Sms.MESSAGE_TYPE_ALL, smsTable); 145 break; 146 147 case SMS_UNDELIVERED: 148 constructQueryForUndelivered(qb, smsTable); 149 break; 150 151 case SMS_FAILED: 152 constructQueryForBox(qb, Sms.MESSAGE_TYPE_FAILED, smsTable); 153 break; 154 155 case SMS_QUEUED: 156 constructQueryForBox(qb, Sms.MESSAGE_TYPE_QUEUED, smsTable); 157 break; 158 159 case SMS_INBOX: 160 constructQueryForBox(qb, Sms.MESSAGE_TYPE_INBOX, smsTable); 161 break; 162 163 case SMS_SENT: 164 constructQueryForBox(qb, Sms.MESSAGE_TYPE_SENT, smsTable); 165 break; 166 167 case SMS_DRAFT: 168 constructQueryForBox(qb, Sms.MESSAGE_TYPE_DRAFT, smsTable); 169 break; 170 171 case SMS_OUTBOX: 172 constructQueryForBox(qb, Sms.MESSAGE_TYPE_OUTBOX, smsTable); 173 break; 174 175 case SMS_ALL_ID: 176 qb.setTables(smsTable); 177 qb.appendWhere("(_id = " + url.getPathSegments().get(0) + ")"); 178 break; 179 180 case SMS_INBOX_ID: 181 case SMS_FAILED_ID: 182 case SMS_SENT_ID: 183 case SMS_DRAFT_ID: 184 case SMS_OUTBOX_ID: 185 qb.setTables(smsTable); 186 qb.appendWhere("(_id = " + url.getPathSegments().get(1) + ")"); 187 break; 188 189 case SMS_CONVERSATIONS_ID: 190 int threadID; 191 192 try { 193 threadID = Integer.parseInt(url.getPathSegments().get(1)); 194 if (Log.isLoggable(TAG, Log.VERBOSE)) { 195 Log.d(TAG, "query conversations: threadID=" + threadID); 196 } 197 } 198 catch (Exception ex) { 199 Log.e(TAG, 200 "Bad conversation thread id: " 201 + url.getPathSegments().get(1)); 202 return null; 203 } 204 205 qb.setTables(smsTable); 206 qb.appendWhere("thread_id = " + threadID); 207 break; 208 209 case SMS_CONVERSATIONS: 210 qb.setTables(smsTable + ", " 211 + "(SELECT thread_id AS group_thread_id, " 212 + "MAX(date) AS group_date, " 213 + "COUNT(*) AS msg_count " 214 + "FROM " + smsTable + " " 215 + "GROUP BY thread_id) AS groups"); 216 qb.appendWhere(smsTable + ".thread_id=groups.group_thread_id" 217 + " AND " + smsTable + ".date=groups.group_date"); 218 final HashMap<String, String> projectionMap = new HashMap<>(); 219 projectionMap.put(Sms.Conversations.SNIPPET, 220 smsTable + ".body AS snippet"); 221 projectionMap.put(Sms.Conversations.THREAD_ID, 222 smsTable + ".thread_id AS thread_id"); 223 projectionMap.put(Sms.Conversations.MESSAGE_COUNT, 224 "groups.msg_count AS msg_count"); 225 projectionMap.put("delta", null); 226 qb.setProjectionMap(projectionMap); 227 break; 228 229 case SMS_RAW_MESSAGE: 230 // before querying purge old entries with deleted = 1 231 purgeDeletedMessagesInRawTable(db); 232 qb.setTables("raw"); 233 break; 234 235 case SMS_STATUS_PENDING: 236 qb.setTables("sr_pending"); 237 break; 238 239 case SMS_ATTACHMENT: 240 qb.setTables("attachments"); 241 break; 242 243 case SMS_ATTACHMENT_ID: 244 qb.setTables("attachments"); 245 qb.appendWhere( 246 "(sms_id = " + url.getPathSegments().get(1) + ")"); 247 break; 248 249 case SMS_QUERY_THREAD_ID: 250 qb.setTables("canonical_addresses"); 251 if (projectionIn == null) { 252 projectionIn = sIDProjection; 253 } 254 break; 255 256 case SMS_STATUS_ID: 257 qb.setTables(smsTable); 258 qb.appendWhere("(_id = " + url.getPathSegments().get(1) + ")"); 259 break; 260 261 case SMS_ALL_ICC: 262 case SMS_ALL_ICC_SUBID: 263 { 264 int subId; 265 if (match == SMS_ALL_ICC) { 266 subId = SmsManager.getDefaultSmsSubscriptionId(); 267 } else { 268 try { 269 subId = Integer.parseInt(url.getPathSegments().get(1)); 270 } catch (NumberFormatException e) { 271 throw new IllegalArgumentException("Wrong path segements, uri= " + url); 272 } 273 } 274 Cursor ret = getAllMessagesFromIcc(subId); 275 ret.setNotificationUri(getContext().getContentResolver(), 276 match == SMS_ALL_ICC ? ICC_URI : ICC_SUBID_URI); 277 return ret; 278 } 279 280 case SMS_ICC: 281 case SMS_ICC_SUBID: 282 { 283 int subId; 284 int messageIndex; 285 try { 286 if (match == SMS_ICC) { 287 subId = SmsManager.getDefaultSmsSubscriptionId(); 288 messageIndex = Integer.parseInt(url.getPathSegments().get(1)); 289 } else { 290 subId = Integer.parseInt(url.getPathSegments().get(1)); 291 messageIndex = Integer.parseInt(url.getPathSegments().get(2)); 292 } 293 } catch (NumberFormatException e) { 294 throw new IllegalArgumentException("Wrong path segements, uri= " + url); 295 } 296 Cursor ret = getSingleMessageFromIcc(subId, messageIndex); 297 ret.setNotificationUri(getContext().getContentResolver(), 298 match == SMS_ICC ? ICC_URI : ICC_SUBID_URI); 299 return ret; 300 } 301 302 default: 303 Log.e(TAG, "Invalid request: " + url); 304 return null; 305 } 306 307 String orderBy = null; 308 309 if (!TextUtils.isEmpty(sort)) { 310 orderBy = sort; 311 } else if (qb.getTables().equals(smsTable)) { 312 orderBy = Sms.DEFAULT_SORT_ORDER; 313 } 314 315 Cursor ret = qb.query(db, projectionIn, selection, selectionArgs, 316 null, null, orderBy); 317 318 // TODO: Since the URLs are a mess, always use content://sms 319 ret.setNotificationUri(getContext().getContentResolver(), 320 NOTIFICATION_URI); 321 return ret; 322 } 323 purgeDeletedMessagesInRawTable(SQLiteDatabase db)324 private void purgeDeletedMessagesInRawTable(SQLiteDatabase db) { 325 long oldTimestamp = System.currentTimeMillis() - RAW_MESSAGE_EXPIRE_AGE_MS; 326 int num = db.delete(TABLE_RAW, "deleted = 1 AND date < " + oldTimestamp, null); 327 if (Log.isLoggable(TAG, Log.VERBOSE)) { 328 Log.d(TAG, "purgeDeletedMessagesInRawTable: num rows older than " + oldTimestamp + 329 " purged: " + num); 330 } 331 } 332 getDBOpenHelper(int match)333 private SQLiteOpenHelper getDBOpenHelper(int match) { 334 // Raw table is stored on de database. Other tables are stored in ce database. 335 if (match == SMS_RAW_MESSAGE || match == SMS_RAW_MESSAGE_PERMANENT_DELETE) { 336 return mDeOpenHelper; 337 } 338 return mCeOpenHelper; 339 } 340 convertIccToSms(SmsMessage message, int id)341 private Object[] convertIccToSms(SmsMessage message, int id) { 342 int statusOnIcc = message.getStatusOnIcc(); 343 int type = Sms.MESSAGE_TYPE_ALL; 344 switch (statusOnIcc) { 345 case SmsManager.STATUS_ON_ICC_READ: 346 case SmsManager.STATUS_ON_ICC_UNREAD: 347 type = Sms.MESSAGE_TYPE_INBOX; 348 break; 349 case SmsManager.STATUS_ON_ICC_SENT: 350 type = Sms.MESSAGE_TYPE_SENT; 351 break; 352 case SmsManager.STATUS_ON_ICC_UNSENT: 353 type = Sms.MESSAGE_TYPE_OUTBOX; 354 break; 355 } 356 357 String address = (type == Sms.MESSAGE_TYPE_INBOX) 358 ? message.getDisplayOriginatingAddress() 359 : message.getRecipientAddress(); 360 361 int index = message.getIndexOnIcc(); 362 if (address == null) { 363 // The status byte of an EF_SMS record may not be correct. try to read other address 364 // type again. 365 Log.e(TAG, "convertIccToSms: EF_SMS(" + index + ")=> address=null, type=" + type 366 + ", status=" + statusOnIcc + "(may not be correct). fallback to other type."); 367 address = (type == Sms.MESSAGE_TYPE_INBOX) 368 ? message.getRecipientAddress() 369 : message.getDisplayOriginatingAddress(); 370 371 if (address != null) { 372 // Rely on actual PDU(address) to set type again. 373 type = (type == Sms.MESSAGE_TYPE_INBOX) 374 ? Sms.MESSAGE_TYPE_SENT 375 : Sms.MESSAGE_TYPE_INBOX; 376 Log.d(TAG, "convertIccToSms: new type=" + type + ", address=xxxxxx"); 377 } else { 378 Log.e(TAG, "convertIccToSms: no change"); 379 } 380 } 381 382 // N.B.: These calls must appear in the same order as the 383 // columns appear in ICC_COLUMNS. 384 Object[] row = new Object[13]; 385 row[0] = message.getServiceCenterAddress(); 386 row[1] = address; 387 row[2] = String.valueOf(message.getMessageClass()); 388 row[3] = message.getDisplayMessageBody(); 389 row[4] = message.getTimestampMillis(); 390 row[5] = statusOnIcc; 391 row[6] = index; 392 row[7] = message.isStatusReportMessage(); 393 row[8] = "sms"; 394 row[9] = type; 395 row[10] = 0; // locked 396 row[11] = NO_ERROR_CODE; 397 row[12] = id; 398 return row; 399 } 400 401 /** 402 * Gets single message from the ICC for a subscription ID. 403 * 404 * @param subId the subscription ID. 405 * @param messageIndex the message index of the messaage in the ICC (1-based index). 406 * @return a cursor containing just one message from the ICC for the subscription ID. 407 */ getSingleMessageFromIcc(int subId, int messageIndex)408 private Cursor getSingleMessageFromIcc(int subId, int messageIndex) { 409 if (!SubscriptionManager.isValidSubscriptionId(subId)) { 410 throw new IllegalArgumentException("Invalid Subscription ID " + subId); 411 } 412 SmsManager smsManager = SmsManager.getSmsManagerForSubscriptionId(subId); 413 List<SmsMessage> messages; 414 415 // Use phone app permissions to avoid UID mismatch in AppOpsManager.noteOp() call. 416 long token = Binder.clearCallingIdentity(); 417 try { 418 // getMessagesFromIcc() returns a zero-based list of valid messages in the ICC. 419 messages = smsManager.getMessagesFromIcc(); 420 } finally { 421 Binder.restoreCallingIdentity(token); 422 } 423 424 final int count = messages.size(); 425 for (int i = 0; i < count; i++) { 426 SmsMessage message = messages.get(i); 427 if (message != null && message.getIndexOnIcc() == messageIndex) { 428 MatrixCursor cursor = new MatrixCursor(ICC_COLUMNS, 1); 429 cursor.addRow(convertIccToSms(message, 0)); 430 return cursor; 431 } 432 } 433 434 throw new IllegalArgumentException( 435 "No message in index " + messageIndex + " for subId " + subId); 436 } 437 438 /** 439 * Gets all the messages in the ICC for a subscription ID. 440 * 441 * @param subId the subscription ID. 442 * @return a cursor listing all the message in the ICC for the subscription ID. 443 */ getAllMessagesFromIcc(int subId)444 private Cursor getAllMessagesFromIcc(int subId) { 445 if (!SubscriptionManager.isValidSubscriptionId(subId)) { 446 throw new IllegalArgumentException("Invalid Subscription ID " + subId); 447 } 448 SmsManager smsManager = SmsManager.getSmsManagerForSubscriptionId(subId); 449 List<SmsMessage> messages; 450 451 // Use phone app permissions to avoid UID mismatch in AppOpsManager.noteOp() call 452 long token = Binder.clearCallingIdentity(); 453 try { 454 // getMessagesFromIcc() returns a zero-based list of valid messages in the ICC. 455 messages = smsManager.getMessagesFromIcc(); 456 } finally { 457 Binder.restoreCallingIdentity(token); 458 } 459 460 final int count = messages.size(); 461 MatrixCursor cursor = new MatrixCursor(ICC_COLUMNS, count); 462 for (int i = 0; i < count; i++) { 463 SmsMessage message = messages.get(i); 464 if (message != null) { 465 cursor.addRow(convertIccToSms(message, i)); 466 } 467 } 468 return cursor; 469 } 470 constructQueryForBox(SQLiteQueryBuilder qb, int type, String smsTable)471 private void constructQueryForBox(SQLiteQueryBuilder qb, int type, String smsTable) { 472 qb.setTables(smsTable); 473 474 if (type != Sms.MESSAGE_TYPE_ALL) { 475 qb.appendWhere("type=" + type); 476 } 477 } 478 constructQueryForUndelivered(SQLiteQueryBuilder qb, String smsTable)479 private void constructQueryForUndelivered(SQLiteQueryBuilder qb, String smsTable) { 480 qb.setTables(smsTable); 481 482 qb.appendWhere("(type=" + Sms.MESSAGE_TYPE_OUTBOX + 483 " OR type=" + Sms.MESSAGE_TYPE_FAILED + 484 " OR type=" + Sms.MESSAGE_TYPE_QUEUED + ")"); 485 } 486 487 @Override getType(Uri url)488 public String getType(Uri url) { 489 switch (url.getPathSegments().size()) { 490 case 0: 491 return VND_ANDROID_DIR_SMS; 492 case 1: 493 try { 494 Integer.parseInt(url.getPathSegments().get(0)); 495 return VND_ANDROID_SMS; 496 } catch (NumberFormatException ex) { 497 return VND_ANDROID_DIR_SMS; 498 } 499 case 2: 500 // TODO: What about "threadID"? 501 if (url.getPathSegments().get(0).equals("conversations")) { 502 return VND_ANDROID_SMSCHAT; 503 } else { 504 return VND_ANDROID_SMS; 505 } 506 } 507 return null; 508 } 509 510 @Override bulkInsert(@onNull Uri url, @NonNull ContentValues[] values)511 public int bulkInsert(@NonNull Uri url, @NonNull ContentValues[] values) { 512 final int callerUid = Binder.getCallingUid(); 513 final String callerPkg = getCallingPackage(); 514 long token = Binder.clearCallingIdentity(); 515 try { 516 int messagesInserted = 0; 517 for (ContentValues initialValues : values) { 518 Uri insertUri = insertInner(url, initialValues, callerUid, callerPkg); 519 if (insertUri != null) { 520 messagesInserted++; 521 } 522 } 523 524 // The raw table is used by the telephony layer for storing an sms before 525 // sending out a notification that an sms has arrived. We don't want to notify 526 // the default sms app of changes to this table. 527 final boolean notifyIfNotDefault = sURLMatcher.match(url) != SMS_RAW_MESSAGE; 528 notifyChange(notifyIfNotDefault, url, callerPkg); 529 return messagesInserted; 530 } finally { 531 Binder.restoreCallingIdentity(token); 532 } 533 } 534 535 @Override insert(Uri url, ContentValues initialValues)536 public Uri insert(Uri url, ContentValues initialValues) { 537 final int callerUid = Binder.getCallingUid(); 538 final String callerPkg = getCallingPackage(); 539 long token = Binder.clearCallingIdentity(); 540 try { 541 Uri insertUri = insertInner(url, initialValues, callerUid, callerPkg); 542 543 int match = sURLMatcher.match(url); 544 // Skip notifyChange() if insertUri is null for SMS_ALL_ICC or SMS_ALL_ICC_SUBID caused 545 // by failure of insertMessageToIcc()(e.g. SIM full). 546 if (insertUri != null || (match != SMS_ALL_ICC && match != SMS_ALL_ICC_SUBID)) { 547 // The raw table is used by the telephony layer for storing an sms before sending 548 // out a notification that an sms has arrived. We don't want to notify the default 549 // sms app of changes to this table. 550 final boolean notifyIfNotDefault = match != SMS_RAW_MESSAGE; 551 notifyChange(notifyIfNotDefault, insertUri, callerPkg); 552 } 553 return insertUri; 554 } finally { 555 Binder.restoreCallingIdentity(token); 556 } 557 } 558 insertInner(Uri url, ContentValues initialValues, int callerUid, String callerPkg)559 private Uri insertInner(Uri url, ContentValues initialValues, int callerUid, String callerPkg) { 560 ContentValues values; 561 long rowID; 562 int type = Sms.MESSAGE_TYPE_ALL; 563 564 int match = sURLMatcher.match(url); 565 String table = TABLE_SMS; 566 567 switch (match) { 568 case SMS_ALL: 569 Integer typeObj = initialValues.getAsInteger(Sms.TYPE); 570 if (typeObj != null) { 571 type = typeObj.intValue(); 572 } else { 573 // default to inbox 574 type = Sms.MESSAGE_TYPE_INBOX; 575 } 576 break; 577 578 case SMS_INBOX: 579 type = Sms.MESSAGE_TYPE_INBOX; 580 break; 581 582 case SMS_FAILED: 583 type = Sms.MESSAGE_TYPE_FAILED; 584 break; 585 586 case SMS_QUEUED: 587 type = Sms.MESSAGE_TYPE_QUEUED; 588 break; 589 590 case SMS_SENT: 591 type = Sms.MESSAGE_TYPE_SENT; 592 break; 593 594 case SMS_DRAFT: 595 type = Sms.MESSAGE_TYPE_DRAFT; 596 break; 597 598 case SMS_OUTBOX: 599 type = Sms.MESSAGE_TYPE_OUTBOX; 600 break; 601 602 case SMS_RAW_MESSAGE: 603 table = "raw"; 604 break; 605 606 case SMS_STATUS_PENDING: 607 table = "sr_pending"; 608 break; 609 610 case SMS_ATTACHMENT: 611 table = "attachments"; 612 break; 613 614 case SMS_NEW_THREAD_ID: 615 table = "canonical_addresses"; 616 break; 617 618 case SMS_ALL_ICC: 619 case SMS_ALL_ICC_SUBID: 620 int subId; 621 if (match == SMS_ALL_ICC) { 622 subId = SmsManager.getDefaultSmsSubscriptionId(); 623 } else { 624 try { 625 subId = Integer.parseInt(url.getPathSegments().get(1)); 626 } catch (NumberFormatException e) { 627 throw new IllegalArgumentException( 628 "Wrong path segements for SMS_ALL_ICC_SUBID, uri= " + url); 629 } 630 } 631 632 if (initialValues == null) { 633 throw new IllegalArgumentException("ContentValues is null"); 634 } 635 636 String scAddress = initialValues.getAsString(Sms.SERVICE_CENTER); 637 String address = initialValues.getAsString(Sms.ADDRESS); 638 String message = initialValues.getAsString(Sms.BODY); 639 boolean isRead = true; 640 Integer obj = initialValues.getAsInteger(Sms.TYPE); 641 642 if (obj == null || address == null || message == null) { 643 throw new IllegalArgumentException("Missing SMS data"); 644 } 645 646 type = obj.intValue(); 647 if (!isSupportedType(type)) { 648 throw new IllegalArgumentException("Unsupported message type= " + type); 649 } 650 obj = initialValues.getAsInteger(Sms.READ); // 0: Unread, 1: Read 651 if (obj != null && obj.intValue() == 0) { 652 isRead = false; 653 } 654 655 Long date = initialValues.getAsLong(Sms.DATE); 656 return insertMessageToIcc(subId, scAddress, address, message, type, isRead, 657 date != null ? date : 0) ? url : null; 658 659 default: 660 Log.e(TAG, "Invalid request: " + url); 661 return null; 662 } 663 664 SQLiteDatabase db = getWritableDatabase(match); 665 666 if (table.equals(TABLE_SMS)) { 667 boolean addDate = false; 668 boolean addType = false; 669 670 // Make sure that the date and type are set 671 if (initialValues == null) { 672 values = new ContentValues(1); 673 addDate = true; 674 addType = true; 675 } else { 676 values = new ContentValues(initialValues); 677 678 if (!initialValues.containsKey(Sms.DATE)) { 679 addDate = true; 680 } 681 682 if (!initialValues.containsKey(Sms.TYPE)) { 683 addType = true; 684 } 685 } 686 687 if (addDate) { 688 values.put(Sms.DATE, new Long(System.currentTimeMillis())); 689 } 690 691 if (addType && (type != Sms.MESSAGE_TYPE_ALL)) { 692 values.put(Sms.TYPE, Integer.valueOf(type)); 693 } 694 695 // thread_id 696 Long threadId = values.getAsLong(Sms.THREAD_ID); 697 String address = values.getAsString(Sms.ADDRESS); 698 699 if (((threadId == null) || (threadId == 0)) && (!TextUtils.isEmpty(address))) { 700 values.put(Sms.THREAD_ID, Threads.getOrCreateThreadId( 701 getContext(), address)); 702 } 703 704 // If this message is going in as a draft, it should replace any 705 // other draft messages in the thread. Just delete all draft 706 // messages with this thread ID. We could add an OR REPLACE to 707 // the insert below, but we'd have to query to find the old _id 708 // to produce a conflict anyway. 709 if (values.getAsInteger(Sms.TYPE) == Sms.MESSAGE_TYPE_DRAFT) { 710 db.delete(TABLE_SMS, "thread_id=? AND type=?", 711 new String[] { values.getAsString(Sms.THREAD_ID), 712 Integer.toString(Sms.MESSAGE_TYPE_DRAFT) }); 713 } 714 715 if (type == Sms.MESSAGE_TYPE_INBOX) { 716 // Look up the person if not already filled in. 717 if ((values.getAsLong(Sms.PERSON) == null) && (!TextUtils.isEmpty(address))) { 718 Cursor cursor = null; 719 Uri uri = Uri.withAppendedPath(Contacts.Phones.CONTENT_FILTER_URL, 720 Uri.encode(address)); 721 try { 722 cursor = getContext().getContentResolver().query( 723 uri, 724 CONTACT_QUERY_PROJECTION, 725 null, null, null); 726 727 if (cursor.moveToFirst()) { 728 Long id = Long.valueOf(cursor.getLong(PERSON_ID_COLUMN)); 729 values.put(Sms.PERSON, id); 730 } 731 } catch (Exception ex) { 732 Log.e(TAG, "insert: query contact uri " + uri + " caught ", ex); 733 } finally { 734 if (cursor != null) { 735 cursor.close(); 736 } 737 } 738 } 739 } else { 740 // Mark all non-inbox messages read. 741 values.put(Sms.READ, ONE); 742 } 743 if (ProviderUtil.shouldSetCreator(values, callerUid)) { 744 // Only SYSTEM or PHONE can set CREATOR 745 // If caller is not SYSTEM or PHONE, or SYSTEM or PHONE does not set CREATOR 746 // set CREATOR using the truth on caller. 747 // Note: Inferring package name from UID may include unrelated package names 748 values.put(Sms.CREATOR, callerPkg); 749 } 750 } else { 751 if (initialValues == null) { 752 values = new ContentValues(1); 753 } else { 754 values = initialValues; 755 } 756 } 757 758 rowID = db.insert(table, "body", values); 759 760 // Don't use a trigger for updating the words table because of a bug 761 // in FTS3. The bug is such that the call to get the last inserted 762 // row is incorrect. 763 if (table == TABLE_SMS) { 764 // Update the words table with a corresponding row. The words table 765 // allows us to search for words quickly, without scanning the whole 766 // table; 767 ContentValues cv = new ContentValues(); 768 cv.put(Telephony.MmsSms.WordsTable.ID, rowID); 769 cv.put(Telephony.MmsSms.WordsTable.INDEXED_TEXT, values.getAsString("body")); 770 cv.put(Telephony.MmsSms.WordsTable.SOURCE_ROW_ID, rowID); 771 cv.put(Telephony.MmsSms.WordsTable.TABLE_ID, 1); 772 db.insert(TABLE_WORDS, Telephony.MmsSms.WordsTable.INDEXED_TEXT, cv); 773 } 774 if (rowID > 0) { 775 Uri uri = null; 776 if (table == TABLE_SMS) { 777 uri = Uri.withAppendedPath(Sms.CONTENT_URI, String.valueOf(rowID)); 778 } else { 779 uri = Uri.withAppendedPath(url, String.valueOf(rowID)); 780 } 781 if (Log.isLoggable(TAG, Log.VERBOSE)) { 782 Log.d(TAG, "insert " + uri + " succeeded"); 783 } 784 return uri; 785 } else { 786 Log.e(TAG, "insert: failed!"); 787 } 788 789 return null; 790 } 791 isSupportedType(int messageType)792 private boolean isSupportedType(int messageType) { 793 return (messageType == Sms.MESSAGE_TYPE_INBOX) 794 || (messageType == Sms.MESSAGE_TYPE_OUTBOX) 795 || (messageType == Sms.MESSAGE_TYPE_SENT); 796 } 797 getMessageStatusForIcc(int messageType, boolean isRead)798 private int getMessageStatusForIcc(int messageType, boolean isRead) { 799 if (messageType == Sms.MESSAGE_TYPE_SENT) { 800 return SmsManager.STATUS_ON_ICC_SENT; 801 } else if (messageType == Sms.MESSAGE_TYPE_OUTBOX) { 802 return SmsManager.STATUS_ON_ICC_UNSENT; 803 } else { // Sms.MESSAGE_BOX_INBOX 804 if (isRead) { 805 return SmsManager.STATUS_ON_ICC_READ; 806 } else { 807 return SmsManager.STATUS_ON_ICC_UNREAD; 808 } 809 } 810 } 811 812 /** 813 * Inserts new message to the ICC for a subscription ID. 814 * 815 * @param subId the subscription ID. 816 * @param scAddress the SMSC for this message. 817 * @param address destination or originating address. 818 * @param message the message text. 819 * @param messageType type of the message. 820 * @param isRead ture if the message has been read. Otherwise false. 821 * @param date the date the message was received. 822 * @return true for succeess. Otherwise false. 823 */ insertMessageToIcc(int subId, String scAddress, String address, String message, int messageType, boolean isRead, long date)824 private boolean insertMessageToIcc(int subId, String scAddress, String address, String message, 825 int messageType, boolean isRead, long date) { 826 if (!SubscriptionManager.isValidSubscriptionId(subId)) { 827 throw new IllegalArgumentException("Invalid Subscription ID " + subId); 828 } 829 SmsManager smsManager = SmsManager.getSmsManagerForSubscriptionId(subId); 830 831 int status = getMessageStatusForIcc(messageType, isRead); 832 SmsMessage.SubmitPdu smsPdu = 833 SmsMessage.getSmsPdu(subId, status, scAddress, address, message, date); 834 835 if (smsPdu == null) { 836 throw new IllegalArgumentException("Failed to create SMS PDU"); 837 } 838 839 // Use phone app permissions to avoid UID mismatch in AppOpsManager.noteOp() call. 840 long token = Binder.clearCallingIdentity(); 841 try { 842 return smsManager.copyMessageToIcc( 843 smsPdu.encodedScAddress, smsPdu.encodedMessage, status); 844 } finally { 845 Binder.restoreCallingIdentity(token); 846 } 847 } 848 849 @Override delete(Uri url, String where, String[] whereArgs)850 public int delete(Uri url, String where, String[] whereArgs) { 851 int count; 852 int match = sURLMatcher.match(url); 853 SQLiteDatabase db = getWritableDatabase(match); 854 boolean notifyIfNotDefault = true; 855 switch (match) { 856 case SMS_ALL: 857 count = db.delete(TABLE_SMS, where, whereArgs); 858 if (count != 0) { 859 // Don't update threads unless something changed. 860 MmsSmsDatabaseHelper.updateThreads(db, where, whereArgs); 861 } 862 break; 863 864 case SMS_ALL_ID: 865 try { 866 int message_id = Integer.parseInt(url.getPathSegments().get(0)); 867 count = MmsSmsDatabaseHelper.deleteOneSms(db, message_id); 868 } catch (Exception e) { 869 throw new IllegalArgumentException( 870 "Bad message id: " + url.getPathSegments().get(0)); 871 } 872 break; 873 874 case SMS_CONVERSATIONS_ID: 875 int threadID; 876 877 try { 878 threadID = Integer.parseInt(url.getPathSegments().get(1)); 879 } catch (Exception ex) { 880 throw new IllegalArgumentException( 881 "Bad conversation thread id: " 882 + url.getPathSegments().get(1)); 883 } 884 885 // delete the messages from the sms table 886 where = DatabaseUtils.concatenateWhere("thread_id=" + threadID, where); 887 count = db.delete(TABLE_SMS, where, whereArgs); 888 MmsSmsDatabaseHelper.updateThread(db, threadID); 889 break; 890 891 case SMS_RAW_MESSAGE: 892 ContentValues cv = new ContentValues(); 893 cv.put("deleted", 1); 894 count = db.update(TABLE_RAW, cv, where, whereArgs); 895 if (Log.isLoggable(TAG, Log.VERBOSE)) { 896 Log.d(TAG, "delete: num rows marked deleted in raw table: " + count); 897 } 898 notifyIfNotDefault = false; 899 break; 900 901 case SMS_RAW_MESSAGE_PERMANENT_DELETE: 902 count = db.delete(TABLE_RAW, where, whereArgs); 903 if (Log.isLoggable(TAG, Log.VERBOSE)) { 904 Log.d(TAG, "delete: num rows permanently deleted in raw table: " + count); 905 } 906 notifyIfNotDefault = false; 907 break; 908 909 case SMS_STATUS_PENDING: 910 count = db.delete("sr_pending", where, whereArgs); 911 break; 912 913 case SMS_ALL_ICC: 914 case SMS_ALL_ICC_SUBID: 915 { 916 int subId; 917 int deletedCnt; 918 if (match == SMS_ALL_ICC) { 919 subId = SmsManager.getDefaultSmsSubscriptionId(); 920 } else { 921 try { 922 subId = Integer.parseInt(url.getPathSegments().get(1)); 923 } catch (NumberFormatException e) { 924 throw new IllegalArgumentException("Wrong path segements, uri= " + url); 925 } 926 } 927 deletedCnt = deleteAllMessagesFromIcc(subId); 928 // Notify changes even failure case since there might be some changes should be 929 // known. 930 getContext() 931 .getContentResolver() 932 .notifyChange( 933 match == SMS_ALL_ICC ? ICC_URI : ICC_SUBID_URI, 934 null, 935 true, 936 UserHandle.USER_ALL); 937 return deletedCnt; 938 } 939 940 case SMS_ICC: 941 case SMS_ICC_SUBID: 942 { 943 int subId; 944 int messageIndex; 945 boolean success; 946 try { 947 if (match == SMS_ICC) { 948 subId = SmsManager.getDefaultSmsSubscriptionId(); 949 messageIndex = Integer.parseInt(url.getPathSegments().get(1)); 950 } else { 951 subId = Integer.parseInt(url.getPathSegments().get(1)); 952 messageIndex = Integer.parseInt(url.getPathSegments().get(2)); 953 } 954 } catch (NumberFormatException e) { 955 throw new IllegalArgumentException("Wrong path segements, uri= " + url); 956 } 957 success = deleteMessageFromIcc(subId, messageIndex); 958 // Notify changes even failure case since there might be some changes should be 959 // known. 960 getContext() 961 .getContentResolver() 962 .notifyChange( 963 match == SMS_ICC ? ICC_URI : ICC_SUBID_URI, 964 null, 965 true, 966 UserHandle.USER_ALL); 967 return success ? 1 : 0; // return deleted count 968 } 969 970 default: 971 throw new IllegalArgumentException("Unknown URL"); 972 } 973 974 if (count > 0) { 975 notifyChange(notifyIfNotDefault, url, getCallingPackage()); 976 } 977 return count; 978 } 979 980 /** 981 * Deletes the message at index from the ICC for a subscription ID. 982 * 983 * @param subId the subscription ID. 984 * @param messageIndex the message index of the message in the ICC (1-based index). 985 * @return true for succeess. Otherwise false. 986 */ deleteMessageFromIcc(int subId, int messageIndex)987 private boolean deleteMessageFromIcc(int subId, int messageIndex) { 988 if (!SubscriptionManager.isValidSubscriptionId(subId)) { 989 throw new IllegalArgumentException("Invalid Subscription ID " + subId); 990 } 991 SmsManager smsManager = SmsManager.getSmsManagerForSubscriptionId(subId); 992 993 // Use phone app permissions to avoid UID mismatch in AppOpsManager.noteOp() call. 994 long token = Binder.clearCallingIdentity(); 995 try { 996 return smsManager.deleteMessageFromIcc(messageIndex); 997 } finally { 998 Binder.restoreCallingIdentity(token); 999 } 1000 } 1001 1002 /** 1003 * Deletes all the messages from the ICC for a subscription ID. 1004 * 1005 * @param subId the subscription ID. 1006 * @return return deleted messaegs count. 1007 */ deleteAllMessagesFromIcc(int subId)1008 private int deleteAllMessagesFromIcc(int subId) { 1009 if (!SubscriptionManager.isValidSubscriptionId(subId)) { 1010 throw new IllegalArgumentException("Invalid Subscription ID " + subId); 1011 } 1012 SmsManager smsManager = SmsManager.getSmsManagerForSubscriptionId(subId); 1013 1014 // Use phone app permissions to avoid UID mismatch in AppOpsManager.noteOp() call. 1015 long token = Binder.clearCallingIdentity(); 1016 try { 1017 int deletedCnt = 0; 1018 int maxIndex = smsManager.getSmsCapacityOnIcc(); 1019 // messageIndex is 1-based index of the message in the ICC. 1020 for (int messageIndex = 1; messageIndex <= maxIndex; messageIndex++) { 1021 if (smsManager.deleteMessageFromIcc(messageIndex)) { 1022 deletedCnt++; 1023 } else { 1024 Log.e(TAG, "Fail to delete SMS at index " + messageIndex 1025 + " for subId " + subId); 1026 } 1027 } 1028 return deletedCnt; 1029 } finally { 1030 Binder.restoreCallingIdentity(token); 1031 } 1032 } 1033 1034 @Override update(Uri url, ContentValues values, String where, String[] whereArgs)1035 public int update(Uri url, ContentValues values, String where, String[] whereArgs) { 1036 final int callerUid = Binder.getCallingUid(); 1037 final String callerPkg = getCallingPackage(); 1038 int count = 0; 1039 String table = TABLE_SMS; 1040 String extraWhere = null; 1041 boolean notifyIfNotDefault = true; 1042 int match = sURLMatcher.match(url); 1043 SQLiteDatabase db = getWritableDatabase(match); 1044 1045 switch (match) { 1046 case SMS_RAW_MESSAGE: 1047 table = TABLE_RAW; 1048 notifyIfNotDefault = false; 1049 break; 1050 1051 case SMS_STATUS_PENDING: 1052 table = TABLE_SR_PENDING; 1053 break; 1054 1055 case SMS_ALL: 1056 case SMS_FAILED: 1057 case SMS_QUEUED: 1058 case SMS_INBOX: 1059 case SMS_SENT: 1060 case SMS_DRAFT: 1061 case SMS_OUTBOX: 1062 case SMS_CONVERSATIONS: 1063 break; 1064 1065 case SMS_ALL_ID: 1066 extraWhere = "_id=" + url.getPathSegments().get(0); 1067 break; 1068 1069 case SMS_INBOX_ID: 1070 case SMS_FAILED_ID: 1071 case SMS_SENT_ID: 1072 case SMS_DRAFT_ID: 1073 case SMS_OUTBOX_ID: 1074 extraWhere = "_id=" + url.getPathSegments().get(1); 1075 break; 1076 1077 case SMS_CONVERSATIONS_ID: { 1078 String threadId = url.getPathSegments().get(1); 1079 1080 try { 1081 Integer.parseInt(threadId); 1082 } catch (Exception ex) { 1083 Log.e(TAG, "Bad conversation thread id: " + threadId); 1084 break; 1085 } 1086 1087 extraWhere = "thread_id=" + threadId; 1088 break; 1089 } 1090 1091 case SMS_STATUS_ID: 1092 extraWhere = "_id=" + url.getPathSegments().get(1); 1093 break; 1094 1095 default: 1096 throw new UnsupportedOperationException( 1097 "URI " + url + " not supported"); 1098 } 1099 1100 if (table.equals(TABLE_SMS) && ProviderUtil.shouldRemoveCreator(values, callerUid)) { 1101 // CREATOR should not be changed by non-SYSTEM/PHONE apps 1102 Log.w(TAG, callerPkg + " tries to update CREATOR"); 1103 values.remove(Sms.CREATOR); 1104 } 1105 1106 where = DatabaseUtils.concatenateWhere(where, extraWhere); 1107 count = db.update(table, values, where, whereArgs); 1108 1109 if (count > 0) { 1110 if (Log.isLoggable(TAG, Log.VERBOSE)) { 1111 Log.d(TAG, "update " + url + " succeeded"); 1112 } 1113 notifyChange(notifyIfNotDefault, url, callerPkg); 1114 } 1115 return count; 1116 } 1117 notifyChange(boolean notifyIfNotDefault, Uri uri, final String callingPackage)1118 private void notifyChange(boolean notifyIfNotDefault, Uri uri, final String callingPackage) { 1119 final Context context = getContext(); 1120 ContentResolver cr = context.getContentResolver(); 1121 cr.notifyChange(uri, null, true, UserHandle.USER_ALL); 1122 cr.notifyChange(MmsSms.CONTENT_URI, null, true, UserHandle.USER_ALL); 1123 cr.notifyChange(Uri.parse("content://mms-sms/conversations/"), null, true, 1124 UserHandle.USER_ALL); 1125 if (notifyIfNotDefault) { 1126 ProviderUtil.notifyIfNotDefaultSmsApp(uri, callingPackage, context); 1127 } 1128 } 1129 1130 // Db open helper for tables stored in CE(Credential Encrypted) storage. 1131 @VisibleForTesting 1132 public SQLiteOpenHelper mCeOpenHelper; 1133 // Db open helper for tables stored in DE(Device Encrypted) storage. It's currently only used 1134 // to store raw table. 1135 @VisibleForTesting 1136 public SQLiteOpenHelper mDeOpenHelper; 1137 1138 private final static String TAG = "SmsProvider"; 1139 private final static String VND_ANDROID_SMS = "vnd.android.cursor.item/sms"; 1140 private final static String VND_ANDROID_SMSCHAT = 1141 "vnd.android.cursor.item/sms-chat"; 1142 private final static String VND_ANDROID_DIR_SMS = 1143 "vnd.android.cursor.dir/sms"; 1144 1145 private static final String[] sIDProjection = new String[] { "_id" }; 1146 1147 private static final int SMS_ALL = 0; 1148 private static final int SMS_ALL_ID = 1; 1149 private static final int SMS_INBOX = 2; 1150 private static final int SMS_INBOX_ID = 3; 1151 private static final int SMS_SENT = 4; 1152 private static final int SMS_SENT_ID = 5; 1153 private static final int SMS_DRAFT = 6; 1154 private static final int SMS_DRAFT_ID = 7; 1155 private static final int SMS_OUTBOX = 8; 1156 private static final int SMS_OUTBOX_ID = 9; 1157 private static final int SMS_CONVERSATIONS = 10; 1158 private static final int SMS_CONVERSATIONS_ID = 11; 1159 private static final int SMS_RAW_MESSAGE = 15; 1160 private static final int SMS_ATTACHMENT = 16; 1161 private static final int SMS_ATTACHMENT_ID = 17; 1162 private static final int SMS_NEW_THREAD_ID = 18; 1163 private static final int SMS_QUERY_THREAD_ID = 19; 1164 private static final int SMS_STATUS_ID = 20; 1165 private static final int SMS_STATUS_PENDING = 21; 1166 private static final int SMS_ALL_ICC = 22; 1167 private static final int SMS_ICC = 23; 1168 private static final int SMS_FAILED = 24; 1169 private static final int SMS_FAILED_ID = 25; 1170 private static final int SMS_QUEUED = 26; 1171 private static final int SMS_UNDELIVERED = 27; 1172 private static final int SMS_RAW_MESSAGE_PERMANENT_DELETE = 28; 1173 private static final int SMS_ALL_ICC_SUBID = 29; 1174 private static final int SMS_ICC_SUBID = 30; 1175 1176 private static final UriMatcher sURLMatcher = 1177 new UriMatcher(UriMatcher.NO_MATCH); 1178 1179 static { 1180 sURLMatcher.addURI("sms", null, SMS_ALL); 1181 sURLMatcher.addURI("sms", "#", SMS_ALL_ID); 1182 sURLMatcher.addURI("sms", "inbox", SMS_INBOX); 1183 sURLMatcher.addURI("sms", "inbox/#", SMS_INBOX_ID); 1184 sURLMatcher.addURI("sms", "sent", SMS_SENT); 1185 sURLMatcher.addURI("sms", "sent/#", SMS_SENT_ID); 1186 sURLMatcher.addURI("sms", "draft", SMS_DRAFT); 1187 sURLMatcher.addURI("sms", "draft/#", SMS_DRAFT_ID); 1188 sURLMatcher.addURI("sms", "outbox", SMS_OUTBOX); 1189 sURLMatcher.addURI("sms", "outbox/#", SMS_OUTBOX_ID); 1190 sURLMatcher.addURI("sms", "undelivered", SMS_UNDELIVERED); 1191 sURLMatcher.addURI("sms", "failed", SMS_FAILED); 1192 sURLMatcher.addURI("sms", "failed/#", SMS_FAILED_ID); 1193 sURLMatcher.addURI("sms", "queued", SMS_QUEUED); 1194 sURLMatcher.addURI("sms", "conversations", SMS_CONVERSATIONS); 1195 sURLMatcher.addURI("sms", "conversations/*", SMS_CONVERSATIONS_ID); 1196 sURLMatcher.addURI("sms", "raw", SMS_RAW_MESSAGE); 1197 sURLMatcher.addURI("sms", "raw/permanentDelete", SMS_RAW_MESSAGE_PERMANENT_DELETE); 1198 sURLMatcher.addURI("sms", "attachments", SMS_ATTACHMENT); 1199 sURLMatcher.addURI("sms", "attachments/#", SMS_ATTACHMENT_ID); 1200 sURLMatcher.addURI("sms", "threadID", SMS_NEW_THREAD_ID); 1201 sURLMatcher.addURI("sms", "threadID/*", SMS_QUERY_THREAD_ID); 1202 sURLMatcher.addURI("sms", "status/#", SMS_STATUS_ID); 1203 sURLMatcher.addURI("sms", "sr_pending", SMS_STATUS_PENDING); 1204 sURLMatcher.addURI("sms", "icc", SMS_ALL_ICC); 1205 sURLMatcher.addURI("sms", "icc/#", SMS_ICC); 1206 sURLMatcher.addURI("sms", "icc_subId/#", SMS_ALL_ICC_SUBID); 1207 sURLMatcher.addURI("sms", "icc_subId/#/#", SMS_ICC_SUBID); 1208 //we keep these for not breaking old applications 1209 sURLMatcher.addURI("sms", "sim", SMS_ALL_ICC); 1210 sURLMatcher.addURI("sms", "sim/#", SMS_ICC); 1211 } 1212 1213 /** 1214 * These methods can be overridden in a subclass for testing SmsProvider using an 1215 * in-memory database. 1216 */ getReadableDatabase(int match)1217 SQLiteDatabase getReadableDatabase(int match) { 1218 return getDBOpenHelper(match).getReadableDatabase(); 1219 } 1220 getWritableDatabase(int match)1221 SQLiteDatabase getWritableDatabase(int match) { 1222 return getDBOpenHelper(match).getWritableDatabase(); 1223 } 1224 } 1225