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