1 /* 2 * Copyright (C) 2008 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.providers.telephony; 18 19 import java.util.ArrayList; 20 import java.util.Arrays; 21 import java.util.HashSet; 22 import java.util.List; 23 import java.util.Set; 24 25 import android.app.SearchManager; 26 import android.content.ContentProvider; 27 import android.content.ContentValues; 28 import android.content.Context; 29 import android.content.UriMatcher; 30 import android.database.Cursor; 31 import android.database.DatabaseUtils; 32 import android.database.sqlite.SQLiteDatabase; 33 import android.database.sqlite.SQLiteOpenHelper; 34 import android.database.sqlite.SQLiteQueryBuilder; 35 import android.net.Uri; 36 import android.provider.BaseColumns; 37 import android.provider.Telephony.CanonicalAddressesColumns; 38 import android.provider.Telephony.Mms; 39 import android.provider.Telephony.MmsSms; 40 import android.provider.Telephony.Sms; 41 import android.provider.Telephony.Threads; 42 import android.provider.Telephony.ThreadsColumns; 43 import android.provider.Telephony.MmsSms.PendingMessages; 44 import android.provider.Telephony.Sms.Conversations; 45 import android.text.TextUtils; 46 import android.util.Log; 47 48 import 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 */ 71 public 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 { URI_MATCHER.addURI(AUTHORITY, "conversations", URI_CONVERSATIONS)181 URI_MATCHER.addURI(AUTHORITY, "conversations", URI_CONVERSATIONS); URI_MATCHER.addURI(AUTHORITY, "complete-conversations", URI_COMPLETE_CONVERSATIONS)182 URI_MATCHER.addURI(AUTHORITY, "complete-conversations", URI_COMPLETE_CONVERSATIONS); 183 184 // In these patterns, "#" is the thread ID. URI_MATCHER.addURI( AUTHORITY, "conversations/#", URI_CONVERSATIONS_MESSAGES)185 URI_MATCHER.addURI( 186 AUTHORITY, "conversations/#", URI_CONVERSATIONS_MESSAGES); URI_MATCHER.addURI( AUTHORITY, "conversations/#/recipients", URI_CONVERSATIONS_RECIPIENTS)187 URI_MATCHER.addURI( 188 AUTHORITY, "conversations/#/recipients", 189 URI_CONVERSATIONS_RECIPIENTS); 190 URI_MATCHER.addURI( AUTHORITY, "conversations/#/subject", URI_CONVERSATIONS_SUBJECT)191 URI_MATCHER.addURI( 192 AUTHORITY, "conversations/#/subject", 193 URI_CONVERSATIONS_SUBJECT); 194 195 // URI for deleting obsolete threads. URI_MATCHER.addURI(AUTHORITY, "conversations/obsolete", URI_OBSOLETE_THREADS)196 URI_MATCHER.addURI(AUTHORITY, "conversations/obsolete", URI_OBSOLETE_THREADS); 197 URI_MATCHER.addURI( AUTHORITY, "messages/byphone/*", URI_MESSAGES_BY_PHONE)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. URI_MATCHER.addURI(AUTHORITY, "threadID", URI_THREAD_ID)205 URI_MATCHER.addURI(AUTHORITY, "threadID", URI_THREAD_ID); 206 207 // Use this pattern to query the canonical address by given ID. URI_MATCHER.addURI(AUTHORITY, "canonical-address/#", URI_CANONICAL_ADDRESS)208 URI_MATCHER.addURI(AUTHORITY, "canonical-address/#", URI_CANONICAL_ADDRESS); 209 210 // Use this pattern to query all canonical addresses. URI_MATCHER.addURI(AUTHORITY, "canonical-addresses", URI_CANONICAL_ADDRESSES)211 URI_MATCHER.addURI(AUTHORITY, "canonical-addresses", URI_CANONICAL_ADDRESSES); 212 URI_MATCHER.addURI(AUTHORITY, "search", URI_SEARCH)213 URI_MATCHER.addURI(AUTHORITY, "search", URI_SEARCH); URI_MATCHER.addURI(AUTHORITY, "searchSuggest", URI_SEARCH_SUGGEST)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 // URI_MATCHER.addURI(AUTHORITY, "pending", URI_PENDING_MSG)225 URI_MATCHER.addURI(AUTHORITY, "pending", URI_PENDING_MSG); 226 227 // Use this pattern to get a list of undelivered messages. URI_MATCHER.addURI(AUTHORITY, "undelivered", URI_UNDELIVERED_MSG)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. URI_MATCHER.addURI(AUTHORITY, "notifications", URI_NOTIFICATIONS)232 URI_MATCHER.addURI(AUTHORITY, "notifications", URI_NOTIFICATIONS); 233 URI_MATCHER.addURI(AUTHORITY, "draft", URI_DRAFT)234 URI_MATCHER.addURI(AUTHORITY, "draft", URI_DRAFT); 235 URI_MATCHER.addURI(AUTHORITY, "locked", URI_FIRST_LOCKED_MESSAGE_ALL)236 URI_MATCHER.addURI(AUTHORITY, "locked", URI_FIRST_LOCKED_MESSAGE_ALL); 237 URI_MATCHER.addURI(AUTHORITY, "locked/#", URI_FIRST_LOCKED_MESSAGE_BY_THREAD_ID)238 URI_MATCHER.addURI(AUTHORITY, "locked/#", URI_FIRST_LOCKED_MESSAGE_BY_THREAD_ID); 239 initializeColumnSets()240 initializeColumnSets(); 241 } 242 243 private SQLiteOpenHelper mOpenHelper; 244 245 private boolean mUseStrictPhoneNumberComparation; 246 247 @Override onCreate()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 query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder)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 */ getSingleAddressId(String address)454 private long getSingleAddressId(String address) { 455 boolean isEmail = Mms.isEmailAddress(address); 456 boolean isPhoneNumber = Mms.isPhoneNumber(address); 457 458 // We lowercase all email addresses, but not addresses that aren't numbers, because 459 // that would incorrectly turn an address such as "My Vodafone" into "my vodafone" 460 // and the thread title would be incorrect when displayed in the UI. 461 String refinedAddress = isEmail ? address.toLowerCase() : address; 462 463 String selection = "address=?"; 464 String[] selectionArgs; 465 long retVal = -1L; 466 467 if (!isPhoneNumber) { 468 selectionArgs = new String[] { refinedAddress }; 469 } else { 470 selection += " OR " + String.format("PHONE_NUMBERS_EQUAL(address, ?, %d)", 471 (mUseStrictPhoneNumberComparation ? 1 : 0)); 472 selectionArgs = new String[] { refinedAddress, refinedAddress }; 473 } 474 475 Cursor cursor = null; 476 477 try { 478 SQLiteDatabase db = mOpenHelper.getReadableDatabase(); 479 cursor = db.query( 480 "canonical_addresses", ID_PROJECTION, 481 selection, selectionArgs, null, null, null); 482 483 if (cursor.getCount() == 0) { 484 ContentValues contentValues = new ContentValues(1); 485 contentValues.put(CanonicalAddressesColumns.ADDRESS, refinedAddress); 486 487 db = mOpenHelper.getWritableDatabase(); 488 retVal = db.insert("canonical_addresses", 489 CanonicalAddressesColumns.ADDRESS, contentValues); 490 491 Log.d(LOG_TAG, "getSingleAddressId: insert new canonical_address for " + 492 /*address*/ "xxxxxx" + ", _id=" + retVal); 493 494 return retVal; 495 } 496 497 if (cursor.moveToFirst()) { 498 retVal = cursor.getLong(cursor.getColumnIndexOrThrow(BaseColumns._ID)); 499 } 500 } finally { 501 if (cursor != null) { 502 cursor.close(); 503 } 504 } 505 506 return retVal; 507 } 508 509 /** 510 * Return the canonical address IDs for these addresses. 511 */ getAddressIds(List<String> addresses)512 private Set<Long> getAddressIds(List<String> addresses) { 513 Set<Long> result = new HashSet<Long>(addresses.size()); 514 515 for (String address : addresses) { 516 if (!address.equals(PduHeaders.FROM_INSERT_ADDRESS_TOKEN_STR)) { 517 long id = getSingleAddressId(address); 518 if (id != -1L) { 519 result.add(id); 520 } else { 521 Log.e(LOG_TAG, "getAddressIds: address ID not found for " + address); 522 } 523 } 524 } 525 return result; 526 } 527 528 /** 529 * Return a sorted array of the given Set of Longs. 530 */ getSortedSet(Set<Long> numbers)531 private long[] getSortedSet(Set<Long> numbers) { 532 int size = numbers.size(); 533 long[] result = new long[size]; 534 int i = 0; 535 536 for (Long number : numbers) { 537 result[i++] = number; 538 } 539 540 if (size > 1) { 541 Arrays.sort(result); 542 } 543 544 return result; 545 } 546 547 /** 548 * Return a String of the numbers in the given array, in order, 549 * separated by spaces. 550 */ getSpaceSeparatedNumbers(long[] numbers)551 private String getSpaceSeparatedNumbers(long[] numbers) { 552 int size = numbers.length; 553 StringBuilder buffer = new StringBuilder(); 554 555 for (int i = 0; i < size; i++) { 556 if (i != 0) { 557 buffer.append(' '); 558 } 559 buffer.append(numbers[i]); 560 } 561 return buffer.toString(); 562 } 563 564 /** 565 * Insert a record for a new thread. 566 */ insertThread(String recipientIds, int numberOfRecipients)567 private void insertThread(String recipientIds, int numberOfRecipients) { 568 ContentValues values = new ContentValues(4); 569 570 long date = System.currentTimeMillis(); 571 values.put(ThreadsColumns.DATE, date - date % 1000); 572 values.put(ThreadsColumns.RECIPIENT_IDS, recipientIds); 573 if (numberOfRecipients > 1) { 574 values.put(Threads.TYPE, Threads.BROADCAST_THREAD); 575 } 576 values.put(ThreadsColumns.MESSAGE_COUNT, 0); 577 578 long result = mOpenHelper.getWritableDatabase().insert("threads", null, values); 579 Log.d(LOG_TAG, "insertThread: created new thread_id " + result + 580 " for recipientIds " + /*recipientIds*/ "xxxxxxx"); 581 582 getContext().getContentResolver().notifyChange(MmsSms.CONTENT_URI, null); 583 } 584 585 private static final String THREAD_QUERY = 586 "SELECT _id FROM threads " + "WHERE recipient_ids=?"; 587 588 /** 589 * Return the thread ID for this list of 590 * recipients IDs. If no thread exists with this ID, create 591 * one and return it. Callers should always use 592 * Threads.getThreadId to access this information. 593 */ getThreadId(List<String> recipients)594 private synchronized Cursor getThreadId(List<String> recipients) { 595 Set<Long> addressIds = getAddressIds(recipients); 596 String recipientIds = ""; 597 598 // optimize for size==1, which should be most of the cases 599 if (addressIds.size() == 1) { 600 for (Long addressId : addressIds) { 601 recipientIds = Long.toString(addressId); 602 } 603 } else { 604 recipientIds = getSpaceSeparatedNumbers(getSortedSet(addressIds)); 605 } 606 607 if (Log.isLoggable(LOG_TAG, Log.VERBOSE)) { 608 Log.d(LOG_TAG, "getThreadId: recipientIds (selectionArgs) =" + 609 /*recipientIds*/ "xxxxxxx"); 610 } 611 612 String[] selectionArgs = new String[] { recipientIds }; 613 SQLiteDatabase db = mOpenHelper.getReadableDatabase(); 614 Cursor cursor = db.rawQuery(THREAD_QUERY, selectionArgs); 615 616 if (cursor.getCount() == 0) { 617 cursor.close(); 618 619 Log.d(LOG_TAG, "getThreadId: create new thread_id for recipients " + 620 /*recipients*/ "xxxxxxxx"); 621 insertThread(recipientIds, recipients.size()); 622 623 db = mOpenHelper.getReadableDatabase(); // In case insertThread closed it 624 cursor = db.rawQuery(THREAD_QUERY, selectionArgs); 625 } 626 627 if (cursor.getCount() > 1) { 628 Log.w(LOG_TAG, "getThreadId: why is cursorCount=" + cursor.getCount()); 629 } 630 631 return cursor; 632 } 633 concatSelections(String selection1, String selection2)634 private static String concatSelections(String selection1, String selection2) { 635 if (TextUtils.isEmpty(selection1)) { 636 return selection2; 637 } else if (TextUtils.isEmpty(selection2)) { 638 return selection1; 639 } else { 640 return selection1 + " AND " + selection2; 641 } 642 } 643 644 /** 645 * If a null projection is given, return the union of all columns 646 * in both the MMS and SMS messages tables. Otherwise, return the 647 * given projection. 648 */ handleNullMessageProjection( String[] projection)649 private static String[] handleNullMessageProjection( 650 String[] projection) { 651 return projection == null ? UNION_COLUMNS : projection; 652 } 653 654 /** 655 * If a null projection is given, return the set of all columns in 656 * the threads table. Otherwise, return the given projection. 657 */ handleNullThreadsProjection( String[] projection)658 private static String[] handleNullThreadsProjection( 659 String[] projection) { 660 return projection == null ? THREADS_COLUMNS : projection; 661 } 662 663 /** 664 * If a null sort order is given, return "normalized_date ASC". 665 * Otherwise, return the given sort order. 666 */ handleNullSortOrder(String sortOrder)667 private static String handleNullSortOrder (String sortOrder) { 668 return sortOrder == null ? "normalized_date ASC" : sortOrder; 669 } 670 671 /** 672 * Return existing threads in the database. 673 */ getSimpleConversations(String[] projection, String selection, String[] selectionArgs, String sortOrder)674 private Cursor getSimpleConversations(String[] projection, String selection, 675 String[] selectionArgs, String sortOrder) { 676 return mOpenHelper.getReadableDatabase().query("threads", projection, 677 selection, selectionArgs, null, null, " date DESC"); 678 } 679 680 /** 681 * Return the thread which has draft in both MMS and SMS. 682 * 683 * Use this query: 684 * 685 * SELECT ... 686 * FROM (SELECT _id, thread_id, ... 687 * FROM pdu 688 * WHERE msg_box = 3 AND ... 689 * UNION 690 * SELECT _id, thread_id, ... 691 * FROM sms 692 * WHERE type = 3 AND ... 693 * ) 694 * ; 695 */ getDraftThread(String[] projection, String selection, String[] selectionArgs, String sortOrder)696 private Cursor getDraftThread(String[] projection, String selection, 697 String[] selectionArgs, String sortOrder) { 698 String[] innerProjection = new String[] {BaseColumns._ID, Conversations.THREAD_ID}; 699 SQLiteQueryBuilder mmsQueryBuilder = new SQLiteQueryBuilder(); 700 SQLiteQueryBuilder smsQueryBuilder = new SQLiteQueryBuilder(); 701 702 mmsQueryBuilder.setTables(MmsProvider.TABLE_PDU); 703 smsQueryBuilder.setTables(SmsProvider.TABLE_SMS); 704 705 String mmsSubQuery = mmsQueryBuilder.buildUnionSubQuery( 706 MmsSms.TYPE_DISCRIMINATOR_COLUMN, innerProjection, 707 MMS_COLUMNS, 1, "mms", 708 concatSelections(selection, Mms.MESSAGE_BOX + "=" + Mms.MESSAGE_BOX_DRAFTS), 709 selectionArgs, null, null); 710 String smsSubQuery = smsQueryBuilder.buildUnionSubQuery( 711 MmsSms.TYPE_DISCRIMINATOR_COLUMN, innerProjection, 712 SMS_COLUMNS, 1, "sms", 713 concatSelections(selection, Sms.TYPE + "=" + Sms.MESSAGE_TYPE_DRAFT), 714 selectionArgs, null, null); 715 SQLiteQueryBuilder unionQueryBuilder = new SQLiteQueryBuilder(); 716 717 unionQueryBuilder.setDistinct(true); 718 719 String unionQuery = unionQueryBuilder.buildUnionQuery( 720 new String[] { mmsSubQuery, smsSubQuery }, null, null); 721 722 SQLiteQueryBuilder outerQueryBuilder = new SQLiteQueryBuilder(); 723 724 outerQueryBuilder.setTables("(" + unionQuery + ")"); 725 726 String outerQuery = outerQueryBuilder.buildQuery( 727 projection, null, null, null, null, sortOrder, null); 728 729 return mOpenHelper.getReadableDatabase().rawQuery(outerQuery, EMPTY_STRING_ARRAY); 730 } 731 732 /** 733 * Return the most recent message in each conversation in both MMS 734 * and SMS. 735 * 736 * Use this query: 737 * 738 * SELECT ... 739 * FROM (SELECT thread_id AS tid, date * 1000 AS normalized_date, ... 740 * FROM pdu 741 * WHERE msg_box != 3 AND ... 742 * GROUP BY thread_id 743 * HAVING date = MAX(date) 744 * UNION 745 * SELECT thread_id AS tid, date AS normalized_date, ... 746 * FROM sms 747 * WHERE ... 748 * GROUP BY thread_id 749 * HAVING date = MAX(date)) 750 * GROUP BY tid 751 * HAVING normalized_date = MAX(normalized_date); 752 * 753 * The msg_box != 3 comparisons ensure that we don't include draft 754 * messages. 755 */ getConversations(String[] projection, String selection, String[] selectionArgs, String sortOrder)756 private Cursor getConversations(String[] projection, String selection, 757 String[] selectionArgs, String sortOrder) { 758 SQLiteQueryBuilder mmsQueryBuilder = new SQLiteQueryBuilder(); 759 SQLiteQueryBuilder smsQueryBuilder = new SQLiteQueryBuilder(); 760 761 mmsQueryBuilder.setTables(MmsProvider.TABLE_PDU); 762 smsQueryBuilder.setTables(SmsProvider.TABLE_SMS); 763 764 String[] columns = handleNullMessageProjection(projection); 765 String[] innerMmsProjection = makeProjectionWithDateAndThreadId( 766 UNION_COLUMNS, 1000); 767 String[] innerSmsProjection = makeProjectionWithDateAndThreadId( 768 UNION_COLUMNS, 1); 769 String mmsSubQuery = mmsQueryBuilder.buildUnionSubQuery( 770 MmsSms.TYPE_DISCRIMINATOR_COLUMN, innerMmsProjection, 771 MMS_COLUMNS, 1, "mms", 772 concatSelections(selection, MMS_CONVERSATION_CONSTRAINT), selectionArgs, 773 "thread_id", "date = MAX(date)"); 774 String smsSubQuery = smsQueryBuilder.buildUnionSubQuery( 775 MmsSms.TYPE_DISCRIMINATOR_COLUMN, innerSmsProjection, 776 SMS_COLUMNS, 1, "sms", 777 concatSelections(selection, SMS_CONVERSATION_CONSTRAINT), selectionArgs, 778 "thread_id", "date = MAX(date)"); 779 SQLiteQueryBuilder unionQueryBuilder = new SQLiteQueryBuilder(); 780 781 unionQueryBuilder.setDistinct(true); 782 783 String unionQuery = unionQueryBuilder.buildUnionQuery( 784 new String[] { mmsSubQuery, smsSubQuery }, null, null); 785 786 SQLiteQueryBuilder outerQueryBuilder = new SQLiteQueryBuilder(); 787 788 outerQueryBuilder.setTables("(" + unionQuery + ")"); 789 790 String outerQuery = outerQueryBuilder.buildQuery( 791 columns, null, null, "tid", 792 "normalized_date = MAX(normalized_date)", sortOrder, null); 793 794 return mOpenHelper.getReadableDatabase().rawQuery(outerQuery, EMPTY_STRING_ARRAY); 795 } 796 797 /** 798 * Return the first locked message found in the union of MMS 799 * and SMS messages. 800 * 801 * Use this query: 802 * 803 * SELECT _id FROM pdu GROUP BY _id HAVING locked=1 UNION SELECT _id FROM sms GROUP 804 * BY _id HAVING locked=1 LIMIT 1 805 * 806 * We limit by 1 because we're only interested in knowing if 807 * there is *any* locked message, not the actual messages themselves. 808 */ getFirstLockedMessage(String[] projection, String selection, String[] selectionArgs, String sortOrder)809 private Cursor getFirstLockedMessage(String[] projection, String selection, 810 String[] selectionArgs, String sortOrder) { 811 SQLiteQueryBuilder mmsQueryBuilder = new SQLiteQueryBuilder(); 812 SQLiteQueryBuilder smsQueryBuilder = new SQLiteQueryBuilder(); 813 814 mmsQueryBuilder.setTables(MmsProvider.TABLE_PDU); 815 smsQueryBuilder.setTables(SmsProvider.TABLE_SMS); 816 817 String[] idColumn = new String[] { BaseColumns._ID }; 818 819 String mmsSubQuery = mmsQueryBuilder.buildUnionSubQuery( 820 MmsSms.TYPE_DISCRIMINATOR_COLUMN, idColumn, 821 null, 1, "mms", 822 selection, selectionArgs, 823 BaseColumns._ID, "locked=1"); 824 825 String smsSubQuery = smsQueryBuilder.buildUnionSubQuery( 826 MmsSms.TYPE_DISCRIMINATOR_COLUMN, idColumn, 827 null, 1, "sms", 828 selection, selectionArgs, 829 BaseColumns._ID, "locked=1"); 830 831 SQLiteQueryBuilder unionQueryBuilder = new SQLiteQueryBuilder(); 832 833 unionQueryBuilder.setDistinct(true); 834 835 String unionQuery = unionQueryBuilder.buildUnionQuery( 836 new String[] { mmsSubQuery, smsSubQuery }, null, "1"); 837 838 Cursor cursor = mOpenHelper.getReadableDatabase().rawQuery(unionQuery, EMPTY_STRING_ARRAY); 839 840 if (DEBUG) { 841 Log.v("MmsSmsProvider", "getFirstLockedMessage query: " + unionQuery); 842 Log.v("MmsSmsProvider", "cursor count: " + cursor.getCount()); 843 } 844 return cursor; 845 } 846 847 /** 848 * Return every message in each conversation in both MMS 849 * and SMS. 850 */ getCompleteConversations(String[] projection, String selection, String[] selectionArgs, String sortOrder)851 private Cursor getCompleteConversations(String[] projection, 852 String selection, String[] selectionArgs, String sortOrder) { 853 String unionQuery = buildConversationQuery( 854 projection, selection, selectionArgs, sortOrder); 855 856 return mOpenHelper.getReadableDatabase().rawQuery(unionQuery, EMPTY_STRING_ARRAY); 857 } 858 859 /** 860 * Add normalized date and thread_id to the list of columns for an 861 * inner projection. This is necessary so that the outer query 862 * can have access to these columns even if the caller hasn't 863 * requested them in the result. 864 */ makeProjectionWithDateAndThreadId( String[] projection, int dateMultiple)865 private String[] makeProjectionWithDateAndThreadId( 866 String[] projection, int dateMultiple) { 867 int projectionSize = projection.length; 868 String[] result = new String[projectionSize + 2]; 869 870 result[0] = "thread_id AS tid"; 871 result[1] = "date * " + dateMultiple + " AS normalized_date"; 872 for (int i = 0; i < projectionSize; i++) { 873 result[i + 2] = projection[i]; 874 } 875 return result; 876 } 877 878 /** 879 * Return the union of MMS and SMS messages for this thread ID. 880 */ getConversationMessages( String threadIdString, String[] projection, String selection, String[] selectionArgs, String sortOrder)881 private Cursor getConversationMessages( 882 String threadIdString, String[] projection, String selection, 883 String[] selectionArgs, String sortOrder) { 884 try { 885 Long.parseLong(threadIdString); 886 } catch (NumberFormatException exception) { 887 Log.e(LOG_TAG, "Thread ID must be a Long."); 888 return null; 889 } 890 891 String finalSelection = concatSelections( 892 selection, "thread_id = " + threadIdString); 893 String unionQuery = buildConversationQuery( 894 projection, finalSelection, selectionArgs, sortOrder); 895 896 return mOpenHelper.getReadableDatabase().rawQuery(unionQuery, EMPTY_STRING_ARRAY); 897 } 898 899 /** 900 * Return the union of MMS and SMS messages whose recipients 901 * included this phone number. 902 * 903 * Use this query: 904 * 905 * SELECT ... 906 * FROM pdu, (SELECT _id AS address_id 907 * FROM addr 908 * WHERE (address='<phoneNumber>' OR 909 * PHONE_NUMBERS_EQUAL(addr.address, '<phoneNumber>', 1/0))) 910 * AS matching_addresses 911 * WHERE pdu._id = matching_addresses.address_id 912 * UNION 913 * SELECT ... 914 * FROM sms 915 * WHERE (address='<phoneNumber>' OR PHONE_NUMBERS_EQUAL(sms.address, '<phoneNumber>', 1/0)); 916 */ getMessagesByPhoneNumber( String phoneNumber, String[] projection, String selection, String[] selectionArgs, String sortOrder)917 private Cursor getMessagesByPhoneNumber( 918 String phoneNumber, String[] projection, String selection, 919 String[] selectionArgs, String sortOrder) { 920 String escapedPhoneNumber = DatabaseUtils.sqlEscapeString(phoneNumber); 921 String finalMmsSelection = 922 concatSelections( 923 selection, 924 "pdu._id = matching_addresses.address_id"); 925 String finalSmsSelection = 926 concatSelections( 927 selection, 928 "(address=" + escapedPhoneNumber + " OR PHONE_NUMBERS_EQUAL(address, " + 929 escapedPhoneNumber + 930 (mUseStrictPhoneNumberComparation ? ", 1))" : ", 0))")); 931 SQLiteQueryBuilder mmsQueryBuilder = new SQLiteQueryBuilder(); 932 SQLiteQueryBuilder smsQueryBuilder = new SQLiteQueryBuilder(); 933 934 mmsQueryBuilder.setDistinct(true); 935 smsQueryBuilder.setDistinct(true); 936 mmsQueryBuilder.setTables( 937 MmsProvider.TABLE_PDU + 938 ", (SELECT _id AS address_id " + 939 "FROM addr WHERE (address=" + escapedPhoneNumber + 940 " OR PHONE_NUMBERS_EQUAL(addr.address, " + 941 escapedPhoneNumber + 942 (mUseStrictPhoneNumberComparation ? ", 1))) " : ", 0))) ") + 943 "AS matching_addresses"); 944 smsQueryBuilder.setTables(SmsProvider.TABLE_SMS); 945 946 String[] columns = handleNullMessageProjection(projection); 947 String mmsSubQuery = mmsQueryBuilder.buildUnionSubQuery( 948 MmsSms.TYPE_DISCRIMINATOR_COLUMN, columns, MMS_COLUMNS, 949 0, "mms", finalMmsSelection, selectionArgs, null, null); 950 String smsSubQuery = smsQueryBuilder.buildUnionSubQuery( 951 MmsSms.TYPE_DISCRIMINATOR_COLUMN, columns, SMS_COLUMNS, 952 0, "sms", finalSmsSelection, selectionArgs, null, null); 953 SQLiteQueryBuilder unionQueryBuilder = new SQLiteQueryBuilder(); 954 955 unionQueryBuilder.setDistinct(true); 956 957 String unionQuery = unionQueryBuilder.buildUnionQuery( 958 new String[] { mmsSubQuery, smsSubQuery }, sortOrder, null); 959 960 return mOpenHelper.getReadableDatabase().rawQuery(unionQuery, EMPTY_STRING_ARRAY); 961 } 962 963 /** 964 * Return the conversation of certain thread ID. 965 */ getConversationById( String threadIdString, String[] projection, String selection, String[] selectionArgs, String sortOrder)966 private Cursor getConversationById( 967 String threadIdString, String[] projection, String selection, 968 String[] selectionArgs, String sortOrder) { 969 try { 970 Long.parseLong(threadIdString); 971 } catch (NumberFormatException exception) { 972 Log.e(LOG_TAG, "Thread ID must be a Long."); 973 return null; 974 } 975 976 String extraSelection = "_id=" + threadIdString; 977 String finalSelection = concatSelections(selection, extraSelection); 978 SQLiteQueryBuilder queryBuilder = new SQLiteQueryBuilder(); 979 String[] columns = handleNullThreadsProjection(projection); 980 981 queryBuilder.setDistinct(true); 982 queryBuilder.setTables("threads"); 983 return queryBuilder.query( 984 mOpenHelper.getReadableDatabase(), columns, finalSelection, 985 selectionArgs, sortOrder, null, null); 986 } 987 joinPduAndPendingMsgTables()988 private static String joinPduAndPendingMsgTables() { 989 return MmsProvider.TABLE_PDU + " LEFT JOIN " + TABLE_PENDING_MSG 990 + " ON pdu._id = pending_msgs.msg_id"; 991 } 992 createMmsProjection(String[] old)993 private static String[] createMmsProjection(String[] old) { 994 String[] newProjection = new String[old.length]; 995 for (int i = 0; i < old.length; i++) { 996 if (old[i].equals(BaseColumns._ID)) { 997 newProjection[i] = "pdu._id"; 998 } else { 999 newProjection[i] = old[i]; 1000 } 1001 } 1002 return newProjection; 1003 } 1004 getUndeliveredMessages( String[] projection, String selection, String[] selectionArgs, String sortOrder)1005 private Cursor getUndeliveredMessages( 1006 String[] projection, String selection, String[] selectionArgs, 1007 String sortOrder) { 1008 String[] mmsProjection = createMmsProjection(projection); 1009 1010 SQLiteQueryBuilder mmsQueryBuilder = new SQLiteQueryBuilder(); 1011 SQLiteQueryBuilder smsQueryBuilder = new SQLiteQueryBuilder(); 1012 1013 mmsQueryBuilder.setTables(joinPduAndPendingMsgTables()); 1014 smsQueryBuilder.setTables(SmsProvider.TABLE_SMS); 1015 1016 String finalMmsSelection = concatSelections( 1017 selection, Mms.MESSAGE_BOX + " = " + Mms.MESSAGE_BOX_OUTBOX); 1018 String finalSmsSelection = concatSelections( 1019 selection, "(" + Sms.TYPE + " = " + Sms.MESSAGE_TYPE_OUTBOX 1020 + " OR " + Sms.TYPE + " = " + Sms.MESSAGE_TYPE_FAILED 1021 + " OR " + Sms.TYPE + " = " + Sms.MESSAGE_TYPE_QUEUED + ")"); 1022 1023 String[] smsColumns = handleNullMessageProjection(projection); 1024 String[] mmsColumns = handleNullMessageProjection(mmsProjection); 1025 String[] innerMmsProjection = makeProjectionWithDateAndThreadId( 1026 mmsColumns, 1000); 1027 String[] innerSmsProjection = makeProjectionWithDateAndThreadId( 1028 smsColumns, 1); 1029 1030 Set<String> columnsPresentInTable = new HashSet<String>(MMS_COLUMNS); 1031 columnsPresentInTable.add("pdu._id"); 1032 columnsPresentInTable.add(PendingMessages.ERROR_TYPE); 1033 String mmsSubQuery = mmsQueryBuilder.buildUnionSubQuery( 1034 MmsSms.TYPE_DISCRIMINATOR_COLUMN, innerMmsProjection, 1035 columnsPresentInTable, 1, "mms", finalMmsSelection, selectionArgs, 1036 null, null); 1037 String smsSubQuery = smsQueryBuilder.buildUnionSubQuery( 1038 MmsSms.TYPE_DISCRIMINATOR_COLUMN, innerSmsProjection, 1039 SMS_COLUMNS, 1, "sms", finalSmsSelection, selectionArgs, 1040 null, null); 1041 SQLiteQueryBuilder unionQueryBuilder = new SQLiteQueryBuilder(); 1042 1043 unionQueryBuilder.setDistinct(true); 1044 1045 String unionQuery = unionQueryBuilder.buildUnionQuery( 1046 new String[] { smsSubQuery, mmsSubQuery }, null, null); 1047 1048 SQLiteQueryBuilder outerQueryBuilder = new SQLiteQueryBuilder(); 1049 1050 outerQueryBuilder.setTables("(" + unionQuery + ")"); 1051 1052 String outerQuery = outerQueryBuilder.buildQuery( 1053 smsColumns, null, null, null, null, sortOrder, null); 1054 1055 return mOpenHelper.getReadableDatabase().rawQuery(outerQuery, EMPTY_STRING_ARRAY); 1056 } 1057 1058 /** 1059 * Add normalized date to the list of columns for an inner 1060 * projection. 1061 */ makeProjectionWithNormalizedDate( String[] projection, int dateMultiple)1062 private static String[] makeProjectionWithNormalizedDate( 1063 String[] projection, int dateMultiple) { 1064 int projectionSize = projection.length; 1065 String[] result = new String[projectionSize + 1]; 1066 1067 result[0] = "date * " + dateMultiple + " AS normalized_date"; 1068 System.arraycopy(projection, 0, result, 1, projectionSize); 1069 return result; 1070 } 1071 buildConversationQuery(String[] projection, String selection, String[] selectionArgs, String sortOrder)1072 private static String buildConversationQuery(String[] projection, 1073 String selection, String[] selectionArgs, String sortOrder) { 1074 String[] mmsProjection = createMmsProjection(projection); 1075 1076 SQLiteQueryBuilder mmsQueryBuilder = new SQLiteQueryBuilder(); 1077 SQLiteQueryBuilder smsQueryBuilder = new SQLiteQueryBuilder(); 1078 1079 mmsQueryBuilder.setDistinct(true); 1080 smsQueryBuilder.setDistinct(true); 1081 mmsQueryBuilder.setTables(joinPduAndPendingMsgTables()); 1082 smsQueryBuilder.setTables(SmsProvider.TABLE_SMS); 1083 1084 String[] smsColumns = handleNullMessageProjection(projection); 1085 String[] mmsColumns = handleNullMessageProjection(mmsProjection); 1086 String[] innerMmsProjection = makeProjectionWithNormalizedDate(mmsColumns, 1000); 1087 String[] innerSmsProjection = makeProjectionWithNormalizedDate(smsColumns, 1); 1088 1089 Set<String> columnsPresentInTable = new HashSet<String>(MMS_COLUMNS); 1090 columnsPresentInTable.add("pdu._id"); 1091 columnsPresentInTable.add(PendingMessages.ERROR_TYPE); 1092 1093 String mmsSelection = concatSelections(selection, 1094 Mms.MESSAGE_BOX + " != " + Mms.MESSAGE_BOX_DRAFTS); 1095 String mmsSubQuery = mmsQueryBuilder.buildUnionSubQuery( 1096 MmsSms.TYPE_DISCRIMINATOR_COLUMN, innerMmsProjection, 1097 columnsPresentInTable, 0, "mms", 1098 concatSelections(mmsSelection, MMS_CONVERSATION_CONSTRAINT), 1099 selectionArgs, null, null); 1100 String smsSubQuery = smsQueryBuilder.buildUnionSubQuery( 1101 MmsSms.TYPE_DISCRIMINATOR_COLUMN, innerSmsProjection, SMS_COLUMNS, 1102 0, "sms", concatSelections(selection, SMS_CONVERSATION_CONSTRAINT), 1103 selectionArgs, null, null); 1104 SQLiteQueryBuilder unionQueryBuilder = new SQLiteQueryBuilder(); 1105 1106 unionQueryBuilder.setDistinct(true); 1107 1108 String unionQuery = unionQueryBuilder.buildUnionQuery( 1109 new String[] { smsSubQuery, mmsSubQuery }, 1110 handleNullSortOrder(sortOrder), null); 1111 1112 SQLiteQueryBuilder outerQueryBuilder = new SQLiteQueryBuilder(); 1113 1114 outerQueryBuilder.setTables("(" + unionQuery + ")"); 1115 1116 return outerQueryBuilder.buildQuery( 1117 smsColumns, null, null, null, null, sortOrder, null); 1118 } 1119 1120 @Override getType(Uri uri)1121 public String getType(Uri uri) { 1122 return VND_ANDROID_DIR_MMS_SMS; 1123 } 1124 1125 @Override delete(Uri uri, String selection, String[] selectionArgs)1126 public int delete(Uri uri, String selection, 1127 String[] selectionArgs) { 1128 SQLiteDatabase db = mOpenHelper.getWritableDatabase(); 1129 Context context = getContext(); 1130 int affectedRows = 0; 1131 1132 switch(URI_MATCHER.match(uri)) { 1133 case URI_CONVERSATIONS_MESSAGES: 1134 long threadId; 1135 try { 1136 threadId = Long.parseLong(uri.getLastPathSegment()); 1137 } catch (NumberFormatException e) { 1138 Log.e(LOG_TAG, "Thread ID must be a long."); 1139 break; 1140 } 1141 affectedRows = deleteConversation(uri, selection, selectionArgs); 1142 MmsSmsDatabaseHelper.updateThread(db, threadId); 1143 break; 1144 case URI_CONVERSATIONS: 1145 affectedRows = MmsProvider.deleteMessages(context, db, 1146 selection, selectionArgs, uri) 1147 + db.delete("sms", selection, selectionArgs); 1148 // Intentionally don't pass the selection variable to updateAllThreads. 1149 // When we pass in "locked=0" there, the thread will get excluded from 1150 // the selection and not get updated. 1151 MmsSmsDatabaseHelper.updateAllThreads(db, null, null); 1152 break; 1153 case URI_OBSOLETE_THREADS: 1154 affectedRows = db.delete("threads", 1155 "_id NOT IN (SELECT DISTINCT thread_id FROM sms " + 1156 "UNION SELECT DISTINCT thread_id FROM pdu)", null); 1157 break; 1158 default: 1159 throw new UnsupportedOperationException(NO_DELETES_INSERTS_OR_UPDATES); 1160 } 1161 1162 if (affectedRows > 0) { 1163 context.getContentResolver().notifyChange(MmsSms.CONTENT_URI, null); 1164 } 1165 return affectedRows; 1166 } 1167 1168 /** 1169 * Delete the conversation with the given thread ID. 1170 */ deleteConversation(Uri uri, String selection, String[] selectionArgs)1171 private int deleteConversation(Uri uri, String selection, String[] selectionArgs) { 1172 String threadId = uri.getLastPathSegment(); 1173 1174 SQLiteDatabase db = mOpenHelper.getWritableDatabase(); 1175 String finalSelection = concatSelections(selection, "thread_id = " + threadId); 1176 return MmsProvider.deleteMessages(getContext(), db, finalSelection, 1177 selectionArgs, uri) 1178 + db.delete("sms", finalSelection, selectionArgs); 1179 } 1180 1181 @Override insert(Uri uri, ContentValues values)1182 public Uri insert(Uri uri, ContentValues values) { 1183 throw new UnsupportedOperationException(NO_DELETES_INSERTS_OR_UPDATES); 1184 } 1185 1186 @Override update(Uri uri, ContentValues values, String selection, String[] selectionArgs)1187 public int update(Uri uri, ContentValues values, 1188 String selection, String[] selectionArgs) { 1189 SQLiteDatabase db = mOpenHelper.getWritableDatabase(); 1190 int affectedRows = 0; 1191 switch(URI_MATCHER.match(uri)) { 1192 case URI_CONVERSATIONS_MESSAGES: 1193 String threadIdString = uri.getPathSegments().get(1); 1194 affectedRows = updateConversation(threadIdString, values, 1195 selection, selectionArgs); 1196 break; 1197 1198 case URI_PENDING_MSG: 1199 affectedRows = db.update(TABLE_PENDING_MSG, values, selection, null); 1200 break; 1201 1202 case URI_CANONICAL_ADDRESS: { 1203 String extraSelection = "_id=" + uri.getPathSegments().get(1); 1204 String finalSelection = TextUtils.isEmpty(selection) 1205 ? extraSelection : extraSelection + " AND " + selection; 1206 1207 affectedRows = db.update(TABLE_CANONICAL_ADDRESSES, values, finalSelection, null); 1208 break; 1209 } 1210 1211 default: 1212 throw new UnsupportedOperationException( 1213 NO_DELETES_INSERTS_OR_UPDATES); 1214 } 1215 1216 if (affectedRows > 0) { 1217 getContext().getContentResolver().notifyChange( 1218 MmsSms.CONTENT_URI, null); 1219 } 1220 return affectedRows; 1221 } 1222 updateConversation( String threadIdString, ContentValues values, String selection, String[] selectionArgs)1223 private int updateConversation( 1224 String threadIdString, ContentValues values, String selection, 1225 String[] selectionArgs) { 1226 try { 1227 Long.parseLong(threadIdString); 1228 } catch (NumberFormatException exception) { 1229 Log.e(LOG_TAG, "Thread ID must be a Long."); 1230 return 0; 1231 } 1232 1233 SQLiteDatabase db = mOpenHelper.getWritableDatabase(); 1234 String finalSelection = concatSelections(selection, "thread_id=" + threadIdString); 1235 return db.update(MmsProvider.TABLE_PDU, values, finalSelection, selectionArgs) 1236 + db.update("sms", values, finalSelection, selectionArgs); 1237 } 1238 1239 /** 1240 * Construct Sets of Strings containing exactly the columns 1241 * present in each table. We will use this when constructing 1242 * UNION queries across the MMS and SMS tables. 1243 */ initializeColumnSets()1244 private static void initializeColumnSets() { 1245 int commonColumnCount = MMS_SMS_COLUMNS.length; 1246 int mmsOnlyColumnCount = MMS_ONLY_COLUMNS.length; 1247 int smsOnlyColumnCount = SMS_ONLY_COLUMNS.length; 1248 Set<String> unionColumns = new HashSet<String>(); 1249 1250 for (int i = 0; i < commonColumnCount; i++) { 1251 MMS_COLUMNS.add(MMS_SMS_COLUMNS[i]); 1252 SMS_COLUMNS.add(MMS_SMS_COLUMNS[i]); 1253 unionColumns.add(MMS_SMS_COLUMNS[i]); 1254 } 1255 for (int i = 0; i < mmsOnlyColumnCount; i++) { 1256 MMS_COLUMNS.add(MMS_ONLY_COLUMNS[i]); 1257 unionColumns.add(MMS_ONLY_COLUMNS[i]); 1258 } 1259 for (int i = 0; i < smsOnlyColumnCount; i++) { 1260 SMS_COLUMNS.add(SMS_ONLY_COLUMNS[i]); 1261 unionColumns.add(SMS_ONLY_COLUMNS[i]); 1262 } 1263 1264 int i = 0; 1265 for (String columnName : unionColumns) { 1266 UNION_COLUMNS[i++] = columnName; 1267 } 1268 } 1269 } 1270