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