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