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