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