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