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