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