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