1 /* 2 * Copyright (C) 2008 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.ContentValues; 22 import android.content.Context; 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.Bundle; 33 import android.os.UserHandle; 34 import android.provider.BaseColumns; 35 import android.provider.Telephony; 36 import android.provider.Telephony.CanonicalAddressesColumns; 37 import android.provider.Telephony.Mms; 38 import android.provider.Telephony.MmsSms; 39 import android.provider.Telephony.MmsSms.PendingMessages; 40 import android.provider.Telephony.Sms; 41 import android.provider.Telephony.Sms.Conversations; 42 import android.provider.Telephony.Threads; 43 import android.provider.Telephony.ThreadsColumns; 44 import android.telephony.SmsManager; 45 import android.telephony.SubscriptionManager; 46 import android.text.TextUtils; 47 import android.util.Log; 48 49 import com.android.internal.telephony.TelephonyStatsLog; 50 import com.android.internal.telephony.util.TelephonyUtils; 51 52 import com.google.android.mms.pdu.PduHeaders; 53 54 import java.io.FileDescriptor; 55 import java.io.PrintWriter; 56 import java.util.Arrays; 57 import java.util.HashSet; 58 import java.util.List; 59 import java.util.Locale; 60 import java.util.Set; 61 62 /** 63 * This class provides the ability to query the MMS and SMS databases 64 * at the same time, mixing messages from both in a single thread 65 * (A.K.A. conversation). 66 * 67 * A virtual column, MmsSms.TYPE_DISCRIMINATOR_COLUMN, may be 68 * requested in the projection for a query. Its value is either "mms" 69 * or "sms", depending on whether the message represented by the row 70 * is an MMS message or an SMS message, respectively. 71 * 72 * This class also provides the ability to find out what addresses 73 * participated in a particular thread. It doesn't support updates 74 * for either of these. 75 * 76 * This class provides a way to allocate and retrieve thread IDs. 77 * This is done atomically through a query. There is no insert URI 78 * for this. 79 * 80 * Finally, this class provides a way to delete or update all messages 81 * in a thread. 82 */ 83 public class MmsSmsProvider extends ContentProvider { 84 private static final UriMatcher URI_MATCHER = 85 new UriMatcher(UriMatcher.NO_MATCH); 86 private static final String LOG_TAG = "MmsSmsProvider"; 87 private static final boolean DEBUG = false; 88 private static final int MULTIPLE_THREAD_IDS_FOUND = TelephonyStatsLog 89 .MMS_SMS_PROVIDER_GET_THREAD_ID_FAILED__FAILURE_CODE__FAILURE_MULTIPLE_THREAD_IDS_FOUND; 90 private static final int FAILURE_FIND_OR_CREATE_THREAD_ID_SQL = TelephonyStatsLog 91 .MMS_SMS_PROVIDER_GET_THREAD_ID_FAILED__FAILURE_CODE__FAILURE_FIND_OR_CREATE_THREAD_ID_SQL; 92 93 private static final String NO_DELETES_INSERTS_OR_UPDATES = 94 "MmsSmsProvider does not support deletes, inserts, or updates for this URI."; 95 private static final int URI_CONVERSATIONS = 0; 96 private static final int URI_CONVERSATIONS_MESSAGES = 1; 97 private static final int URI_CONVERSATIONS_RECIPIENTS = 2; 98 private static final int URI_MESSAGES_BY_PHONE = 3; 99 private static final int URI_THREAD_ID = 4; 100 private static final int URI_CANONICAL_ADDRESS = 5; 101 private static final int URI_PENDING_MSG = 6; 102 private static final int URI_COMPLETE_CONVERSATIONS = 7; 103 private static final int URI_UNDELIVERED_MSG = 8; 104 private static final int URI_CONVERSATIONS_SUBJECT = 9; 105 private static final int URI_NOTIFICATIONS = 10; 106 private static final int URI_OBSOLETE_THREADS = 11; 107 private static final int URI_DRAFT = 12; 108 private static final int URI_CANONICAL_ADDRESSES = 13; 109 private static final int URI_SEARCH = 14; 110 private static final int URI_SEARCH_SUGGEST = 15; 111 private static final int URI_FIRST_LOCKED_MESSAGE_ALL = 16; 112 private static final int URI_FIRST_LOCKED_MESSAGE_BY_THREAD_ID = 17; 113 private static final int URI_MESSAGE_ID_TO_THREAD = 18; 114 115 /** 116 * the name of the table that is used to store the queue of 117 * messages(both MMS and SMS) to be sent/downloaded. 118 */ 119 public static final String TABLE_PENDING_MSG = "pending_msgs"; 120 121 /** 122 * the name of the table that is used to store the canonical addresses for both SMS and MMS. 123 */ 124 static final String TABLE_CANONICAL_ADDRESSES = "canonical_addresses"; 125 126 /** 127 * the name of the table that is used to store the conversation threads. 128 */ 129 static final String TABLE_THREADS = "threads"; 130 131 // These constants are used to construct union queries across the 132 // MMS and SMS base tables. 133 134 // These are the columns that appear in both the MMS ("pdu") and 135 // SMS ("sms") message tables. 136 private static final String[] MMS_SMS_COLUMNS = 137 { BaseColumns._ID, Mms.DATE, Mms.DATE_SENT, Mms.READ, Mms.THREAD_ID, Mms.LOCKED, 138 Mms.SUBSCRIPTION_ID }; 139 140 // These are the columns that appear only in the MMS message 141 // table. 142 private static final String[] MMS_ONLY_COLUMNS = { 143 Mms.CONTENT_CLASS, Mms.CONTENT_LOCATION, Mms.CONTENT_TYPE, 144 Mms.DELIVERY_REPORT, Mms.EXPIRY, Mms.MESSAGE_CLASS, Mms.MESSAGE_ID, 145 Mms.MESSAGE_SIZE, Mms.MESSAGE_TYPE, Mms.MESSAGE_BOX, Mms.PRIORITY, 146 Mms.READ_STATUS, Mms.RESPONSE_STATUS, Mms.RESPONSE_TEXT, 147 Mms.RETRIEVE_STATUS, Mms.RETRIEVE_TEXT_CHARSET, Mms.REPORT_ALLOWED, 148 Mms.READ_REPORT, Mms.STATUS, Mms.SUBJECT, Mms.SUBJECT_CHARSET, 149 Mms.TRANSACTION_ID, Mms.MMS_VERSION, Mms.TEXT_ONLY }; 150 151 // These are the columns that appear only in the SMS message 152 // table. 153 private static final String[] SMS_ONLY_COLUMNS = 154 { "address", "body", "person", "reply_path_present", 155 "service_center", "status", "subject", "type", "error_code" }; 156 157 // These are all the columns that appear in the "threads" table. 158 private static final String[] THREADS_COLUMNS = { 159 BaseColumns._ID, 160 ThreadsColumns.DATE, 161 ThreadsColumns.RECIPIENT_IDS, 162 ThreadsColumns.MESSAGE_COUNT 163 }; 164 165 private static final String[] CANONICAL_ADDRESSES_COLUMNS_1 = 166 new String[] { CanonicalAddressesColumns.ADDRESS }; 167 168 private static final String[] CANONICAL_ADDRESSES_COLUMNS_2 = 169 new String[] { CanonicalAddressesColumns._ID, 170 CanonicalAddressesColumns.ADDRESS }; 171 172 // These are all the columns that appear in the MMS and SMS 173 // message tables. 174 private static final String[] UNION_COLUMNS = 175 new String[MMS_SMS_COLUMNS.length 176 + MMS_ONLY_COLUMNS.length 177 + SMS_ONLY_COLUMNS.length]; 178 179 // These are all the columns that appear in the MMS table. 180 private static final Set<String> MMS_COLUMNS = new HashSet<String>(); 181 182 // These are all the columns that appear in the SMS table. 183 private static final Set<String> SMS_COLUMNS = new HashSet<String>(); 184 185 private static final String VND_ANDROID_DIR_MMS_SMS = 186 "vnd.android-dir/mms-sms"; 187 188 private static final String[] ID_PROJECTION = { BaseColumns._ID }; 189 190 private static final String[] EMPTY_STRING_ARRAY = new String[0]; 191 192 private static final String[] SEARCH_STRING = new String[1]; 193 private static final String SEARCH_QUERY = "SELECT snippet(words, '', ' ', '', 1, 1) as " + 194 "snippet FROM words WHERE index_text MATCH ? ORDER BY snippet LIMIT 50;"; 195 196 private static final String SMS_CONVERSATION_CONSTRAINT = "(" + 197 Sms.TYPE + " != " + Sms.MESSAGE_TYPE_DRAFT + ")"; 198 199 private static final String MMS_CONVERSATION_CONSTRAINT = "(" + 200 Mms.MESSAGE_BOX + " != " + Mms.MESSAGE_BOX_DRAFTS + " AND (" + 201 Mms.MESSAGE_TYPE + " = " + PduHeaders.MESSAGE_TYPE_SEND_REQ + " OR " + 202 Mms.MESSAGE_TYPE + " = " + PduHeaders.MESSAGE_TYPE_RETRIEVE_CONF + " OR " + 203 Mms.MESSAGE_TYPE + " = " + PduHeaders.MESSAGE_TYPE_NOTIFICATION_IND + "))"; 204 getTextSearchQuery(String smsTable, String pduTable)205 private static String getTextSearchQuery(String smsTable, String pduTable) { 206 // Search on the words table but return the rows from the corresponding sms table 207 final String smsQuery = "SELECT " 208 + smsTable + "._id AS _id," 209 + "thread_id," 210 + "address," 211 + "body," 212 + "date," 213 + "date_sent," 214 + "index_text," 215 + "words._id " 216 + "FROM " + smsTable + ",words " 217 + "WHERE (index_text MATCH ? " 218 + "AND " + smsTable + "._id=words.source_id " 219 + "AND words.table_to_use=1)"; 220 221 // Search on the words table but return the rows from the corresponding parts table 222 final String mmsQuery = "SELECT " 223 + pduTable + "._id," 224 + "thread_id," 225 + "addr.address," 226 + "part.text AS body," 227 + pduTable + ".date," 228 + pduTable + ".date_sent," 229 + "index_text," 230 + "words._id " 231 + "FROM " + pduTable + ",part,addr,words " 232 + "WHERE ((part.mid=" + pduTable + "._id) " 233 + "AND (addr.msg_id=" + pduTable + "._id) " 234 + "AND (addr.type=" + PduHeaders.TO + ") " 235 + "AND (part.ct='text/plain') " 236 + "AND (index_text MATCH ?) " 237 + "AND (part._id = words.source_id) " 238 + "AND (words.table_to_use=2))"; 239 240 // This code queries the sms and mms tables and returns a unified result set 241 // of text matches. We query the sms table which is pretty simple. We also 242 // query the pdu, part and addr table to get the mms result. Note we're 243 // using a UNION so we have to have the same number of result columns from 244 // both queries. 245 return smsQuery + " UNION " + mmsQuery + " " 246 + "GROUP BY thread_id " 247 + "ORDER BY thread_id ASC, date DESC"; 248 } 249 250 private static final String AUTHORITY = "mms-sms"; 251 252 static { URI_MATCHER.addURI(AUTHORITY, "conversations", URI_CONVERSATIONS)253 URI_MATCHER.addURI(AUTHORITY, "conversations", URI_CONVERSATIONS); URI_MATCHER.addURI(AUTHORITY, "complete-conversations", URI_COMPLETE_CONVERSATIONS)254 URI_MATCHER.addURI(AUTHORITY, "complete-conversations", URI_COMPLETE_CONVERSATIONS); 255 256 // In these patterns, "#" is the thread ID. URI_MATCHER.addURI( AUTHORITY, "conversations/#", URI_CONVERSATIONS_MESSAGES)257 URI_MATCHER.addURI( 258 AUTHORITY, "conversations/#", URI_CONVERSATIONS_MESSAGES); URI_MATCHER.addURI( AUTHORITY, "conversations/#/recipients", URI_CONVERSATIONS_RECIPIENTS)259 URI_MATCHER.addURI( 260 AUTHORITY, "conversations/#/recipients", 261 URI_CONVERSATIONS_RECIPIENTS); 262 URI_MATCHER.addURI( AUTHORITY, "conversations/#/subject", URI_CONVERSATIONS_SUBJECT)263 URI_MATCHER.addURI( 264 AUTHORITY, "conversations/#/subject", 265 URI_CONVERSATIONS_SUBJECT); 266 267 // URI for deleting obsolete threads. URI_MATCHER.addURI(AUTHORITY, "conversations/obsolete", URI_OBSOLETE_THREADS)268 URI_MATCHER.addURI(AUTHORITY, "conversations/obsolete", URI_OBSOLETE_THREADS); 269 URI_MATCHER.addURI( AUTHORITY, "messages/byphone/*", URI_MESSAGES_BY_PHONE)270 URI_MATCHER.addURI( 271 AUTHORITY, "messages/byphone/*", 272 URI_MESSAGES_BY_PHONE); 273 274 // In this pattern, two query parameter names are expected: 275 // "subject" and "recipient." Multiple "recipient" parameters 276 // may be present. URI_MATCHER.addURI(AUTHORITY, "threadID", URI_THREAD_ID)277 URI_MATCHER.addURI(AUTHORITY, "threadID", URI_THREAD_ID); 278 279 // Use this pattern to query the canonical address by given ID. URI_MATCHER.addURI(AUTHORITY, "canonical-address/#", URI_CANONICAL_ADDRESS)280 URI_MATCHER.addURI(AUTHORITY, "canonical-address/#", URI_CANONICAL_ADDRESS); 281 282 // Use this pattern to query all canonical addresses. URI_MATCHER.addURI(AUTHORITY, "canonical-addresses", URI_CANONICAL_ADDRESSES)283 URI_MATCHER.addURI(AUTHORITY, "canonical-addresses", URI_CANONICAL_ADDRESSES); 284 URI_MATCHER.addURI(AUTHORITY, "search", URI_SEARCH)285 URI_MATCHER.addURI(AUTHORITY, "search", URI_SEARCH); URI_MATCHER.addURI(AUTHORITY, "searchSuggest", URI_SEARCH_SUGGEST)286 URI_MATCHER.addURI(AUTHORITY, "searchSuggest", URI_SEARCH_SUGGEST); 287 288 // In this pattern, two query parameters may be supplied: 289 // "protocol" and "message." For example: 290 // content://mms-sms/pending? 291 // -> Return all pending messages; 292 // content://mms-sms/pending?protocol=sms 293 // -> Only return pending SMs; 294 // content://mms-sms/pending?protocol=mms&message=1 295 // -> Return the the pending MM which ID equals '1'. 296 // URI_MATCHER.addURI(AUTHORITY, "pending", URI_PENDING_MSG)297 URI_MATCHER.addURI(AUTHORITY, "pending", URI_PENDING_MSG); 298 299 // Use this pattern to get a list of undelivered messages. URI_MATCHER.addURI(AUTHORITY, "undelivered", URI_UNDELIVERED_MSG)300 URI_MATCHER.addURI(AUTHORITY, "undelivered", URI_UNDELIVERED_MSG); 301 302 // Use this pattern to see what delivery status reports (for 303 // both MMS and SMS) have not been delivered to the user. URI_MATCHER.addURI(AUTHORITY, "notifications", URI_NOTIFICATIONS)304 URI_MATCHER.addURI(AUTHORITY, "notifications", URI_NOTIFICATIONS); 305 URI_MATCHER.addURI(AUTHORITY, "draft", URI_DRAFT)306 URI_MATCHER.addURI(AUTHORITY, "draft", URI_DRAFT); 307 URI_MATCHER.addURI(AUTHORITY, "locked", URI_FIRST_LOCKED_MESSAGE_ALL)308 URI_MATCHER.addURI(AUTHORITY, "locked", URI_FIRST_LOCKED_MESSAGE_ALL); 309 URI_MATCHER.addURI(AUTHORITY, "locked/#", URI_FIRST_LOCKED_MESSAGE_BY_THREAD_ID)310 URI_MATCHER.addURI(AUTHORITY, "locked/#", URI_FIRST_LOCKED_MESSAGE_BY_THREAD_ID); 311 URI_MATCHER.addURI(AUTHORITY, "messageIdToThread", URI_MESSAGE_ID_TO_THREAD)312 URI_MATCHER.addURI(AUTHORITY, "messageIdToThread", URI_MESSAGE_ID_TO_THREAD); initializeColumnSets()313 initializeColumnSets(); 314 } 315 316 private SQLiteOpenHelper mOpenHelper; 317 318 private boolean mUseStrictPhoneNumberComparation; 319 320 // Call() methods and parameters 321 private static final String METHOD_IS_RESTORING = "is_restoring"; 322 private static final String IS_RESTORING_KEY = "restoring"; 323 private static final String METHOD_GARBAGE_COLLECT = "garbage_collect"; 324 private static final String DO_DELETE = "delete"; 325 326 @Override onCreate()327 public boolean onCreate() { 328 setAppOps(AppOpsManager.OP_READ_SMS, AppOpsManager.OP_WRITE_SMS); 329 mOpenHelper = MmsSmsDatabaseHelper.getInstanceForCe(getContext()); 330 mUseStrictPhoneNumberComparation = 331 getContext().getResources().getBoolean( 332 com.android.internal.R.bool.config_use_strict_phone_number_comparation); 333 TelephonyBackupAgent.DeferredSmsMmsRestoreService.startIfFilesExist(getContext()); 334 return true; 335 } 336 337 @Override query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder)338 public Cursor query(Uri uri, String[] projection, 339 String selection, String[] selectionArgs, String sortOrder) { 340 final int callerUid = Binder.getCallingUid(); 341 final UserHandle callerUserHandle = Binder.getCallingUserHandle(); 342 String callingPackage = getCallingPackage(); 343 344 // First check if restricted views of the "sms" and "pdu" tables should be used based on the 345 // caller's identity. Only system, phone or the default sms app can have full access 346 // of sms/mms data. For other apps, we present a restricted view which only contains sent 347 // or received messages, without wap pushes. 348 final boolean accessRestricted = ProviderUtil.isAccessRestricted( 349 getContext(), getCallingPackage(), callerUid); 350 final String pduTable = MmsProvider.getPduTable(accessRestricted); 351 final String smsTable = SmsProvider.getSmsTable(accessRestricted); 352 353 // If access is restricted, we don't allow subqueries in the query. 354 if (accessRestricted) { 355 try { 356 SqlQueryChecker.checkQueryParametersForSubqueries(projection, selection, sortOrder); 357 } catch (IllegalArgumentException e) { 358 Log.w(LOG_TAG, "Query rejected: " + e.getMessage()); 359 return null; 360 } 361 } 362 363 String selectionBySubIds; 364 final long token = Binder.clearCallingIdentity(); 365 try { 366 // Filter MMS/SMS based on subId 367 selectionBySubIds = ProviderUtil.getSelectionBySubIds(getContext(), callerUserHandle); 368 } finally { 369 Binder.restoreCallingIdentity(token); 370 } 371 372 SQLiteDatabase db = mOpenHelper.getReadableDatabase(); 373 if (mOpenHelper instanceof MmsSmsDatabaseHelper) { 374 ((MmsSmsDatabaseHelper) mOpenHelper).addDatabaseOpeningDebugLog( 375 callingPackage + ";MmsSmsProvider.query;" + uri, true); 376 } 377 Cursor cursor = null; 378 Cursor emptyCursor = new MatrixCursor((projection == null) ? 379 (new String[] {}) : projection); 380 final int match = URI_MATCHER.match(uri); 381 switch (match) { 382 case URI_COMPLETE_CONVERSATIONS: 383 if (selectionBySubIds == null) { 384 // No subscriptions associated with user, return empty cursor. 385 return emptyCursor; 386 } 387 selection = DatabaseUtils.concatenateWhere(selection, selectionBySubIds); 388 389 cursor = getCompleteConversations(projection, selection, sortOrder, smsTable, 390 pduTable); 391 break; 392 case URI_CONVERSATIONS: 393 String simple = uri.getQueryParameter("simple"); 394 if ((simple != null) && simple.equals("true")) { 395 String threadType = uri.getQueryParameter("thread_type"); 396 if (!TextUtils.isEmpty(threadType)) { 397 try { 398 Integer.parseInt(threadType); 399 selection = concatSelections( 400 selection, Threads.TYPE + "=" + threadType); 401 } catch (NumberFormatException ex) { 402 Log.e(LOG_TAG, "Thread type must be int"); 403 // return empty cursor 404 break; 405 } 406 } 407 cursor = getSimpleConversations( 408 projection, selection, selectionArgs, sortOrder); 409 } else { 410 if (selectionBySubIds == null) { 411 // No subscriptions associated with user, return empty cursor. 412 return emptyCursor; 413 } 414 selection = DatabaseUtils.concatenateWhere(selection, selectionBySubIds); 415 416 cursor = getConversations( 417 projection, selection, sortOrder, smsTable, pduTable); 418 } 419 break; 420 case URI_CONVERSATIONS_MESSAGES: 421 if (selectionBySubIds == null) { 422 // No subscriptions associated with user, return empty cursor. 423 return emptyCursor; 424 } 425 selection = DatabaseUtils.concatenateWhere(selection, selectionBySubIds); 426 427 cursor = getConversationMessages(uri.getPathSegments().get(1), projection, 428 selection, sortOrder, smsTable, pduTable); 429 break; 430 case URI_CONVERSATIONS_RECIPIENTS: 431 cursor = getConversationById( 432 uri.getPathSegments().get(1), projection, selection, 433 selectionArgs, sortOrder); 434 break; 435 case URI_CONVERSATIONS_SUBJECT: 436 cursor = getConversationById( 437 uri.getPathSegments().get(1), projection, selection, 438 selectionArgs, sortOrder); 439 break; 440 case URI_MESSAGES_BY_PHONE: 441 if (selectionBySubIds == null) { 442 // No subscriptions associated with user, return emptyCursor. 443 return emptyCursor; 444 } 445 selection = DatabaseUtils.concatenateWhere(selection, selectionBySubIds); 446 447 cursor = getMessagesByPhoneNumber( 448 uri.getPathSegments().get(2), projection, selection, sortOrder, smsTable, 449 pduTable); 450 break; 451 case URI_THREAD_ID: 452 List<String> recipients = uri.getQueryParameters("recipient"); 453 454 cursor = getThreadId(recipients); 455 break; 456 case URI_CANONICAL_ADDRESS: { 457 if (selectionBySubIds == null) { 458 // No subscriptions associated with user, return empty cursor. 459 return emptyCursor; 460 } 461 selection = DatabaseUtils.concatenateWhere(selection, selectionBySubIds); 462 463 String extraSelection = "_id=" + uri.getPathSegments().get(1); 464 String finalSelection = TextUtils.isEmpty(selection) 465 ? extraSelection : extraSelection + " AND " + selection; 466 cursor = db.query(TABLE_CANONICAL_ADDRESSES, 467 CANONICAL_ADDRESSES_COLUMNS_1, 468 finalSelection, 469 selectionArgs, 470 null, null, 471 sortOrder); 472 break; 473 } 474 case URI_CANONICAL_ADDRESSES: 475 if (selectionBySubIds == null) { 476 // No subscriptions associated with user, return empty cursor. 477 return emptyCursor; 478 } 479 selection = DatabaseUtils.concatenateWhere(selection, selectionBySubIds); 480 481 cursor = db.query(TABLE_CANONICAL_ADDRESSES, 482 CANONICAL_ADDRESSES_COLUMNS_2, 483 selection, 484 selectionArgs, 485 null, null, 486 sortOrder); 487 break; 488 case URI_SEARCH_SUGGEST: { 489 SEARCH_STRING[0] = uri.getQueryParameter("pattern") + '*' ; 490 491 // find the words which match the pattern using the snippet function. The 492 // snippet function parameters mainly describe how to format the result. 493 // See http://www.sqlite.org/fts3.html#section_4_2 for details. 494 if ( sortOrder != null 495 || selection != null 496 || selectionArgs != null 497 || projection != null) { 498 throw new IllegalArgumentException( 499 "do not specify sortOrder, selection, selectionArgs, or projection" + 500 "with this query"); 501 } 502 503 cursor = db.rawQuery(SEARCH_QUERY, SEARCH_STRING); 504 break; 505 } 506 case URI_MESSAGE_ID_TO_THREAD: { 507 // Given a message ID and an indicator for SMS vs. MMS return 508 // the thread id of the corresponding thread. 509 try { 510 long id = Long.parseLong(uri.getQueryParameter("row_id")); 511 switch (Integer.parseInt(uri.getQueryParameter("table_to_use"))) { 512 case 1: // sms 513 cursor = db.query( 514 smsTable, 515 new String[] { "thread_id" }, 516 "_id=?", 517 new String[] { String.valueOf(id) }, 518 null, 519 null, 520 null); 521 break; 522 case 2: // mms 523 String mmsQuery = "SELECT thread_id " 524 + "FROM " + pduTable + ",part " 525 + "WHERE ((part.mid=" + pduTable + "._id) " 526 + "AND " + "(part._id=?))"; 527 cursor = db.rawQuery(mmsQuery, new String[] { String.valueOf(id) }); 528 break; 529 } 530 } catch (NumberFormatException ex) { 531 // ignore... return empty cursor 532 } 533 break; 534 } 535 case URI_SEARCH: { 536 if ( sortOrder != null 537 || selection != null 538 || selectionArgs != null 539 || projection != null) { 540 throw new IllegalArgumentException( 541 "do not specify sortOrder, selection, selectionArgs, or projection" + 542 "with this query"); 543 } 544 545 String searchString = uri.getQueryParameter("pattern") + "*"; 546 547 try { 548 cursor = db.rawQuery(getTextSearchQuery(smsTable, pduTable), 549 new String[] { searchString, searchString }); 550 } catch (Exception ex) { 551 Log.e(LOG_TAG, "got exception: " + ex.toString()); 552 } 553 break; 554 } 555 case URI_PENDING_MSG: { 556 String protoName = uri.getQueryParameter("protocol"); 557 String msgId = uri.getQueryParameter("message"); 558 int proto = TextUtils.isEmpty(protoName) ? -1 559 : (protoName.equals("sms") ? MmsSms.SMS_PROTO : MmsSms.MMS_PROTO); 560 561 String extraSelection = (proto != -1) ? 562 (PendingMessages.PROTO_TYPE + "=" + proto) : " 0=0 "; 563 if (!TextUtils.isEmpty(msgId)) { 564 try { 565 Long.parseLong(msgId); 566 extraSelection += " AND " + PendingMessages.MSG_ID + "=" + msgId; 567 } catch(NumberFormatException ex) { 568 Log.e(LOG_TAG, "MSG ID must be a Long."); 569 // return empty cursor 570 break; 571 } 572 } 573 if (selectionBySubIds == null) { 574 // No subscriptions associated with user, return empty cursor. 575 return emptyCursor; 576 } 577 // In PendingMessages table, SUBSCRIPTION_ID column name is pending_sub_id. 578 selectionBySubIds = "pending_" + selectionBySubIds; 579 selection = DatabaseUtils.concatenateWhere(selection, selectionBySubIds); 580 581 String finalSelection = TextUtils.isEmpty(selection) 582 ? extraSelection : ("(" + extraSelection + ") AND " + selection); 583 String finalOrder = TextUtils.isEmpty(sortOrder) 584 ? PendingMessages.DUE_TIME : sortOrder; 585 cursor = db.query(TABLE_PENDING_MSG, null, 586 finalSelection, selectionArgs, null, null, finalOrder); 587 break; 588 } 589 case URI_UNDELIVERED_MSG: { 590 if (selectionBySubIds == null) { 591 // No subscriptions associated with user, return empty cursor. 592 return emptyCursor; 593 } 594 selection = DatabaseUtils.concatenateWhere(selection, selectionBySubIds); 595 596 cursor = getUndeliveredMessages(projection, selection, 597 selectionArgs, sortOrder, smsTable, pduTable); 598 break; 599 } 600 case URI_DRAFT: { 601 if (selectionBySubIds == null) { 602 // No subscriptions associated with user, return empty cursor. 603 return emptyCursor; 604 } 605 selection = DatabaseUtils.concatenateWhere(selection, selectionBySubIds); 606 607 cursor = getDraftThread(projection, selection, sortOrder, smsTable, pduTable); 608 break; 609 } 610 case URI_FIRST_LOCKED_MESSAGE_BY_THREAD_ID: { 611 long threadId; 612 try { 613 threadId = Long.parseLong(uri.getLastPathSegment()); 614 } catch (NumberFormatException e) { 615 Log.e(LOG_TAG, "Thread ID must be a long."); 616 break; 617 } 618 selection = DatabaseUtils.concatenateWhere(selection, ("thread_id=" + threadId)); 619 620 if (selectionBySubIds == null) { 621 // No subscriptions associated with user, return empty cursor. 622 return emptyCursor; 623 } 624 selection = DatabaseUtils.concatenateWhere(selection, selectionBySubIds); 625 626 cursor = getFirstLockedMessage(projection, selection, sortOrder, 627 smsTable, pduTable); 628 break; 629 } 630 case URI_FIRST_LOCKED_MESSAGE_ALL: { 631 if (selectionBySubIds == null) { 632 // No subscriptions associated with user, return empty cursor. 633 return emptyCursor; 634 } 635 selection = DatabaseUtils.concatenateWhere(selection, selectionBySubIds); 636 637 cursor = getFirstLockedMessage( 638 projection, selection, sortOrder, smsTable, pduTable); 639 break; 640 } 641 default: 642 throw new IllegalStateException("Unrecognized URI:" + uri); 643 } 644 645 if (cursor != null) { 646 cursor.setNotificationUri(getContext().getContentResolver(), MmsSms.CONTENT_URI); 647 } 648 return cursor; 649 } 650 651 /** 652 * Return the canonical address ID for this address. 653 */ getSingleAddressId(String address)654 private long getSingleAddressId(String address) { 655 boolean isEmail = Mms.isEmailAddress(address); 656 boolean isPhoneNumber = Mms.isPhoneNumber(address); 657 658 // We lowercase all email addresses, but not addresses that aren't numbers, because 659 // that would incorrectly turn an address such as "My Vodafone" into "my vodafone" 660 // and the thread title would be incorrect when displayed in the UI. 661 String refinedAddress = isEmail ? address.toLowerCase(Locale.ROOT) : address; 662 663 String selection = "address=?"; 664 String[] selectionArgs; 665 long retVal = -1L; 666 int minMatch = 667 getContext().getResources().getInteger( 668 com.android.internal.R.integer.config_phonenumber_compare_min_match); 669 670 if (!isPhoneNumber) { 671 selectionArgs = new String[] { refinedAddress }; 672 } else { 673 selection += " OR PHONE_NUMBERS_EQUAL(address, ?, " + 674 (mUseStrictPhoneNumberComparation ? "1)" : "0, " + minMatch + ")"); 675 selectionArgs = new String[] { refinedAddress, refinedAddress }; 676 } 677 678 Cursor cursor = null; 679 680 try { 681 SQLiteDatabase db = mOpenHelper.getReadableDatabase(); 682 cursor = db.query( 683 "canonical_addresses", ID_PROJECTION, 684 selection, selectionArgs, null, null, null); 685 686 if (cursor.getCount() == 0) { 687 // TODO (b/256992531): Currently, one sim card is set as default sms subId in work 688 // profile. Default sms subId should be updated based on user pref. 689 int subId = SmsManager.getDefaultSmsSubscriptionId(); 690 ContentValues contentValues = new ContentValues(1); 691 contentValues.put(CanonicalAddressesColumns.ADDRESS, refinedAddress); 692 contentValues.put(CanonicalAddressesColumns.SUBSCRIPTION_ID, subId); 693 694 db = mOpenHelper.getWritableDatabase(); 695 retVal = db.insert("canonical_addresses", 696 CanonicalAddressesColumns.ADDRESS, contentValues); 697 698 Log.d(LOG_TAG, "getSingleAddressId: insert new canonical_address for " + 699 /*address*/ "xxxxxx" + ", sub_id=" + subId + ", _id=" + retVal); 700 701 return retVal; 702 } 703 704 if (cursor.moveToFirst()) { 705 retVal = cursor.getLong(cursor.getColumnIndexOrThrow(BaseColumns._ID)); 706 } 707 } finally { 708 if (cursor != null) { 709 cursor.close(); 710 } 711 } 712 713 return retVal; 714 } 715 716 /** 717 * Return the canonical address IDs for these addresses. 718 */ getAddressIds(List<String> addresses)719 private Set<Long> getAddressIds(List<String> addresses) { 720 Set<Long> result = new HashSet<Long>(addresses.size()); 721 722 for (String address : addresses) { 723 if (!address.equals(PduHeaders.FROM_INSERT_ADDRESS_TOKEN_STR)) { 724 long id = getSingleAddressId(address); 725 if (id != -1L) { 726 result.add(id); 727 } else { 728 Log.e(LOG_TAG, "getAddressIds: address ID not found for " + address); 729 } 730 } 731 } 732 return result; 733 } 734 735 /** 736 * Return a sorted array of the given Set of Longs. 737 */ getSortedSet(Set<Long> numbers)738 private long[] getSortedSet(Set<Long> numbers) { 739 int size = numbers.size(); 740 long[] result = new long[size]; 741 int i = 0; 742 743 for (Long number : numbers) { 744 result[i++] = number; 745 } 746 747 if (size > 1) { 748 Arrays.sort(result); 749 } 750 751 return result; 752 } 753 754 /** 755 * Return a String of the numbers in the given array, in order, 756 * separated by spaces. 757 */ getSpaceSeparatedNumbers(long[] numbers)758 private String getSpaceSeparatedNumbers(long[] numbers) { 759 int size = numbers.length; 760 StringBuilder buffer = new StringBuilder(); 761 762 for (int i = 0; i < size; i++) { 763 if (i != 0) { 764 buffer.append(' '); 765 } 766 buffer.append(numbers[i]); 767 } 768 return buffer.toString(); 769 } 770 771 /** 772 * Insert a record for a new thread. 773 */ insertThread(String recipientIds, int numberOfRecipients)774 private void insertThread(String recipientIds, int numberOfRecipients) { 775 ContentValues values = new ContentValues(4); 776 777 long date = System.currentTimeMillis(); 778 values.put(ThreadsColumns.DATE, date - date % 1000); 779 values.put(ThreadsColumns.RECIPIENT_IDS, recipientIds); 780 // TODO (b/256992531): Currently, one sim card is set as default sms subId in work 781 // profile. Default sms subId should be updated based on user pref. 782 values.put(ThreadsColumns.SUBSCRIPTION_ID, SmsManager.getDefaultSmsSubscriptionId()); 783 if (numberOfRecipients > 1) { 784 values.put(Threads.TYPE, Threads.BROADCAST_THREAD); 785 } 786 values.put(ThreadsColumns.MESSAGE_COUNT, 0); 787 788 long result = mOpenHelper.getWritableDatabase().insert(TABLE_THREADS, null, values); 789 Log.d(LOG_TAG, "insertThread: created new thread_id " + result + 790 " for recipientIds " + /*recipientIds*/ "xxxxxxx"); 791 792 getContext().getContentResolver().notifyChange(MmsSms.CONTENT_URI, null, true, 793 UserHandle.USER_ALL); 794 } 795 796 private static final String THREAD_QUERY = 797 "SELECT _id FROM threads " + "WHERE recipient_ids=?"; 798 799 /** 800 * Return the thread ID for this list of 801 * recipients IDs. If no thread exists with this ID, create 802 * one and return it. Callers should always use 803 * Threads.getThreadId to access this information. 804 */ getThreadId(List<String> recipients)805 private synchronized Cursor getThreadId(List<String> recipients) { 806 Set<Long> addressIds = getAddressIds(recipients); 807 String recipientIds = ""; 808 809 if (addressIds.size() == 0) { 810 Log.e(LOG_TAG, "getThreadId: NO receipients specified -- NOT creating thread", 811 new Exception()); 812 TelephonyStatsLog.write( 813 TelephonyStatsLog.MMS_SMS_PROVIDER_GET_THREAD_ID_FAILED, 814 TelephonyStatsLog 815 .MMS_SMS_PROVIDER_GET_THREAD_ID_FAILED__FAILURE_CODE__FAILURE_NO_RECIPIENTS); 816 return null; 817 } else if (addressIds.size() == 1) { 818 // optimize for size==1, which should be most of the cases 819 for (Long addressId : addressIds) { 820 recipientIds = Long.toString(addressId); 821 } 822 } else { 823 recipientIds = getSpaceSeparatedNumbers(getSortedSet(addressIds)); 824 } 825 826 if (Log.isLoggable(LOG_TAG, Log.VERBOSE)) { 827 Log.d(LOG_TAG, "getThreadId: recipientIds (selectionArgs) =" + 828 /*recipientIds*/ "xxxxxxx"); 829 } 830 831 String[] selectionArgs = new String[] { recipientIds }; 832 833 SQLiteDatabase db = mOpenHelper.getReadableDatabase(); 834 db.beginTransaction(); 835 Cursor cursor = null; 836 try { 837 // Find the thread with the given recipients 838 cursor = db.rawQuery(THREAD_QUERY, selectionArgs); 839 840 if (cursor.getCount() == 0) { 841 // No thread with those recipients exists, so create the thread. 842 cursor.close(); 843 844 Log.d(LOG_TAG, "getThreadId: create new thread_id for recipients " + 845 /*recipients*/ "xxxxxxxx"); 846 insertThread(recipientIds, recipients.size()); 847 848 // The thread was just created, now find it and return it. 849 cursor = db.rawQuery(THREAD_QUERY, selectionArgs); 850 } 851 db.setTransactionSuccessful(); 852 } catch (Throwable ex) { 853 Log.e(LOG_TAG, ex.getMessage(), ex); 854 if (mOpenHelper instanceof MmsSmsDatabaseHelper) { 855 ((MmsSmsDatabaseHelper) mOpenHelper).printDatabaseOpeningDebugLog(); 856 } 857 TelephonyStatsLog.write( 858 TelephonyStatsLog.MMS_SMS_PROVIDER_GET_THREAD_ID_FAILED, 859 FAILURE_FIND_OR_CREATE_THREAD_ID_SQL); 860 } finally { 861 db.endTransaction(); 862 } 863 864 if (cursor != null && cursor.getCount() > 1) { 865 Log.w(LOG_TAG, "getThreadId: why is cursorCount=" + cursor.getCount()); 866 TelephonyStatsLog.write( 867 TelephonyStatsLog.MMS_SMS_PROVIDER_GET_THREAD_ID_FAILED, 868 MULTIPLE_THREAD_IDS_FOUND); 869 } 870 return cursor; 871 } 872 concatSelections(String selection1, String selection2)873 private static String concatSelections(String selection1, String selection2) { 874 if (TextUtils.isEmpty(selection1)) { 875 return selection2; 876 } else if (TextUtils.isEmpty(selection2)) { 877 return selection1; 878 } else { 879 return selection1 + " AND " + selection2; 880 } 881 } 882 883 /** 884 * If a null projection is given, return the union of all columns 885 * in both the MMS and SMS messages tables. Otherwise, return the 886 * given projection. 887 */ handleNullMessageProjection( String[] projection)888 private static String[] handleNullMessageProjection( 889 String[] projection) { 890 return projection == null ? UNION_COLUMNS : projection; 891 } 892 893 /** 894 * If a null projection is given, return the set of all columns in 895 * the threads table. Otherwise, return the given projection. 896 */ handleNullThreadsProjection( String[] projection)897 private static String[] handleNullThreadsProjection( 898 String[] projection) { 899 return projection == null ? THREADS_COLUMNS : projection; 900 } 901 902 /** 903 * If a null sort order is given, return "normalized_date ASC". 904 * Otherwise, return the given sort order. 905 */ handleNullSortOrder(String sortOrder)906 private static String handleNullSortOrder (String sortOrder) { 907 return sortOrder == null ? "normalized_date ASC" : sortOrder; 908 } 909 910 /** 911 * Return existing threads in the database. 912 */ getSimpleConversations(String[] projection, String selection, String[] selectionArgs, String sortOrder)913 private Cursor getSimpleConversations(String[] projection, String selection, 914 String[] selectionArgs, String sortOrder) { 915 return mOpenHelper.getReadableDatabase().query(TABLE_THREADS, projection, 916 selection, selectionArgs, null, null, " date DESC"); 917 } 918 919 /** 920 * Return the thread which has draft in both MMS and SMS. 921 * 922 * Use this query: 923 * 924 * SELECT ... 925 * FROM (SELECT _id, thread_id, ... 926 * FROM pdu 927 * WHERE msg_box = 3 AND ... 928 * UNION 929 * SELECT _id, thread_id, ... 930 * FROM sms 931 * WHERE type = 3 AND ... 932 * ) 933 * ; 934 */ getDraftThread(String[] projection, String selection, String sortOrder, String smsTable, String pduTable)935 private Cursor getDraftThread(String[] projection, String selection, 936 String sortOrder, String smsTable, String pduTable) { 937 String[] innerProjection = new String[] {BaseColumns._ID, Conversations.THREAD_ID}; 938 SQLiteQueryBuilder mmsQueryBuilder = new SQLiteQueryBuilder(); 939 SQLiteQueryBuilder smsQueryBuilder = new SQLiteQueryBuilder(); 940 941 mmsQueryBuilder.setTables(pduTable); 942 smsQueryBuilder.setTables(smsTable); 943 944 String mmsSubQuery = mmsQueryBuilder.buildUnionSubQuery( 945 MmsSms.TYPE_DISCRIMINATOR_COLUMN, innerProjection, 946 MMS_COLUMNS, 1, "mms", 947 concatSelections(selection, Mms.MESSAGE_BOX + "=" + Mms.MESSAGE_BOX_DRAFTS), 948 null, null); 949 String smsSubQuery = smsQueryBuilder.buildUnionSubQuery( 950 MmsSms.TYPE_DISCRIMINATOR_COLUMN, innerProjection, 951 SMS_COLUMNS, 1, "sms", 952 concatSelections(selection, Sms.TYPE + "=" + Sms.MESSAGE_TYPE_DRAFT), 953 null, null); 954 SQLiteQueryBuilder unionQueryBuilder = new SQLiteQueryBuilder(); 955 956 unionQueryBuilder.setDistinct(true); 957 958 String unionQuery = unionQueryBuilder.buildUnionQuery( 959 new String[] { mmsSubQuery, smsSubQuery }, null, null); 960 961 SQLiteQueryBuilder outerQueryBuilder = new SQLiteQueryBuilder(); 962 963 outerQueryBuilder.setTables("(" + unionQuery + ")"); 964 965 String outerQuery = outerQueryBuilder.buildQuery( 966 projection, null, null, null, sortOrder, null); 967 968 return mOpenHelper.getReadableDatabase().rawQuery(outerQuery, EMPTY_STRING_ARRAY); 969 } 970 971 /** 972 * Return the most recent message in each conversation in both MMS 973 * and SMS. 974 * 975 * Use this query: 976 * 977 * SELECT ... 978 * FROM (SELECT thread_id AS tid, date * 1000 AS normalized_date, ... 979 * FROM pdu 980 * WHERE msg_box != 3 AND ... 981 * GROUP BY thread_id 982 * HAVING date = MAX(date) 983 * UNION 984 * SELECT thread_id AS tid, date AS normalized_date, ... 985 * FROM sms 986 * WHERE ... 987 * GROUP BY thread_id 988 * HAVING date = MAX(date)) 989 * GROUP BY tid 990 * HAVING normalized_date = MAX(normalized_date); 991 * 992 * The msg_box != 3 comparisons ensure that we don't include draft 993 * messages. 994 */ getConversations(String[] projection, String selection, String sortOrder, String smsTable, String pduTable)995 private Cursor getConversations(String[] projection, String selection, 996 String sortOrder, String smsTable, String pduTable) { 997 SQLiteQueryBuilder mmsQueryBuilder = new SQLiteQueryBuilder(); 998 SQLiteQueryBuilder smsQueryBuilder = new SQLiteQueryBuilder(); 999 1000 mmsQueryBuilder.setTables(pduTable); 1001 smsQueryBuilder.setTables(smsTable); 1002 1003 String[] columns = handleNullMessageProjection(projection); 1004 String[] innerMmsProjection = makeProjectionWithDateAndThreadId( 1005 UNION_COLUMNS, 1000); 1006 String[] innerSmsProjection = makeProjectionWithDateAndThreadId( 1007 UNION_COLUMNS, 1); 1008 String mmsSubQuery = mmsQueryBuilder.buildUnionSubQuery( 1009 MmsSms.TYPE_DISCRIMINATOR_COLUMN, innerMmsProjection, 1010 MMS_COLUMNS, 1, "mms", 1011 concatSelections(selection, MMS_CONVERSATION_CONSTRAINT), 1012 "thread_id", "date = MAX(date)"); 1013 String smsSubQuery = smsQueryBuilder.buildUnionSubQuery( 1014 MmsSms.TYPE_DISCRIMINATOR_COLUMN, innerSmsProjection, 1015 SMS_COLUMNS, 1, "sms", 1016 concatSelections(selection, SMS_CONVERSATION_CONSTRAINT), 1017 "thread_id", "date = MAX(date)"); 1018 SQLiteQueryBuilder unionQueryBuilder = new SQLiteQueryBuilder(); 1019 1020 unionQueryBuilder.setDistinct(true); 1021 1022 String unionQuery = unionQueryBuilder.buildUnionQuery( 1023 new String[] { mmsSubQuery, smsSubQuery }, null, null); 1024 1025 SQLiteQueryBuilder outerQueryBuilder = new SQLiteQueryBuilder(); 1026 1027 outerQueryBuilder.setTables("(" + unionQuery + ")"); 1028 1029 String outerQuery = outerQueryBuilder.buildQuery( 1030 columns, null, "tid", 1031 "normalized_date = MAX(normalized_date)", sortOrder, null); 1032 1033 return mOpenHelper.getReadableDatabase().rawQuery(outerQuery, EMPTY_STRING_ARRAY); 1034 } 1035 1036 /** 1037 * Return the first locked message found in the union of MMS 1038 * and SMS messages. 1039 * 1040 * Use this query: 1041 * 1042 * SELECT _id FROM pdu GROUP BY _id HAVING locked=1 UNION SELECT _id FROM sms GROUP 1043 * BY _id HAVING locked=1 LIMIT 1 1044 * 1045 * We limit by 1 because we're only interested in knowing if 1046 * there is *any* locked message, not the actual messages themselves. 1047 */ getFirstLockedMessage(String[] projection, String selection, String sortOrder, String smsTable, String pduTable)1048 private Cursor getFirstLockedMessage(String[] projection, String selection, 1049 String sortOrder, String smsTable, String pduTable) { 1050 SQLiteQueryBuilder mmsQueryBuilder = new SQLiteQueryBuilder(); 1051 SQLiteQueryBuilder smsQueryBuilder = new SQLiteQueryBuilder(); 1052 1053 mmsQueryBuilder.setTables(pduTable); 1054 smsQueryBuilder.setTables(smsTable); 1055 1056 String[] idColumn = new String[] { BaseColumns._ID }; 1057 1058 // NOTE: buildUnionSubQuery *ignores* selectionArgs 1059 String mmsSubQuery = mmsQueryBuilder.buildUnionSubQuery( 1060 MmsSms.TYPE_DISCRIMINATOR_COLUMN, idColumn, 1061 null, 1, "mms", 1062 selection, 1063 BaseColumns._ID, "locked=1"); 1064 1065 String smsSubQuery = smsQueryBuilder.buildUnionSubQuery( 1066 MmsSms.TYPE_DISCRIMINATOR_COLUMN, idColumn, 1067 null, 1, "sms", 1068 selection, 1069 BaseColumns._ID, "locked=1"); 1070 1071 SQLiteQueryBuilder unionQueryBuilder = new SQLiteQueryBuilder(); 1072 1073 unionQueryBuilder.setDistinct(true); 1074 1075 String unionQuery = unionQueryBuilder.buildUnionQuery( 1076 new String[] { mmsSubQuery, smsSubQuery }, null, "1"); 1077 1078 Cursor cursor = mOpenHelper.getReadableDatabase().rawQuery(unionQuery, EMPTY_STRING_ARRAY); 1079 1080 if (DEBUG) { 1081 Log.v("MmsSmsProvider", "getFirstLockedMessage query: " + unionQuery); 1082 Log.v("MmsSmsProvider", "cursor count: " + cursor.getCount()); 1083 } 1084 return cursor; 1085 } 1086 1087 /** 1088 * Return every message in each conversation in both MMS 1089 * and SMS. 1090 */ getCompleteConversations(String[] projection, String selection, String sortOrder, String smsTable, String pduTable)1091 private Cursor getCompleteConversations(String[] projection, 1092 String selection, String sortOrder, String smsTable, String pduTable) { 1093 String unionQuery = buildConversationQuery(projection, selection, sortOrder, smsTable, 1094 pduTable); 1095 1096 return mOpenHelper.getReadableDatabase().rawQuery(unionQuery, EMPTY_STRING_ARRAY); 1097 } 1098 1099 /** 1100 * Add normalized date and thread_id to the list of columns for an 1101 * inner projection. This is necessary so that the outer query 1102 * can have access to these columns even if the caller hasn't 1103 * requested them in the result. 1104 */ makeProjectionWithDateAndThreadId( String[] projection, int dateMultiple)1105 private String[] makeProjectionWithDateAndThreadId( 1106 String[] projection, int dateMultiple) { 1107 int projectionSize = projection.length; 1108 String[] result = new String[projectionSize + 2]; 1109 1110 result[0] = "thread_id AS tid"; 1111 result[1] = "date * " + dateMultiple + " AS normalized_date"; 1112 for (int i = 0; i < projectionSize; i++) { 1113 result[i + 2] = projection[i]; 1114 } 1115 return result; 1116 } 1117 1118 /** 1119 * Return the union of MMS and SMS messages for this thread ID. 1120 */ getConversationMessages( String threadIdString, String[] projection, String selection, String sortOrder, String smsTable, String pduTable)1121 private Cursor getConversationMessages( 1122 String threadIdString, String[] projection, String selection, 1123 String sortOrder, String smsTable, String pduTable) { 1124 try { 1125 Long.parseLong(threadIdString); 1126 } catch (NumberFormatException exception) { 1127 Log.e(LOG_TAG, "Thread ID must be a Long."); 1128 return null; 1129 } 1130 1131 String finalSelection = concatSelections( 1132 selection, "thread_id = " + threadIdString); 1133 String unionQuery = buildConversationQuery(projection, finalSelection, sortOrder, smsTable, 1134 pduTable); 1135 1136 return mOpenHelper.getReadableDatabase().rawQuery(unionQuery, EMPTY_STRING_ARRAY); 1137 } 1138 1139 /** 1140 * Return the union of MMS and SMS messages whose recipients 1141 * included this phone number. 1142 * 1143 * Use this query: 1144 * 1145 * SELECT ... 1146 * FROM pdu, (SELECT msg_id AS address_msg_id 1147 * FROM addr 1148 * WHERE (address='<phoneNumber>' OR 1149 * PHONE_NUMBERS_EQUAL(addr.address, '<phoneNumber>', 1/0, none/minMatch))) 1150 * AS matching_addresses 1151 * WHERE pdu._id = matching_addresses.address_msg_id 1152 * UNION 1153 * SELECT ... 1154 * FROM sms 1155 * WHERE (address='<phoneNumber>' OR 1156 * PHONE_NUMBERS_EQUAL(sms.address, '<phoneNumber>', 1/0, none/minMatch)); 1157 */ getMessagesByPhoneNumber( String phoneNumber, String[] projection, String selection, String sortOrder, String smsTable, String pduTable)1158 private Cursor getMessagesByPhoneNumber( 1159 String phoneNumber, String[] projection, String selection, 1160 String sortOrder, String smsTable, String pduTable) { 1161 int minMatch = 1162 getContext().getResources().getInteger( 1163 com.android.internal.R.integer.config_phonenumber_compare_min_match); 1164 String finalMmsSelection = 1165 concatSelections( 1166 selection, 1167 pduTable + "._id = matching_addresses.address_msg_id"); 1168 String finalSmsSelection = 1169 concatSelections( 1170 selection, 1171 "(address=? OR PHONE_NUMBERS_EQUAL(address, ?" + 1172 (mUseStrictPhoneNumberComparation ? ", 1))" : ", 0, " + minMatch + "))")); 1173 SQLiteQueryBuilder mmsQueryBuilder = new SQLiteQueryBuilder(); 1174 SQLiteQueryBuilder smsQueryBuilder = new SQLiteQueryBuilder(); 1175 1176 mmsQueryBuilder.setDistinct(true); 1177 smsQueryBuilder.setDistinct(true); 1178 mmsQueryBuilder.setTables( 1179 pduTable + 1180 ", (SELECT msg_id AS address_msg_id " + 1181 "FROM addr WHERE (address=?" + 1182 " OR PHONE_NUMBERS_EQUAL(addr.address, ?" + 1183 (mUseStrictPhoneNumberComparation ? ", 1))) " : ", 0, " + minMatch + "))) ") + 1184 "AS matching_addresses"); 1185 smsQueryBuilder.setTables(smsTable); 1186 1187 String[] columns = handleNullMessageProjection(projection); 1188 String mmsSubQuery = mmsQueryBuilder.buildUnionSubQuery( 1189 MmsSms.TYPE_DISCRIMINATOR_COLUMN, columns, MMS_COLUMNS, 1190 0, "mms", finalMmsSelection, null, null); 1191 String smsSubQuery = smsQueryBuilder.buildUnionSubQuery( 1192 MmsSms.TYPE_DISCRIMINATOR_COLUMN, columns, SMS_COLUMNS, 1193 0, "sms", finalSmsSelection, null, null); 1194 SQLiteQueryBuilder unionQueryBuilder = new SQLiteQueryBuilder(); 1195 1196 unionQueryBuilder.setDistinct(true); 1197 1198 String unionQuery = unionQueryBuilder.buildUnionQuery( 1199 new String[] { mmsSubQuery, smsSubQuery }, sortOrder, null); 1200 1201 return mOpenHelper.getReadableDatabase().rawQuery(unionQuery, 1202 new String[] { phoneNumber, phoneNumber, phoneNumber, phoneNumber }); 1203 } 1204 1205 /** 1206 * Return the conversation of certain thread ID. 1207 */ getConversationById( String threadIdString, String[] projection, String selection, String[] selectionArgs, String sortOrder)1208 private Cursor getConversationById( 1209 String threadIdString, String[] projection, String selection, 1210 String[] selectionArgs, String sortOrder) { 1211 try { 1212 Long.parseLong(threadIdString); 1213 } catch (NumberFormatException exception) { 1214 Log.e(LOG_TAG, "Thread ID must be a Long."); 1215 return null; 1216 } 1217 1218 String extraSelection = "_id=" + threadIdString; 1219 String finalSelection = concatSelections(selection, extraSelection); 1220 SQLiteQueryBuilder queryBuilder = new SQLiteQueryBuilder(); 1221 String[] columns = handleNullThreadsProjection(projection); 1222 1223 queryBuilder.setDistinct(true); 1224 queryBuilder.setTables(TABLE_THREADS); 1225 return queryBuilder.query( 1226 mOpenHelper.getReadableDatabase(), columns, finalSelection, 1227 selectionArgs, sortOrder, null, null); 1228 } 1229 joinPduAndPendingMsgTables(String pduTable)1230 private static String joinPduAndPendingMsgTables(String pduTable) { 1231 return pduTable + " LEFT JOIN " + TABLE_PENDING_MSG 1232 + " ON " + pduTable + "._id = pending_msgs.msg_id"; 1233 } 1234 createMmsProjection(String[] old, String pduTable)1235 private static String[] createMmsProjection(String[] old, String pduTable) { 1236 String[] newProjection = new String[old.length]; 1237 for (int i = 0; i < old.length; i++) { 1238 if (old[i].equals(BaseColumns._ID)) { 1239 newProjection[i] = pduTable + "._id"; 1240 } else { 1241 newProjection[i] = old[i]; 1242 } 1243 } 1244 return newProjection; 1245 } 1246 getUndeliveredMessages( String[] projection, String selection, String[] selectionArgs, String sortOrder, String smsTable, String pduTable)1247 private Cursor getUndeliveredMessages( 1248 String[] projection, String selection, String[] selectionArgs, 1249 String sortOrder, String smsTable, String pduTable) { 1250 String[] mmsProjection = createMmsProjection(projection, pduTable); 1251 1252 SQLiteQueryBuilder mmsQueryBuilder = new SQLiteQueryBuilder(); 1253 SQLiteQueryBuilder smsQueryBuilder = new SQLiteQueryBuilder(); 1254 1255 mmsQueryBuilder.setTables(joinPduAndPendingMsgTables(pduTable)); 1256 smsQueryBuilder.setTables(smsTable); 1257 1258 String finalMmsSelection = concatSelections( 1259 selection, Mms.MESSAGE_BOX + " = " + Mms.MESSAGE_BOX_OUTBOX); 1260 String finalSmsSelection = concatSelections( 1261 selection, "(" + Sms.TYPE + " = " + Sms.MESSAGE_TYPE_OUTBOX 1262 + " OR " + Sms.TYPE + " = " + Sms.MESSAGE_TYPE_FAILED 1263 + " OR " + Sms.TYPE + " = " + Sms.MESSAGE_TYPE_QUEUED + ")"); 1264 1265 String[] smsColumns = handleNullMessageProjection(projection); 1266 String[] mmsColumns = handleNullMessageProjection(mmsProjection); 1267 String[] innerMmsProjection = makeProjectionWithDateAndThreadId( 1268 mmsColumns, 1000); 1269 String[] innerSmsProjection = makeProjectionWithDateAndThreadId( 1270 smsColumns, 1); 1271 1272 Set<String> columnsPresentInTable = new HashSet<String>(MMS_COLUMNS); 1273 columnsPresentInTable.add(pduTable + "._id"); 1274 columnsPresentInTable.add(PendingMessages.ERROR_TYPE); 1275 String mmsSubQuery = mmsQueryBuilder.buildUnionSubQuery( 1276 MmsSms.TYPE_DISCRIMINATOR_COLUMN, innerMmsProjection, 1277 columnsPresentInTable, 1, "mms", finalMmsSelection, 1278 null, null); 1279 String smsSubQuery = smsQueryBuilder.buildUnionSubQuery( 1280 MmsSms.TYPE_DISCRIMINATOR_COLUMN, innerSmsProjection, 1281 SMS_COLUMNS, 1, "sms", finalSmsSelection, 1282 null, null); 1283 SQLiteQueryBuilder unionQueryBuilder = new SQLiteQueryBuilder(); 1284 1285 unionQueryBuilder.setDistinct(true); 1286 1287 String unionQuery = unionQueryBuilder.buildUnionQuery( 1288 new String[] { smsSubQuery, mmsSubQuery }, null, null); 1289 1290 SQLiteQueryBuilder outerQueryBuilder = new SQLiteQueryBuilder(); 1291 1292 outerQueryBuilder.setTables("(" + unionQuery + ")"); 1293 1294 String outerQuery = outerQueryBuilder.buildQuery( 1295 smsColumns, null, null, null, sortOrder, null); 1296 1297 return mOpenHelper.getReadableDatabase().rawQuery(outerQuery, EMPTY_STRING_ARRAY); 1298 } 1299 1300 /** 1301 * Add normalized date to the list of columns for an inner 1302 * projection. 1303 */ makeProjectionWithNormalizedDate( String[] projection, int dateMultiple)1304 private static String[] makeProjectionWithNormalizedDate( 1305 String[] projection, int dateMultiple) { 1306 int projectionSize = projection.length; 1307 String[] result = new String[projectionSize + 1]; 1308 1309 result[0] = "date * " + dateMultiple + " AS normalized_date"; 1310 System.arraycopy(projection, 0, result, 1, projectionSize); 1311 return result; 1312 } 1313 buildConversationQuery(String[] projection, String selection, String sortOrder, String smsTable, String pduTable)1314 private static String buildConversationQuery(String[] projection, 1315 String selection, String sortOrder, String smsTable, String pduTable) { 1316 String[] mmsProjection = createMmsProjection(projection, pduTable); 1317 1318 SQLiteQueryBuilder mmsQueryBuilder = new SQLiteQueryBuilder(); 1319 SQLiteQueryBuilder smsQueryBuilder = new SQLiteQueryBuilder(); 1320 1321 mmsQueryBuilder.setDistinct(true); 1322 smsQueryBuilder.setDistinct(true); 1323 mmsQueryBuilder.setTables(joinPduAndPendingMsgTables(pduTable)); 1324 smsQueryBuilder.setTables(smsTable); 1325 1326 String[] smsColumns = handleNullMessageProjection(projection); 1327 String[] mmsColumns = handleNullMessageProjection(mmsProjection); 1328 String[] innerMmsProjection = makeProjectionWithNormalizedDate(mmsColumns, 1000); 1329 String[] innerSmsProjection = makeProjectionWithNormalizedDate(smsColumns, 1); 1330 1331 Set<String> columnsPresentInTable = new HashSet<String>(MMS_COLUMNS); 1332 columnsPresentInTable.add(pduTable + "._id"); 1333 columnsPresentInTable.add(PendingMessages.ERROR_TYPE); 1334 1335 String mmsSelection = concatSelections(selection, 1336 Mms.MESSAGE_BOX + " != " + Mms.MESSAGE_BOX_DRAFTS); 1337 String mmsSubQuery = mmsQueryBuilder.buildUnionSubQuery( 1338 MmsSms.TYPE_DISCRIMINATOR_COLUMN, innerMmsProjection, 1339 columnsPresentInTable, 0, "mms", 1340 concatSelections(mmsSelection, MMS_CONVERSATION_CONSTRAINT), 1341 null, null); 1342 String smsSubQuery = smsQueryBuilder.buildUnionSubQuery( 1343 MmsSms.TYPE_DISCRIMINATOR_COLUMN, innerSmsProjection, SMS_COLUMNS, 1344 0, "sms", concatSelections(selection, SMS_CONVERSATION_CONSTRAINT), 1345 null, null); 1346 SQLiteQueryBuilder unionQueryBuilder = new SQLiteQueryBuilder(); 1347 1348 unionQueryBuilder.setDistinct(true); 1349 1350 String unionQuery = unionQueryBuilder.buildUnionQuery( 1351 new String[] { smsSubQuery, mmsSubQuery }, 1352 handleNullSortOrder(sortOrder), null); 1353 1354 SQLiteQueryBuilder outerQueryBuilder = new SQLiteQueryBuilder(); 1355 1356 outerQueryBuilder.setTables("(" + unionQuery + ")"); 1357 1358 return outerQueryBuilder.buildQuery( 1359 smsColumns, null, null, null, sortOrder, null); 1360 } 1361 1362 @Override getType(Uri uri)1363 public String getType(Uri uri) { 1364 return VND_ANDROID_DIR_MMS_SMS; 1365 } 1366 1367 @Override delete(Uri uri, String selection, String[] selectionArgs)1368 public int delete(Uri uri, String selection, String[] selectionArgs) { 1369 final UserHandle callerUserHandle = Binder.getCallingUserHandle(); 1370 String selectionBySubIds; 1371 final long token = Binder.clearCallingIdentity(); 1372 try { 1373 // Filter MMS/SMS based on subId 1374 selectionBySubIds = ProviderUtil.getSelectionBySubIds(getContext(), callerUserHandle); 1375 } finally { 1376 Binder.restoreCallingIdentity(token); 1377 } 1378 1379 SQLiteDatabase db = mOpenHelper.getWritableDatabase(); 1380 String debugMessage = getCallingPackage() + ";MmsSmsProvider.delete;" + uri; 1381 // Always log delete for debug purpose, as delete is a critical but non-frequent operation. 1382 Log.d(LOG_TAG, debugMessage); 1383 if (mOpenHelper instanceof MmsSmsDatabaseHelper) { 1384 ((MmsSmsDatabaseHelper) mOpenHelper).addDatabaseOpeningDebugLog( 1385 debugMessage, false); 1386 } 1387 Context context = getContext(); 1388 int affectedRows = 0; 1389 1390 switch(URI_MATCHER.match(uri)) { 1391 case URI_CONVERSATIONS_MESSAGES: 1392 long threadId; 1393 try { 1394 threadId = Long.parseLong(uri.getLastPathSegment()); 1395 } catch (NumberFormatException e) { 1396 Log.e(LOG_TAG, "Thread ID must be a long."); 1397 break; 1398 } 1399 1400 if (selectionBySubIds == null) { 1401 // No subscriptions associated with user, return 0. 1402 return 0; 1403 } 1404 selection = DatabaseUtils.concatenateWhere(selectionBySubIds, selection); 1405 1406 affectedRows = deleteConversation(uri, selection, selectionArgs); 1407 MmsSmsDatabaseHelper.updateThread(db, threadId); 1408 break; 1409 case URI_CONVERSATIONS: 1410 if (selectionBySubIds == null) { 1411 // No subscriptions associated with user, return 0. 1412 return 0; 1413 } 1414 selection = DatabaseUtils.concatenateWhere(selectionBySubIds, selection); 1415 1416 affectedRows = MmsProvider.deleteMessages(context, db, 1417 selection, selectionArgs, uri) 1418 + db.delete("sms", selection, selectionArgs); 1419 // Intentionally don't pass the selection variable to updateThreads. 1420 // When we pass in "locked=0" there, the thread will get excluded from 1421 // the selection and not get updated. 1422 MmsSmsDatabaseHelper.updateThreads(db, null, null); 1423 break; 1424 case URI_OBSOLETE_THREADS: 1425 affectedRows = db.delete(TABLE_THREADS, 1426 "_id NOT IN (SELECT DISTINCT thread_id FROM sms where thread_id NOT NULL " + 1427 "UNION SELECT DISTINCT thread_id FROM pdu where thread_id NOT NULL)", null); 1428 break; 1429 default: 1430 throw new UnsupportedOperationException(NO_DELETES_INSERTS_OR_UPDATES + uri); 1431 } 1432 1433 if (affectedRows > 0) { 1434 context.getContentResolver().notifyChange(MmsSms.CONTENT_URI, null, true, 1435 UserHandle.USER_ALL); 1436 } 1437 return affectedRows; 1438 } 1439 1440 /** 1441 * Delete the conversation with the given thread ID. 1442 */ deleteConversation(Uri uri, String selection, String[] selectionArgs)1443 private int deleteConversation(Uri uri, String selection, String[] selectionArgs) { 1444 String threadId = uri.getLastPathSegment(); 1445 1446 SQLiteDatabase db = mOpenHelper.getWritableDatabase(); 1447 String finalSelection = concatSelections(selection, "thread_id = " + threadId); 1448 return MmsProvider.deleteMessages(getContext(), db, finalSelection, 1449 selectionArgs, uri) 1450 + db.delete("sms", finalSelection, selectionArgs); 1451 } 1452 1453 @Override insert(Uri uri, ContentValues values)1454 public Uri insert(Uri uri, ContentValues values) { 1455 final UserHandle callerUserHandle = Binder.getCallingUserHandle(); 1456 final int callerUid = Binder.getCallingUid(); 1457 SQLiteDatabase db = mOpenHelper.getWritableDatabase(); 1458 if (mOpenHelper instanceof MmsSmsDatabaseHelper) { 1459 ((MmsSmsDatabaseHelper) mOpenHelper).addDatabaseOpeningDebugLog( 1460 getCallingPackage() + ";MmsSmsProvider.insert;" + uri, false); 1461 } 1462 1463 int matchIndex = URI_MATCHER.match(uri); 1464 // TODO (b/256992531): Currently, one sim card is set as default sms subId in work 1465 // profile. Default sms subId should be updated based on user pref. 1466 int defaultSmsSubId = SmsManager.getDefaultSmsSubscriptionId(); 1467 if (matchIndex == URI_PENDING_MSG) { 1468 int subId; 1469 if (values.containsKey(PendingMessages.SUBSCRIPTION_ID)) { 1470 subId = values.getAsInteger(PendingMessages.SUBSCRIPTION_ID); 1471 } else { 1472 subId = defaultSmsSubId; 1473 if (SubscriptionManager.isValidSubscriptionId(subId)) { 1474 values.put(PendingMessages.SUBSCRIPTION_ID, subId); 1475 } 1476 } 1477 1478 if (!ProviderUtil 1479 .allowInteractingWithEntryOfSubscription(getContext(), subId, 1480 callerUserHandle)) { 1481 TelephonyUtils.showSwitchToManagedProfileDialogIfAppropriate(getContext(), subId, 1482 callerUid, getCallingPackage()); 1483 return null; 1484 } 1485 1486 long rowId = db.insert(TABLE_PENDING_MSG, null, values); 1487 return uri.buildUpon().appendPath(Long.toString(rowId)).build(); 1488 } else if (matchIndex == URI_CANONICAL_ADDRESS) { 1489 if (!values.containsKey(CanonicalAddressesColumns.SUBSCRIPTION_ID)) { 1490 if (SubscriptionManager.isValidSubscriptionId(defaultSmsSubId)) { 1491 values.put(CanonicalAddressesColumns.SUBSCRIPTION_ID, defaultSmsSubId); 1492 } 1493 } 1494 1495 long rowId = db.insert(TABLE_CANONICAL_ADDRESSES, null, values); 1496 return uri.buildUpon().appendPath(Long.toString(rowId)).build(); 1497 } 1498 throw new UnsupportedOperationException(NO_DELETES_INSERTS_OR_UPDATES + uri); 1499 } 1500 1501 @Override update(Uri uri, ContentValues values, String selection, String[] selectionArgs)1502 public int update(Uri uri, ContentValues values, 1503 String selection, String[] selectionArgs) { 1504 final int callerUid = Binder.getCallingUid(); 1505 final UserHandle callerUserHandle = Binder.getCallingUserHandle(); 1506 final String callerPkg = getCallingPackage(); 1507 1508 String selectionBySubIds; 1509 final long token = Binder.clearCallingIdentity(); 1510 try { 1511 // Filter MMS/SMS based on subId. 1512 selectionBySubIds = ProviderUtil.getSelectionBySubIds(getContext(), callerUserHandle); 1513 } finally { 1514 Binder.restoreCallingIdentity(token); 1515 } 1516 1517 SQLiteDatabase db = mOpenHelper.getWritableDatabase(); 1518 if (mOpenHelper instanceof MmsSmsDatabaseHelper) { 1519 ((MmsSmsDatabaseHelper) mOpenHelper).addDatabaseOpeningDebugLog( 1520 callerPkg + ";MmsSmsProvider.update;" + uri, false); 1521 } 1522 1523 int affectedRows = 0; 1524 switch(URI_MATCHER.match(uri)) { 1525 case URI_CONVERSATIONS_MESSAGES: 1526 if (selectionBySubIds == null) { 1527 // No subscriptions associated with user, return 0. 1528 return 0; 1529 } 1530 selection = DatabaseUtils.concatenateWhere(selection, selectionBySubIds); 1531 1532 String threadIdString = uri.getPathSegments().get(1); 1533 affectedRows = updateConversation(threadIdString, values, 1534 selection, selectionArgs, callerUid, callerPkg); 1535 break; 1536 1537 case URI_PENDING_MSG: 1538 if (selectionBySubIds == null) { 1539 // No subscriptions associated with user, return 0. 1540 return 0; 1541 } 1542 // In PendingMessages table, SUBSCRIPTION_ID column name is pending_sub_id. 1543 selectionBySubIds = "pending_" + selectionBySubIds; 1544 selection = DatabaseUtils.concatenateWhere(selection, selectionBySubIds); 1545 1546 affectedRows = db.update(TABLE_PENDING_MSG, values, selection, null); 1547 break; 1548 1549 case URI_CANONICAL_ADDRESS: { 1550 if (selectionBySubIds == null) { 1551 // No subscriptions associated with user, return 0. 1552 return 0; 1553 } 1554 selection = DatabaseUtils.concatenateWhere(selection, selectionBySubIds); 1555 1556 String extraSelection = "_id=" + uri.getPathSegments().get(1); 1557 String finalSelection = TextUtils.isEmpty(selection) 1558 ? extraSelection : extraSelection + " AND " + selection; 1559 1560 affectedRows = db.update(TABLE_CANONICAL_ADDRESSES, values, finalSelection, null); 1561 break; 1562 } 1563 1564 case URI_CONVERSATIONS: { 1565 if (selectionBySubIds == null) { 1566 // No subscriptions associated with user, return 0. 1567 return 0; 1568 } 1569 selection = DatabaseUtils.concatenateWhere(selection, selectionBySubIds); 1570 1571 final ContentValues finalValues = new ContentValues(1); 1572 if (values.containsKey(Threads.ARCHIVED)) { 1573 // Only allow update archived 1574 finalValues.put(Threads.ARCHIVED, values.getAsBoolean(Threads.ARCHIVED)); 1575 } 1576 affectedRows = db.update(TABLE_THREADS, finalValues, selection, selectionArgs); 1577 break; 1578 } 1579 1580 default: 1581 throw new UnsupportedOperationException( 1582 NO_DELETES_INSERTS_OR_UPDATES + uri); 1583 } 1584 1585 if (affectedRows > 0) { 1586 getContext().getContentResolver().notifyChange( 1587 MmsSms.CONTENT_URI, null, true, UserHandle.USER_ALL); 1588 } 1589 return affectedRows; 1590 } 1591 updateConversation(String threadIdString, ContentValues values, String selection, String[] selectionArgs, int callerUid, String callerPkg)1592 private int updateConversation(String threadIdString, ContentValues values, String selection, 1593 String[] selectionArgs, int callerUid, String callerPkg) { 1594 try { 1595 Long.parseLong(threadIdString); 1596 } catch (NumberFormatException exception) { 1597 Log.e(LOG_TAG, "Thread ID must be a Long."); 1598 return 0; 1599 1600 } 1601 if (ProviderUtil.shouldRemoveCreator(values, callerUid)) { 1602 // CREATOR should not be changed by non-SYSTEM/PHONE apps 1603 Log.w(LOG_TAG, callerPkg + " tries to update CREATOR"); 1604 // Sms.CREATOR and Mms.CREATOR are same. But let's do this 1605 // twice in case the names may differ in the future 1606 values.remove(Sms.CREATOR); 1607 values.remove(Mms.CREATOR); 1608 } 1609 1610 SQLiteDatabase db = mOpenHelper.getWritableDatabase(); 1611 String finalSelection = concatSelections(selection, "thread_id=" + threadIdString); 1612 return db.update(MmsProvider.TABLE_PDU, values, finalSelection, selectionArgs) 1613 + db.update("sms", values, finalSelection, selectionArgs); 1614 } 1615 1616 /** 1617 * Construct Sets of Strings containing exactly the columns 1618 * present in each table. We will use this when constructing 1619 * UNION queries across the MMS and SMS tables. 1620 */ initializeColumnSets()1621 private static void initializeColumnSets() { 1622 int commonColumnCount = MMS_SMS_COLUMNS.length; 1623 int mmsOnlyColumnCount = MMS_ONLY_COLUMNS.length; 1624 int smsOnlyColumnCount = SMS_ONLY_COLUMNS.length; 1625 Set<String> unionColumns = new HashSet<String>(); 1626 1627 for (int i = 0; i < commonColumnCount; i++) { 1628 MMS_COLUMNS.add(MMS_SMS_COLUMNS[i]); 1629 SMS_COLUMNS.add(MMS_SMS_COLUMNS[i]); 1630 unionColumns.add(MMS_SMS_COLUMNS[i]); 1631 } 1632 for (int i = 0; i < mmsOnlyColumnCount; i++) { 1633 MMS_COLUMNS.add(MMS_ONLY_COLUMNS[i]); 1634 unionColumns.add(MMS_ONLY_COLUMNS[i]); 1635 } 1636 for (int i = 0; i < smsOnlyColumnCount; i++) { 1637 SMS_COLUMNS.add(SMS_ONLY_COLUMNS[i]); 1638 unionColumns.add(SMS_ONLY_COLUMNS[i]); 1639 } 1640 1641 int i = 0; 1642 for (String columnName : unionColumns) { 1643 UNION_COLUMNS[i++] = columnName; 1644 } 1645 } 1646 1647 @Override dump(FileDescriptor fd, PrintWriter writer, String[] args)1648 public void dump(FileDescriptor fd, PrintWriter writer, String[] args) { 1649 // Dump default SMS app 1650 String defaultSmsApp = Telephony.Sms.getDefaultSmsPackage(getContext()); 1651 if (TextUtils.isEmpty(defaultSmsApp)) { 1652 defaultSmsApp = "None"; 1653 } 1654 writer.println("Default SMS app: " + defaultSmsApp); 1655 } 1656 1657 @Override call(String method, String arg, Bundle extras)1658 public Bundle call(String method, String arg, Bundle extras) { 1659 if (ProviderUtil.isAccessRestricted( 1660 getContext(), getCallingPackage(), Binder.getCallingUid())) { 1661 return null; 1662 } 1663 if (METHOD_IS_RESTORING.equals(method)) { 1664 Bundle result = new Bundle(); 1665 result.putBoolean(IS_RESTORING_KEY, TelephonyBackupAgent.getIsRestoring()); 1666 return result; 1667 } else if (METHOD_GARBAGE_COLLECT.equals(method)) { 1668 Bundle result = new Bundle(); 1669 boolean doDelete = TextUtils.equals(DO_DELETE, arg); 1670 MmsPartsCleanup.cleanupDanglingParts(getContext(), doDelete, result); 1671 return result; 1672 } 1673 Log.w(LOG_TAG, "Ignored unsupported " + method + " call"); 1674 return null; 1675 } 1676 } 1677