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