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