Home | History | Annotate | Download | only in telephony
      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