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