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