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