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