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