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