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