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.TextBasedSmsColumns; 40 import android.provider.Telephony.Threads; 41 import android.telephony.SmsManager; 42 import android.telephony.SmsMessage; 43 import android.text.TextUtils; 44 import android.util.Log; 45 46 import java.util.ArrayList; 47 import java.util.HashMap; 48 49 public class SmsProvider extends ContentProvider { 50 private static final Uri NOTIFICATION_URI = Uri.parse("content://sms"); 51 private static final Uri ICC_URI = Uri.parse("content://sms/icc"); 52 static final String TABLE_SMS = "sms"; 53 static final String TABLE_RAW = "raw"; 54 private static final String TABLE_SR_PENDING = "sr_pending"; 55 private static final String TABLE_WORDS = "words"; 56 static final String VIEW_SMS_RESTRICTED = "sms_restricted"; 57 58 private static final Integer ONE = Integer.valueOf(1); 59 60 private static final String[] CONTACT_QUERY_PROJECTION = 61 new String[] { Contacts.Phones.PERSON_ID }; 62 private static final int PERSON_ID_COLUMN = 0; 63 64 /** Delete any raw messages or message segments marked deleted that are older than an hour. */ 65 static final long RAW_MESSAGE_EXPIRE_AGE_MS = (long) (60 * 60 * 1000); 66 67 /** 68 * These are the columns that are available when reading SMS 69 * messages from the ICC. Columns whose names begin with "is_" 70 * have either "true" or "false" as their values. 71 */ 72 private final static String[] ICC_COLUMNS = new String[] { 73 // N.B.: These columns must appear in the same order as the 74 // calls to add appear in convertIccToSms. 75 "service_center_address", // getServiceCenterAddress 76 "address", // getDisplayOriginatingAddress 77 "message_class", // getMessageClass 78 "body", // getDisplayMessageBody 79 "date", // getTimestampMillis 80 "status", // getStatusOnIcc 81 "index_on_icc", // getIndexOnIcc 82 "is_status_report", // isStatusReportMessage 83 "transport_type", // Always "sms". 84 "type", // Always MESSAGE_TYPE_ALL. 85 "locked", // Always 0 (false). 86 "error_code", // Always 0 87 "_id" 88 }; 89 90 @Override 91 public boolean onCreate() { 92 setAppOps(AppOpsManager.OP_READ_SMS, AppOpsManager.OP_WRITE_SMS); 93 mDeOpenHelper = MmsSmsDatabaseHelper.getInstanceForDe(getContext()); 94 mCeOpenHelper = MmsSmsDatabaseHelper.getInstanceForCe(getContext()); 95 TelephonyBackupAgent.DeferredSmsMmsRestoreService.startIfFilesExist(getContext()); 96 return true; 97 } 98 99 /** 100 * Return the proper view of "sms" table for the current access status. 101 * 102 * @param accessRestricted If the access is restricted 103 * @return the table/view name of the "sms" data 104 */ 105 public static String getSmsTable(boolean accessRestricted) { 106 return accessRestricted ? VIEW_SMS_RESTRICTED : TABLE_SMS; 107 } 108 109 @Override 110 public Cursor query(Uri url, String[] projectionIn, String selection, 111 String[] selectionArgs, String sort) { 112 // First check if a restricted view of the "sms" table should be used based on the 113 // caller's identity. Only system, phone or the default sms app can have full access 114 // of sms data. For other apps, we present a restricted view which only contains sent 115 // or received messages. 116 final boolean accessRestricted = ProviderUtil.isAccessRestricted( 117 getContext(), getCallingPackage(), Binder.getCallingUid()); 118 final String smsTable = getSmsTable(accessRestricted); 119 SQLiteQueryBuilder qb = new SQLiteQueryBuilder(); 120 121 // Generate the body of the query. 122 int match = sURLMatcher.match(url); 123 SQLiteDatabase db = getDBOpenHelper(match).getReadableDatabase(); 124 switch (match) { 125 case SMS_ALL: 126 constructQueryForBox(qb, Sms.MESSAGE_TYPE_ALL, smsTable); 127 break; 128 129 case SMS_UNDELIVERED: 130 constructQueryForUndelivered(qb, smsTable); 131 break; 132 133 case SMS_FAILED: 134 constructQueryForBox(qb, Sms.MESSAGE_TYPE_FAILED, smsTable); 135 break; 136 137 case SMS_QUEUED: 138 constructQueryForBox(qb, Sms.MESSAGE_TYPE_QUEUED, smsTable); 139 break; 140 141 case SMS_INBOX: 142 constructQueryForBox(qb, Sms.MESSAGE_TYPE_INBOX, smsTable); 143 break; 144 145 case SMS_SENT: 146 constructQueryForBox(qb, Sms.MESSAGE_TYPE_SENT, smsTable); 147 break; 148 149 case SMS_DRAFT: 150 constructQueryForBox(qb, Sms.MESSAGE_TYPE_DRAFT, smsTable); 151 break; 152 153 case SMS_OUTBOX: 154 constructQueryForBox(qb, Sms.MESSAGE_TYPE_OUTBOX, smsTable); 155 break; 156 157 case SMS_ALL_ID: 158 qb.setTables(smsTable); 159 qb.appendWhere("(_id = " + url.getPathSegments().get(0) + ")"); 160 break; 161 162 case SMS_INBOX_ID: 163 case SMS_FAILED_ID: 164 case SMS_SENT_ID: 165 case SMS_DRAFT_ID: 166 case SMS_OUTBOX_ID: 167 qb.setTables(smsTable); 168 qb.appendWhere("(_id = " + url.getPathSegments().get(1) + ")"); 169 break; 170 171 case SMS_CONVERSATIONS_ID: 172 int threadID; 173 174 try { 175 threadID = Integer.parseInt(url.getPathSegments().get(1)); 176 if (Log.isLoggable(TAG, Log.VERBOSE)) { 177 Log.d(TAG, "query conversations: threadID=" + threadID); 178 } 179 } 180 catch (Exception ex) { 181 Log.e(TAG, 182 "Bad conversation thread id: " 183 + url.getPathSegments().get(1)); 184 return null; 185 } 186 187 qb.setTables(smsTable); 188 qb.appendWhere("thread_id = " + threadID); 189 break; 190 191 case SMS_CONVERSATIONS: 192 qb.setTables(smsTable + ", " 193 + "(SELECT thread_id AS group_thread_id, " 194 + "MAX(date) AS group_date, " 195 + "COUNT(*) AS msg_count " 196 + "FROM " + smsTable + " " 197 + "GROUP BY thread_id) AS groups"); 198 qb.appendWhere(smsTable + ".thread_id=groups.group_thread_id" 199 + " AND " + smsTable + ".date=groups.group_date"); 200 final HashMap<String, String> projectionMap = new HashMap<>(); 201 projectionMap.put(Sms.Conversations.SNIPPET, 202 smsTable + ".body AS snippet"); 203 projectionMap.put(Sms.Conversations.THREAD_ID, 204 smsTable + ".thread_id AS thread_id"); 205 projectionMap.put(Sms.Conversations.MESSAGE_COUNT, 206 "groups.msg_count AS msg_count"); 207 projectionMap.put("delta", null); 208 qb.setProjectionMap(projectionMap); 209 break; 210 211 case SMS_RAW_MESSAGE: 212 // before querying purge old entries with deleted = 1 213 purgeDeletedMessagesInRawTable(db); 214 qb.setTables("raw"); 215 break; 216 217 case SMS_STATUS_PENDING: 218 qb.setTables("sr_pending"); 219 break; 220 221 case SMS_ATTACHMENT: 222 qb.setTables("attachments"); 223 break; 224 225 case SMS_ATTACHMENT_ID: 226 qb.setTables("attachments"); 227 qb.appendWhere( 228 "(sms_id = " + url.getPathSegments().get(1) + ")"); 229 break; 230 231 case SMS_QUERY_THREAD_ID: 232 qb.setTables("canonical_addresses"); 233 if (projectionIn == null) { 234 projectionIn = sIDProjection; 235 } 236 break; 237 238 case SMS_STATUS_ID: 239 qb.setTables(smsTable); 240 qb.appendWhere("(_id = " + url.getPathSegments().get(1) + ")"); 241 break; 242 243 case SMS_ALL_ICC: 244 return getAllMessagesFromIcc(); 245 246 case SMS_ICC: 247 String messageIndexString = url.getPathSegments().get(1); 248 249 return getSingleMessageFromIcc(messageIndexString); 250 251 default: 252 Log.e(TAG, "Invalid request: " + url); 253 return null; 254 } 255 256 String orderBy = null; 257 258 if (!TextUtils.isEmpty(sort)) { 259 orderBy = sort; 260 } else if (qb.getTables().equals(smsTable)) { 261 orderBy = Sms.DEFAULT_SORT_ORDER; 262 } 263 264 Cursor ret = qb.query(db, projectionIn, selection, selectionArgs, 265 null, null, orderBy); 266 267 // TODO: Since the URLs are a mess, always use content://sms 268 ret.setNotificationUri(getContext().getContentResolver(), 269 NOTIFICATION_URI); 270 return ret; 271 } 272 273 private void purgeDeletedMessagesInRawTable(SQLiteDatabase db) { 274 long oldTimestamp = System.currentTimeMillis() - RAW_MESSAGE_EXPIRE_AGE_MS; 275 int num = db.delete(TABLE_RAW, "deleted = 1 AND date < " + oldTimestamp, null); 276 if (Log.isLoggable(TAG, Log.VERBOSE)) { 277 Log.d(TAG, "purgeDeletedMessagesInRawTable: num rows older than " + oldTimestamp + 278 " purged: " + num); 279 } 280 } 281 282 private SQLiteOpenHelper getDBOpenHelper(int match) { 283 if (match == SMS_RAW_MESSAGE) { 284 return mDeOpenHelper; 285 } 286 return mCeOpenHelper; 287 } 288 289 private Object[] convertIccToSms(SmsMessage message, int id) { 290 // N.B.: These calls must appear in the same order as the 291 // columns appear in ICC_COLUMNS. 292 Object[] row = new Object[13]; 293 row[0] = message.getServiceCenterAddress(); 294 row[1] = message.getDisplayOriginatingAddress(); 295 row[2] = String.valueOf(message.getMessageClass()); 296 row[3] = message.getDisplayMessageBody(); 297 row[4] = message.getTimestampMillis(); 298 row[5] = Sms.STATUS_NONE; 299 row[6] = message.getIndexOnIcc(); 300 row[7] = message.isStatusReportMessage(); 301 row[8] = "sms"; 302 row[9] = TextBasedSmsColumns.MESSAGE_TYPE_ALL; 303 row[10] = 0; // locked 304 row[11] = 0; // error_code 305 row[12] = id; 306 return row; 307 } 308 309 /** 310 * Return a Cursor containing just one message from the ICC. 311 */ 312 private Cursor getSingleMessageFromIcc(String messageIndexString) { 313 int messageIndex = -1; 314 try { 315 Integer.parseInt(messageIndexString); 316 } catch (NumberFormatException exception) { 317 throw new IllegalArgumentException("Bad SMS ICC ID: " + messageIndexString); 318 } 319 ArrayList<SmsMessage> messages; 320 final SmsManager smsManager = SmsManager.getDefault(); 321 // Use phone id to avoid AppOps uid mismatch in telephony 322 long token = Binder.clearCallingIdentity(); 323 try { 324 messages = smsManager.getAllMessagesFromIcc(); 325 } finally { 326 Binder.restoreCallingIdentity(token); 327 } 328 if (messages == null) { 329 throw new IllegalArgumentException("ICC message not retrieved"); 330 } 331 final SmsMessage message = messages.get(messageIndex); 332 if (message == null) { 333 throw new IllegalArgumentException( 334 "Message not retrieved. ID: " + messageIndexString); 335 } 336 MatrixCursor cursor = new MatrixCursor(ICC_COLUMNS, 1); 337 cursor.addRow(convertIccToSms(message, 0)); 338 return withIccNotificationUri(cursor); 339 } 340 341 /** 342 * Return a Cursor listing all the messages stored on the ICC. 343 */ 344 private Cursor getAllMessagesFromIcc() { 345 SmsManager smsManager = SmsManager.getDefault(); 346 ArrayList<SmsMessage> messages; 347 348 // use phone app permissions to avoid UID mismatch in AppOpsManager.noteOp() call 349 long token = Binder.clearCallingIdentity(); 350 try { 351 messages = smsManager.getAllMessagesFromIcc(); 352 } finally { 353 Binder.restoreCallingIdentity(token); 354 } 355 356 final int count = messages.size(); 357 MatrixCursor cursor = new MatrixCursor(ICC_COLUMNS, count); 358 for (int i = 0; i < count; i++) { 359 SmsMessage message = messages.get(i); 360 if (message != null) { 361 cursor.addRow(convertIccToSms(message, i)); 362 } 363 } 364 return withIccNotificationUri(cursor); 365 } 366 367 private Cursor withIccNotificationUri(Cursor cursor) { 368 cursor.setNotificationUri(getContext().getContentResolver(), ICC_URI); 369 return cursor; 370 } 371 372 private void constructQueryForBox(SQLiteQueryBuilder qb, int type, String smsTable) { 373 qb.setTables(smsTable); 374 375 if (type != Sms.MESSAGE_TYPE_ALL) { 376 qb.appendWhere("type=" + type); 377 } 378 } 379 380 private void constructQueryForUndelivered(SQLiteQueryBuilder qb, String smsTable) { 381 qb.setTables(smsTable); 382 383 qb.appendWhere("(type=" + Sms.MESSAGE_TYPE_OUTBOX + 384 " OR type=" + Sms.MESSAGE_TYPE_FAILED + 385 " OR type=" + Sms.MESSAGE_TYPE_QUEUED + ")"); 386 } 387 388 @Override 389 public String getType(Uri url) { 390 switch (url.getPathSegments().size()) { 391 case 0: 392 return VND_ANDROID_DIR_SMS; 393 case 1: 394 try { 395 Integer.parseInt(url.getPathSegments().get(0)); 396 return VND_ANDROID_SMS; 397 } catch (NumberFormatException ex) { 398 return VND_ANDROID_DIR_SMS; 399 } 400 case 2: 401 // TODO: What about "threadID"? 402 if (url.getPathSegments().get(0).equals("conversations")) { 403 return VND_ANDROID_SMSCHAT; 404 } else { 405 return VND_ANDROID_SMS; 406 } 407 } 408 return null; 409 } 410 411 @Override 412 public int bulkInsert(@NonNull Uri url, @NonNull ContentValues[] values) { 413 final int callerUid = Binder.getCallingUid(); 414 final String callerPkg = getCallingPackage(); 415 long token = Binder.clearCallingIdentity(); 416 try { 417 int messagesInserted = 0; 418 for (ContentValues initialValues : values) { 419 Uri insertUri = insertInner(url, initialValues, callerUid, callerPkg); 420 if (insertUri != null) { 421 messagesInserted++; 422 } 423 } 424 425 // The raw table is used by the telephony layer for storing an sms before 426 // sending out a notification that an sms has arrived. We don't want to notify 427 // the default sms app of changes to this table. 428 final boolean notifyIfNotDefault = sURLMatcher.match(url) != SMS_RAW_MESSAGE; 429 notifyChange(notifyIfNotDefault, url, callerPkg); 430 return messagesInserted; 431 } finally { 432 Binder.restoreCallingIdentity(token); 433 } 434 } 435 436 @Override 437 public Uri insert(Uri url, ContentValues initialValues) { 438 final int callerUid = Binder.getCallingUid(); 439 final String callerPkg = getCallingPackage(); 440 long token = Binder.clearCallingIdentity(); 441 try { 442 Uri insertUri = insertInner(url, initialValues, callerUid, callerPkg); 443 444 // The raw table is used by the telephony layer for storing an sms before 445 // sending out a notification that an sms has arrived. We don't want to notify 446 // the default sms app of changes to this table. 447 final boolean notifyIfNotDefault = sURLMatcher.match(url) != SMS_RAW_MESSAGE; 448 notifyChange(notifyIfNotDefault, insertUri, callerPkg); 449 return insertUri; 450 } finally { 451 Binder.restoreCallingIdentity(token); 452 } 453 } 454 455 private Uri insertInner(Uri url, ContentValues initialValues, int callerUid, String callerPkg) { 456 ContentValues values; 457 long rowID; 458 int type = Sms.MESSAGE_TYPE_ALL; 459 460 int match = sURLMatcher.match(url); 461 String table = TABLE_SMS; 462 boolean notifyIfNotDefault = true; 463 464 switch (match) { 465 case SMS_ALL: 466 Integer typeObj = initialValues.getAsInteger(Sms.TYPE); 467 if (typeObj != null) { 468 type = typeObj.intValue(); 469 } else { 470 // default to inbox 471 type = Sms.MESSAGE_TYPE_INBOX; 472 } 473 break; 474 475 case SMS_INBOX: 476 type = Sms.MESSAGE_TYPE_INBOX; 477 break; 478 479 case SMS_FAILED: 480 type = Sms.MESSAGE_TYPE_FAILED; 481 break; 482 483 case SMS_QUEUED: 484 type = Sms.MESSAGE_TYPE_QUEUED; 485 break; 486 487 case SMS_SENT: 488 type = Sms.MESSAGE_TYPE_SENT; 489 break; 490 491 case SMS_DRAFT: 492 type = Sms.MESSAGE_TYPE_DRAFT; 493 break; 494 495 case SMS_OUTBOX: 496 type = Sms.MESSAGE_TYPE_OUTBOX; 497 break; 498 499 case SMS_RAW_MESSAGE: 500 table = "raw"; 501 // The raw table is used by the telephony layer for storing an sms before 502 // sending out a notification that an sms has arrived. We don't want to notify 503 // the default sms app of changes to this table. 504 notifyIfNotDefault = false; 505 break; 506 507 case SMS_STATUS_PENDING: 508 table = "sr_pending"; 509 break; 510 511 case SMS_ATTACHMENT: 512 table = "attachments"; 513 break; 514 515 case SMS_NEW_THREAD_ID: 516 table = "canonical_addresses"; 517 break; 518 519 default: 520 Log.e(TAG, "Invalid request: " + url); 521 return null; 522 } 523 524 SQLiteDatabase db = getDBOpenHelper(match).getWritableDatabase(); 525 526 if (table.equals(TABLE_SMS)) { 527 boolean addDate = false; 528 boolean addType = false; 529 530 // Make sure that the date and type are set 531 if (initialValues == null) { 532 values = new ContentValues(1); 533 addDate = true; 534 addType = true; 535 } else { 536 values = new ContentValues(initialValues); 537 538 if (!initialValues.containsKey(Sms.DATE)) { 539 addDate = true; 540 } 541 542 if (!initialValues.containsKey(Sms.TYPE)) { 543 addType = true; 544 } 545 } 546 547 if (addDate) { 548 values.put(Sms.DATE, new Long(System.currentTimeMillis())); 549 } 550 551 if (addType && (type != Sms.MESSAGE_TYPE_ALL)) { 552 values.put(Sms.TYPE, Integer.valueOf(type)); 553 } 554 555 // thread_id 556 Long threadId = values.getAsLong(Sms.THREAD_ID); 557 String address = values.getAsString(Sms.ADDRESS); 558 559 if (((threadId == null) || (threadId == 0)) && (!TextUtils.isEmpty(address))) { 560 values.put(Sms.THREAD_ID, Threads.getOrCreateThreadId( 561 getContext(), address)); 562 } 563 564 // If this message is going in as a draft, it should replace any 565 // other draft messages in the thread. Just delete all draft 566 // messages with this thread ID. We could add an OR REPLACE to 567 // the insert below, but we'd have to query to find the old _id 568 // to produce a conflict anyway. 569 if (values.getAsInteger(Sms.TYPE) == Sms.MESSAGE_TYPE_DRAFT) { 570 db.delete(TABLE_SMS, "thread_id=? AND type=?", 571 new String[] { values.getAsString(Sms.THREAD_ID), 572 Integer.toString(Sms.MESSAGE_TYPE_DRAFT) }); 573 } 574 575 if (type == Sms.MESSAGE_TYPE_INBOX) { 576 // Look up the person if not already filled in. 577 if ((values.getAsLong(Sms.PERSON) == null) && (!TextUtils.isEmpty(address))) { 578 Cursor cursor = null; 579 Uri uri = Uri.withAppendedPath(Contacts.Phones.CONTENT_FILTER_URL, 580 Uri.encode(address)); 581 try { 582 cursor = getContext().getContentResolver().query( 583 uri, 584 CONTACT_QUERY_PROJECTION, 585 null, null, null); 586 587 if (cursor.moveToFirst()) { 588 Long id = Long.valueOf(cursor.getLong(PERSON_ID_COLUMN)); 589 values.put(Sms.PERSON, id); 590 } 591 } catch (Exception ex) { 592 Log.e(TAG, "insert: query contact uri " + uri + " caught ", ex); 593 } finally { 594 if (cursor != null) { 595 cursor.close(); 596 } 597 } 598 } 599 } else { 600 // Mark all non-inbox messages read. 601 values.put(Sms.READ, ONE); 602 } 603 if (ProviderUtil.shouldSetCreator(values, callerUid)) { 604 // Only SYSTEM or PHONE can set CREATOR 605 // If caller is not SYSTEM or PHONE, or SYSTEM or PHONE does not set CREATOR 606 // set CREATOR using the truth on caller. 607 // Note: Inferring package name from UID may include unrelated package names 608 values.put(Sms.CREATOR, callerPkg); 609 } 610 } else { 611 if (initialValues == null) { 612 values = new ContentValues(1); 613 } else { 614 values = initialValues; 615 } 616 } 617 618 rowID = db.insert(table, "body", values); 619 620 // Don't use a trigger for updating the words table because of a bug 621 // in FTS3. The bug is such that the call to get the last inserted 622 // row is incorrect. 623 if (table == TABLE_SMS) { 624 // Update the words table with a corresponding row. The words table 625 // allows us to search for words quickly, without scanning the whole 626 // table; 627 ContentValues cv = new ContentValues(); 628 cv.put(Telephony.MmsSms.WordsTable.ID, rowID); 629 cv.put(Telephony.MmsSms.WordsTable.INDEXED_TEXT, values.getAsString("body")); 630 cv.put(Telephony.MmsSms.WordsTable.SOURCE_ROW_ID, rowID); 631 cv.put(Telephony.MmsSms.WordsTable.TABLE_ID, 1); 632 db.insert(TABLE_WORDS, Telephony.MmsSms.WordsTable.INDEXED_TEXT, cv); 633 } 634 if (rowID > 0) { 635 Uri uri = Uri.parse("content://" + table + "/" + rowID); 636 637 if (Log.isLoggable(TAG, Log.VERBOSE)) { 638 Log.d(TAG, "insert " + uri + " succeeded"); 639 } 640 return uri; 641 } else { 642 Log.e(TAG, "insert: failed!"); 643 } 644 645 return null; 646 } 647 648 @Override 649 public int delete(Uri url, String where, String[] whereArgs) { 650 int count; 651 int match = sURLMatcher.match(url); 652 SQLiteDatabase db = getDBOpenHelper(match).getWritableDatabase(); 653 boolean notifyIfNotDefault = true; 654 switch (match) { 655 case SMS_ALL: 656 count = db.delete(TABLE_SMS, where, whereArgs); 657 if (count != 0) { 658 // Don't update threads unless something changed. 659 MmsSmsDatabaseHelper.updateAllThreads(db, where, whereArgs); 660 } 661 break; 662 663 case SMS_ALL_ID: 664 try { 665 int message_id = Integer.parseInt(url.getPathSegments().get(0)); 666 count = MmsSmsDatabaseHelper.deleteOneSms(db, message_id); 667 } catch (Exception e) { 668 throw new IllegalArgumentException( 669 "Bad message id: " + url.getPathSegments().get(0)); 670 } 671 break; 672 673 case SMS_CONVERSATIONS_ID: 674 int threadID; 675 676 try { 677 threadID = Integer.parseInt(url.getPathSegments().get(1)); 678 } catch (Exception ex) { 679 throw new IllegalArgumentException( 680 "Bad conversation thread id: " 681 + url.getPathSegments().get(1)); 682 } 683 684 // delete the messages from the sms table 685 where = DatabaseUtils.concatenateWhere("thread_id=" + threadID, where); 686 count = db.delete(TABLE_SMS, where, whereArgs); 687 MmsSmsDatabaseHelper.updateThread(db, threadID); 688 break; 689 690 case SMS_RAW_MESSAGE: 691 ContentValues cv = new ContentValues(); 692 cv.put("deleted", 1); 693 count = db.update(TABLE_RAW, cv, where, whereArgs); 694 if (Log.isLoggable(TAG, Log.VERBOSE)) { 695 Log.d(TAG, "delete: num rows marked deleted in raw table: " + count); 696 } 697 notifyIfNotDefault = false; 698 break; 699 700 case SMS_RAW_MESSAGE_PERMANENT_DELETE: 701 count = db.delete(TABLE_RAW, where, whereArgs); 702 if (Log.isLoggable(TAG, Log.VERBOSE)) { 703 Log.d(TAG, "delete: num rows permanently deleted in raw table: " + count); 704 } 705 notifyIfNotDefault = false; 706 break; 707 708 case SMS_STATUS_PENDING: 709 count = db.delete("sr_pending", where, whereArgs); 710 break; 711 712 case SMS_ICC: 713 String messageIndexString = url.getPathSegments().get(1); 714 715 return deleteMessageFromIcc(messageIndexString); 716 717 default: 718 throw new IllegalArgumentException("Unknown URL"); 719 } 720 721 if (count > 0) { 722 notifyChange(notifyIfNotDefault, url, getCallingPackage()); 723 } 724 return count; 725 } 726 727 /** 728 * Delete the message at index from ICC. Return true iff 729 * successful. 730 */ 731 private int deleteMessageFromIcc(String messageIndexString) { 732 SmsManager smsManager = SmsManager.getDefault(); 733 // Use phone id to avoid AppOps uid mismatch in telephony 734 long token = Binder.clearCallingIdentity(); 735 try { 736 return smsManager.deleteMessageFromIcc( 737 Integer.parseInt(messageIndexString)) 738 ? 1 : 0; 739 } catch (NumberFormatException exception) { 740 throw new IllegalArgumentException( 741 "Bad SMS ICC ID: " + messageIndexString); 742 } finally { 743 ContentResolver cr = getContext().getContentResolver(); 744 cr.notifyChange(ICC_URI, null, true, UserHandle.USER_ALL); 745 746 Binder.restoreCallingIdentity(token); 747 } 748 } 749 750 @Override 751 public int update(Uri url, ContentValues values, String where, String[] whereArgs) { 752 final int callerUid = Binder.getCallingUid(); 753 final String callerPkg = getCallingPackage(); 754 int count = 0; 755 String table = TABLE_SMS; 756 String extraWhere = null; 757 boolean notifyIfNotDefault = true; 758 int match = sURLMatcher.match(url); 759 SQLiteDatabase db = getDBOpenHelper(match).getWritableDatabase(); 760 761 switch (match) { 762 case SMS_RAW_MESSAGE: 763 table = TABLE_RAW; 764 notifyIfNotDefault = false; 765 break; 766 767 case SMS_STATUS_PENDING: 768 table = TABLE_SR_PENDING; 769 break; 770 771 case SMS_ALL: 772 case SMS_FAILED: 773 case SMS_QUEUED: 774 case SMS_INBOX: 775 case SMS_SENT: 776 case SMS_DRAFT: 777 case SMS_OUTBOX: 778 case SMS_CONVERSATIONS: 779 break; 780 781 case SMS_ALL_ID: 782 extraWhere = "_id=" + url.getPathSegments().get(0); 783 break; 784 785 case SMS_INBOX_ID: 786 case SMS_FAILED_ID: 787 case SMS_SENT_ID: 788 case SMS_DRAFT_ID: 789 case SMS_OUTBOX_ID: 790 extraWhere = "_id=" + url.getPathSegments().get(1); 791 break; 792 793 case SMS_CONVERSATIONS_ID: { 794 String threadId = url.getPathSegments().get(1); 795 796 try { 797 Integer.parseInt(threadId); 798 } catch (Exception ex) { 799 Log.e(TAG, "Bad conversation thread id: " + threadId); 800 break; 801 } 802 803 extraWhere = "thread_id=" + threadId; 804 break; 805 } 806 807 case SMS_STATUS_ID: 808 extraWhere = "_id=" + url.getPathSegments().get(1); 809 break; 810 811 default: 812 throw new UnsupportedOperationException( 813 "URI " + url + " not supported"); 814 } 815 816 if (table.equals(TABLE_SMS) && ProviderUtil.shouldRemoveCreator(values, callerUid)) { 817 // CREATOR should not be changed by non-SYSTEM/PHONE apps 818 Log.w(TAG, callerPkg + " tries to update CREATOR"); 819 values.remove(Sms.CREATOR); 820 } 821 822 where = DatabaseUtils.concatenateWhere(where, extraWhere); 823 count = db.update(table, values, where, whereArgs); 824 825 if (count > 0) { 826 if (Log.isLoggable(TAG, Log.VERBOSE)) { 827 Log.d(TAG, "update " + url + " succeeded"); 828 } 829 notifyChange(notifyIfNotDefault, url, callerPkg); 830 } 831 return count; 832 } 833 834 private void notifyChange(boolean notifyIfNotDefault, Uri uri, final String callingPackage) { 835 final Context context = getContext(); 836 ContentResolver cr = context.getContentResolver(); 837 cr.notifyChange(uri, null, true, UserHandle.USER_ALL); 838 cr.notifyChange(MmsSms.CONTENT_URI, null, true, UserHandle.USER_ALL); 839 cr.notifyChange(Uri.parse("content://mms-sms/conversations/"), null, true, 840 UserHandle.USER_ALL); 841 if (notifyIfNotDefault) { 842 ProviderUtil.notifyIfNotDefaultSmsApp(uri, callingPackage, context); 843 } 844 } 845 846 // Db open helper for tables stored in CE(Credential Encrypted) storage. 847 private SQLiteOpenHelper mCeOpenHelper; 848 // Db open helper for tables stored in DE(Device Encrypted) storage. 849 private SQLiteOpenHelper mDeOpenHelper; 850 851 private final static String TAG = "SmsProvider"; 852 private final static String VND_ANDROID_SMS = "vnd.android.cursor.item/sms"; 853 private final static String VND_ANDROID_SMSCHAT = 854 "vnd.android.cursor.item/sms-chat"; 855 private final static String VND_ANDROID_DIR_SMS = 856 "vnd.android.cursor.dir/sms"; 857 858 private static final String[] sIDProjection = new String[] { "_id" }; 859 860 private static final int SMS_ALL = 0; 861 private static final int SMS_ALL_ID = 1; 862 private static final int SMS_INBOX = 2; 863 private static final int SMS_INBOX_ID = 3; 864 private static final int SMS_SENT = 4; 865 private static final int SMS_SENT_ID = 5; 866 private static final int SMS_DRAFT = 6; 867 private static final int SMS_DRAFT_ID = 7; 868 private static final int SMS_OUTBOX = 8; 869 private static final int SMS_OUTBOX_ID = 9; 870 private static final int SMS_CONVERSATIONS = 10; 871 private static final int SMS_CONVERSATIONS_ID = 11; 872 private static final int SMS_RAW_MESSAGE = 15; 873 private static final int SMS_ATTACHMENT = 16; 874 private static final int SMS_ATTACHMENT_ID = 17; 875 private static final int SMS_NEW_THREAD_ID = 18; 876 private static final int SMS_QUERY_THREAD_ID = 19; 877 private static final int SMS_STATUS_ID = 20; 878 private static final int SMS_STATUS_PENDING = 21; 879 private static final int SMS_ALL_ICC = 22; 880 private static final int SMS_ICC = 23; 881 private static final int SMS_FAILED = 24; 882 private static final int SMS_FAILED_ID = 25; 883 private static final int SMS_QUEUED = 26; 884 private static final int SMS_UNDELIVERED = 27; 885 private static final int SMS_RAW_MESSAGE_PERMANENT_DELETE = 28; 886 887 private static final UriMatcher sURLMatcher = 888 new UriMatcher(UriMatcher.NO_MATCH); 889 890 static { 891 sURLMatcher.addURI("sms", null, SMS_ALL); 892 sURLMatcher.addURI("sms", "#", SMS_ALL_ID); 893 sURLMatcher.addURI("sms", "inbox", SMS_INBOX); 894 sURLMatcher.addURI("sms", "inbox/#", SMS_INBOX_ID); 895 sURLMatcher.addURI("sms", "sent", SMS_SENT); 896 sURLMatcher.addURI("sms", "sent/#", SMS_SENT_ID); 897 sURLMatcher.addURI("sms", "draft", SMS_DRAFT); 898 sURLMatcher.addURI("sms", "draft/#", SMS_DRAFT_ID); 899 sURLMatcher.addURI("sms", "outbox", SMS_OUTBOX); 900 sURLMatcher.addURI("sms", "outbox/#", SMS_OUTBOX_ID); 901 sURLMatcher.addURI("sms", "undelivered", SMS_UNDELIVERED); 902 sURLMatcher.addURI("sms", "failed", SMS_FAILED); 903 sURLMatcher.addURI("sms", "failed/#", SMS_FAILED_ID); 904 sURLMatcher.addURI("sms", "queued", SMS_QUEUED); 905 sURLMatcher.addURI("sms", "conversations", SMS_CONVERSATIONS); 906 sURLMatcher.addURI("sms", "conversations/*", SMS_CONVERSATIONS_ID); 907 sURLMatcher.addURI("sms", "raw", SMS_RAW_MESSAGE); 908 sURLMatcher.addURI("sms", "raw/permanentDelete", SMS_RAW_MESSAGE_PERMANENT_DELETE); 909 sURLMatcher.addURI("sms", "attachments", SMS_ATTACHMENT); 910 sURLMatcher.addURI("sms", "attachments/#", SMS_ATTACHMENT_ID); 911 sURLMatcher.addURI("sms", "threadID", SMS_NEW_THREAD_ID); 912 sURLMatcher.addURI("sms", "threadID/*", SMS_QUERY_THREAD_ID); 913 sURLMatcher.addURI("sms", "status/#", SMS_STATUS_ID); 914 sURLMatcher.addURI("sms", "sr_pending", SMS_STATUS_PENDING); 915 sURLMatcher.addURI("sms", "icc", SMS_ALL_ICC); 916 sURLMatcher.addURI("sms", "icc/#", SMS_ICC); 917 //we keep these for not breaking old applications 918 sURLMatcher.addURI("sms", "sim", SMS_ALL_ICC); 919 sURLMatcher.addURI("sms", "sim/#", SMS_ICC); 920 } 921 } 922