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