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