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