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