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
17package com.android.providers.telephony;
18
19import java.util.Arrays;
20import java.util.HashSet;
21import java.util.List;
22import java.util.Set;
23
24import android.app.AppOpsManager;
25import android.content.ContentProvider;
26import android.content.ContentValues;
27import android.content.Context;
28import android.content.UriMatcher;
29import android.database.Cursor;
30import android.database.DatabaseUtils;
31import android.database.sqlite.SQLiteDatabase;
32import android.database.sqlite.SQLiteOpenHelper;
33import android.database.sqlite.SQLiteQueryBuilder;
34import android.net.Uri;
35import android.os.UserHandle;
36import android.provider.BaseColumns;
37import android.provider.Telephony;
38import android.provider.Telephony.CanonicalAddressesColumns;
39import android.provider.Telephony.Mms;
40import android.provider.Telephony.MmsSms;
41import android.provider.Telephony.Sms;
42import android.provider.Telephony.Threads;
43import android.provider.Telephony.ThreadsColumns;
44import android.provider.Telephony.MmsSms.PendingMessages;
45import android.provider.Telephony.Sms.Conversations;
46import android.text.TextUtils;
47import android.util.Log;
48
49import com.google.android.mms.pdu.PduHeaders;
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 */
72public 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.SUB_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        SQLiteDatabase db = mOpenHelper.getWritableDatabase();
1244        int affectedRows = 0;
1245        switch(URI_MATCHER.match(uri)) {
1246            case URI_CONVERSATIONS_MESSAGES:
1247                String threadIdString = uri.getPathSegments().get(1);
1248                affectedRows = updateConversation(threadIdString, values,
1249                        selection, selectionArgs);
1250                break;
1251
1252            case URI_PENDING_MSG:
1253                affectedRows = db.update(TABLE_PENDING_MSG, values, selection, null);
1254                break;
1255
1256            case URI_CANONICAL_ADDRESS: {
1257                String extraSelection = "_id=" + uri.getPathSegments().get(1);
1258                String finalSelection = TextUtils.isEmpty(selection)
1259                        ? extraSelection : extraSelection + " AND " + selection;
1260
1261                affectedRows = db.update(TABLE_CANONICAL_ADDRESSES, values, finalSelection, null);
1262                break;
1263            }
1264
1265            case URI_CONVERSATIONS: {
1266                final ContentValues finalValues = new ContentValues(1);
1267                if (values.containsKey(Threads.ARCHIVED)) {
1268                    // Only allow update archived
1269                    finalValues.put(Threads.ARCHIVED, values.getAsBoolean(Threads.ARCHIVED));
1270                }
1271                affectedRows = db.update(TABLE_THREADS, finalValues, selection, selectionArgs);
1272                break;
1273            }
1274
1275            default:
1276                throw new UnsupportedOperationException(
1277                        NO_DELETES_INSERTS_OR_UPDATES + uri);
1278        }
1279
1280        if (affectedRows > 0) {
1281            getContext().getContentResolver().notifyChange(
1282                    MmsSms.CONTENT_URI, null, true, UserHandle.USER_ALL);
1283        }
1284        return affectedRows;
1285    }
1286
1287    private int updateConversation(
1288            String threadIdString, ContentValues values, String selection,
1289            String[] selectionArgs) {
1290        try {
1291            Long.parseLong(threadIdString);
1292        } catch (NumberFormatException exception) {
1293            Log.e(LOG_TAG, "Thread ID must be a Long.");
1294            return 0;
1295        }
1296
1297        SQLiteDatabase db = mOpenHelper.getWritableDatabase();
1298        String finalSelection = concatSelections(selection, "thread_id=" + threadIdString);
1299        return db.update(MmsProvider.TABLE_PDU, values, finalSelection, selectionArgs)
1300                + db.update("sms", values, finalSelection, selectionArgs);
1301    }
1302
1303    /**
1304     * Construct Sets of Strings containing exactly the columns
1305     * present in each table.  We will use this when constructing
1306     * UNION queries across the MMS and SMS tables.
1307     */
1308    private static void initializeColumnSets() {
1309        int commonColumnCount = MMS_SMS_COLUMNS.length;
1310        int mmsOnlyColumnCount = MMS_ONLY_COLUMNS.length;
1311        int smsOnlyColumnCount = SMS_ONLY_COLUMNS.length;
1312        Set<String> unionColumns = new HashSet<String>();
1313
1314        for (int i = 0; i < commonColumnCount; i++) {
1315            MMS_COLUMNS.add(MMS_SMS_COLUMNS[i]);
1316            SMS_COLUMNS.add(MMS_SMS_COLUMNS[i]);
1317            unionColumns.add(MMS_SMS_COLUMNS[i]);
1318        }
1319        for (int i = 0; i < mmsOnlyColumnCount; i++) {
1320            MMS_COLUMNS.add(MMS_ONLY_COLUMNS[i]);
1321            unionColumns.add(MMS_ONLY_COLUMNS[i]);
1322        }
1323        for (int i = 0; i < smsOnlyColumnCount; i++) {
1324            SMS_COLUMNS.add(SMS_ONLY_COLUMNS[i]);
1325            unionColumns.add(SMS_ONLY_COLUMNS[i]);
1326        }
1327
1328        int i = 0;
1329        for (String columnName : unionColumns) {
1330            UNION_COLUMNS[i++] = columnName;
1331        }
1332    }
1333}
1334