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