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, R, URI_CONVERSATIONS)242         URI_MATCHER.addURI(AUTHORITY, "conversations", URI_CONVERSATIONS);
URI_MATCHER.addURI(AUTHORITY, R, 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, R, URI_CONVERSATIONS_MESSAGES)246         URI_MATCHER.addURI(
247                 AUTHORITY, "conversations/#", URI_CONVERSATIONS_MESSAGES);
URI_MATCHER.addURI( AUTHORITY, R, URI_CONVERSATIONS_RECIPIENTS)248         URI_MATCHER.addURI(
249                 AUTHORITY, "conversations/#/recipients",
250                 URI_CONVERSATIONS_RECIPIENTS);
251 
URI_MATCHER.addURI( AUTHORITY, R, 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, R, URI_OBSOLETE_THREADS)257         URI_MATCHER.addURI(AUTHORITY, "conversations/obsolete", URI_OBSOLETE_THREADS);
258 
URI_MATCHER.addURI( AUTHORITY, R, 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, R, 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, R, 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, R, URI_CANONICAL_ADDRESSES)272         URI_MATCHER.addURI(AUTHORITY, "canonical-addresses", URI_CANONICAL_ADDRESSES);
273 
URI_MATCHER.addURI(AUTHORITY, R, URI_SEARCH)274         URI_MATCHER.addURI(AUTHORITY, "search", URI_SEARCH);
URI_MATCHER.addURI(AUTHORITY, R, 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, R, 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, R, 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, R, URI_NOTIFICATIONS)293         URI_MATCHER.addURI(AUTHORITY, "notifications", URI_NOTIFICATIONS);
294 
URI_MATCHER.addURI(AUTHORITY, R, URI_DRAFT)295         URI_MATCHER.addURI(AUTHORITY, "draft", URI_DRAFT);
296 
URI_MATCHER.addURI(AUTHORITY, R, URI_FIRST_LOCKED_MESSAGE_ALL)297         URI_MATCHER.addURI(AUTHORITY, "locked", URI_FIRST_LOCKED_MESSAGE_ALL);
298 
URI_MATCHER.addURI(AUTHORITY, R, 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, R, 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         // If access is restricted, we don't allow subqueries in the query.
336         if (accessRestricted) {
337             try {
338                 SqlQueryChecker.checkQueryParametersForSubqueries(projection, selection, sortOrder);
339             } catch (IllegalArgumentException e) {
340                 Log.w(LOG_TAG, "Query rejected: " + e.getMessage());
341                 return null;
342             }
343         }
344 
345         SQLiteDatabase db = mOpenHelper.getReadableDatabase();
346         Cursor cursor = null;
347         final int match = URI_MATCHER.match(uri);
348         switch (match) {
349             case URI_COMPLETE_CONVERSATIONS:
350                 cursor = getCompleteConversations(projection, selection, sortOrder, smsTable,
351                         pduTable);
352                 break;
353             case URI_CONVERSATIONS:
354                 String simple = uri.getQueryParameter("simple");
355                 if ((simple != null) && simple.equals("true")) {
356                     String threadType = uri.getQueryParameter("thread_type");
357                     if (!TextUtils.isEmpty(threadType)) {
358                         selection = concatSelections(
359                                 selection, Threads.TYPE + "=" + threadType);
360                     }
361                     cursor = getSimpleConversations(
362                             projection, selection, selectionArgs, sortOrder);
363                 } else {
364                     cursor = getConversations(
365                             projection, selection, sortOrder, smsTable, pduTable);
366                 }
367                 break;
368             case URI_CONVERSATIONS_MESSAGES:
369                 cursor = getConversationMessages(uri.getPathSegments().get(1), projection,
370                         selection, sortOrder, smsTable, pduTable);
371                 break;
372             case URI_CONVERSATIONS_RECIPIENTS:
373                 cursor = getConversationById(
374                         uri.getPathSegments().get(1), projection, selection,
375                         selectionArgs, sortOrder);
376                 break;
377             case URI_CONVERSATIONS_SUBJECT:
378                 cursor = getConversationById(
379                         uri.getPathSegments().get(1), projection, selection,
380                         selectionArgs, sortOrder);
381                 break;
382             case URI_MESSAGES_BY_PHONE:
383                 cursor = getMessagesByPhoneNumber(
384                         uri.getPathSegments().get(2), projection, selection, sortOrder, smsTable,
385                         pduTable);
386                 break;
387             case URI_THREAD_ID:
388                 List<String> recipients = uri.getQueryParameters("recipient");
389 
390                 cursor = getThreadId(recipients);
391                 break;
392             case URI_CANONICAL_ADDRESS: {
393                 String extraSelection = "_id=" + uri.getPathSegments().get(1);
394                 String finalSelection = TextUtils.isEmpty(selection)
395                         ? extraSelection : extraSelection + " AND " + selection;
396                 cursor = db.query(TABLE_CANONICAL_ADDRESSES,
397                         CANONICAL_ADDRESSES_COLUMNS_1,
398                         finalSelection,
399                         selectionArgs,
400                         null, null,
401                         sortOrder);
402                 break;
403             }
404             case URI_CANONICAL_ADDRESSES:
405                 cursor = db.query(TABLE_CANONICAL_ADDRESSES,
406                         CANONICAL_ADDRESSES_COLUMNS_2,
407                         selection,
408                         selectionArgs,
409                         null, null,
410                         sortOrder);
411                 break;
412             case URI_SEARCH_SUGGEST: {
413                 SEARCH_STRING[0] = uri.getQueryParameter("pattern") + '*' ;
414 
415                 // find the words which match the pattern using the snippet function.  The
416                 // snippet function parameters mainly describe how to format the result.
417                 // See http://www.sqlite.org/fts3.html#section_4_2 for details.
418                 if (       sortOrder != null
419                         || selection != null
420                         || selectionArgs != null
421                         || projection != null) {
422                     throw new IllegalArgumentException(
423                             "do not specify sortOrder, selection, selectionArgs, or projection" +
424                             "with this query");
425                 }
426 
427                 cursor = db.rawQuery(SEARCH_QUERY, SEARCH_STRING);
428                 break;
429             }
430             case URI_MESSAGE_ID_TO_THREAD: {
431                 // Given a message ID and an indicator for SMS vs. MMS return
432                 // the thread id of the corresponding thread.
433                 try {
434                     long id = Long.parseLong(uri.getQueryParameter("row_id"));
435                     switch (Integer.parseInt(uri.getQueryParameter("table_to_use"))) {
436                         case 1:  // sms
437                             cursor = db.query(
438                                 smsTable,
439                                 new String[] { "thread_id" },
440                                 "_id=?",
441                                 new String[] { String.valueOf(id) },
442                                 null,
443                                 null,
444                                 null);
445                             break;
446                         case 2:  // mms
447                             String mmsQuery = "SELECT thread_id "
448                                     + "FROM " + pduTable + ",part "
449                                     + "WHERE ((part.mid=" + pduTable + "._id) "
450                                     + "AND " + "(part._id=?))";
451                             cursor = db.rawQuery(mmsQuery, new String[] { String.valueOf(id) });
452                             break;
453                     }
454                 } catch (NumberFormatException ex) {
455                     // ignore... return empty cursor
456                 }
457                 break;
458             }
459             case URI_SEARCH: {
460                 if (       sortOrder != null
461                         || selection != null
462                         || selectionArgs != null
463                         || projection != null) {
464                     throw new IllegalArgumentException(
465                             "do not specify sortOrder, selection, selectionArgs, or projection" +
466                             "with this query");
467                 }
468 
469                 String searchString = uri.getQueryParameter("pattern") + "*";
470 
471                 try {
472                     cursor = db.rawQuery(getTextSearchQuery(smsTable, pduTable),
473                             new String[] { searchString, searchString });
474                 } catch (Exception ex) {
475                     Log.e(LOG_TAG, "got exception: " + ex.toString());
476                 }
477                 break;
478             }
479             case URI_PENDING_MSG: {
480                 String protoName = uri.getQueryParameter("protocol");
481                 String msgId = uri.getQueryParameter("message");
482                 int proto = TextUtils.isEmpty(protoName) ? -1
483                         : (protoName.equals("sms") ? MmsSms.SMS_PROTO : MmsSms.MMS_PROTO);
484 
485                 String extraSelection = (proto != -1) ?
486                         (PendingMessages.PROTO_TYPE + "=" + proto) : " 0=0 ";
487                 if (!TextUtils.isEmpty(msgId)) {
488                     extraSelection += " AND " + PendingMessages.MSG_ID + "=" + msgId;
489                 }
490 
491                 String finalSelection = TextUtils.isEmpty(selection)
492                         ? extraSelection : ("(" + extraSelection + ") AND " + selection);
493                 String finalOrder = TextUtils.isEmpty(sortOrder)
494                         ? PendingMessages.DUE_TIME : sortOrder;
495                 cursor = db.query(TABLE_PENDING_MSG, null,
496                         finalSelection, selectionArgs, null, null, finalOrder);
497                 break;
498             }
499             case URI_UNDELIVERED_MSG: {
500                 cursor = getUndeliveredMessages(projection, selection,
501                         selectionArgs, sortOrder, smsTable, pduTable);
502                 break;
503             }
504             case URI_DRAFT: {
505                 cursor = getDraftThread(projection, selection, sortOrder, smsTable, pduTable);
506                 break;
507             }
508             case URI_FIRST_LOCKED_MESSAGE_BY_THREAD_ID: {
509                 long threadId;
510                 try {
511                     threadId = Long.parseLong(uri.getLastPathSegment());
512                 } catch (NumberFormatException e) {
513                     Log.e(LOG_TAG, "Thread ID must be a long.");
514                     break;
515                 }
516                 cursor = getFirstLockedMessage(projection, "thread_id=" + Long.toString(threadId),
517                         sortOrder, smsTable, pduTable);
518                 break;
519             }
520             case URI_FIRST_LOCKED_MESSAGE_ALL: {
521                 cursor = getFirstLockedMessage(
522                         projection, selection, sortOrder, smsTable, pduTable);
523                 break;
524             }
525             default:
526                 throw new IllegalStateException("Unrecognized URI:" + uri);
527         }
528 
529         if (cursor != null) {
530             cursor.setNotificationUri(getContext().getContentResolver(), MmsSms.CONTENT_URI);
531         }
532         return cursor;
533     }
534 
535     /**
536      * Return the canonical address ID for this address.
537      */
getSingleAddressId(String address)538     private long getSingleAddressId(String address) {
539         boolean isEmail = Mms.isEmailAddress(address);
540         boolean isPhoneNumber = Mms.isPhoneNumber(address);
541 
542         // We lowercase all email addresses, but not addresses that aren't numbers, because
543         // that would incorrectly turn an address such as "My Vodafone" into "my vodafone"
544         // and the thread title would be incorrect when displayed in the UI.
545         String refinedAddress = isEmail ? address.toLowerCase() : address;
546 
547         String selection = "address=?";
548         String[] selectionArgs;
549         long retVal = -1L;
550         int minMatch =
551             getContext().getResources().getInteger(
552                     com.android.internal.R.integer.config_phonenumber_compare_min_match);
553 
554         if (!isPhoneNumber) {
555             selectionArgs = new String[] { refinedAddress };
556         } else {
557             selection += " OR PHONE_NUMBERS_EQUAL(address, ?, " +
558                         (mUseStrictPhoneNumberComparation ? "1)" : "0, " + minMatch + ")");
559             selectionArgs = new String[] { refinedAddress, refinedAddress };
560         }
561 
562         Cursor cursor = null;
563 
564         try {
565             SQLiteDatabase db = mOpenHelper.getReadableDatabase();
566             cursor = db.query(
567                     "canonical_addresses", ID_PROJECTION,
568                     selection, selectionArgs, null, null, null);
569 
570             if (cursor.getCount() == 0) {
571                 ContentValues contentValues = new ContentValues(1);
572                 contentValues.put(CanonicalAddressesColumns.ADDRESS, refinedAddress);
573 
574                 db = mOpenHelper.getWritableDatabase();
575                 retVal = db.insert("canonical_addresses",
576                         CanonicalAddressesColumns.ADDRESS, contentValues);
577 
578                 Log.d(LOG_TAG, "getSingleAddressId: insert new canonical_address for " +
579                         /*address*/ "xxxxxx" + ", _id=" + retVal);
580 
581                 return retVal;
582             }
583 
584             if (cursor.moveToFirst()) {
585                 retVal = cursor.getLong(cursor.getColumnIndexOrThrow(BaseColumns._ID));
586             }
587         } finally {
588             if (cursor != null) {
589                 cursor.close();
590             }
591         }
592 
593         return retVal;
594     }
595 
596     /**
597      * Return the canonical address IDs for these addresses.
598      */
getAddressIds(List<String> addresses)599     private Set<Long> getAddressIds(List<String> addresses) {
600         Set<Long> result = new HashSet<Long>(addresses.size());
601 
602         for (String address : addresses) {
603             if (!address.equals(PduHeaders.FROM_INSERT_ADDRESS_TOKEN_STR)) {
604                 long id = getSingleAddressId(address);
605                 if (id != -1L) {
606                     result.add(id);
607                 } else {
608                     Log.e(LOG_TAG, "getAddressIds: address ID not found for " + address);
609                 }
610             }
611         }
612         return result;
613     }
614 
615     /**
616      * Return a sorted array of the given Set of Longs.
617      */
getSortedSet(Set<Long> numbers)618     private long[] getSortedSet(Set<Long> numbers) {
619         int size = numbers.size();
620         long[] result = new long[size];
621         int i = 0;
622 
623         for (Long number : numbers) {
624             result[i++] = number;
625         }
626 
627         if (size > 1) {
628             Arrays.sort(result);
629         }
630 
631         return result;
632     }
633 
634     /**
635      * Return a String of the numbers in the given array, in order,
636      * separated by spaces.
637      */
getSpaceSeparatedNumbers(long[] numbers)638     private String getSpaceSeparatedNumbers(long[] numbers) {
639         int size = numbers.length;
640         StringBuilder buffer = new StringBuilder();
641 
642         for (int i = 0; i < size; i++) {
643             if (i != 0) {
644                 buffer.append(' ');
645             }
646             buffer.append(numbers[i]);
647         }
648         return buffer.toString();
649     }
650 
651     /**
652      * Insert a record for a new thread.
653      */
insertThread(String recipientIds, int numberOfRecipients)654     private void insertThread(String recipientIds, int numberOfRecipients) {
655         ContentValues values = new ContentValues(4);
656 
657         long date = System.currentTimeMillis();
658         values.put(ThreadsColumns.DATE, date - date % 1000);
659         values.put(ThreadsColumns.RECIPIENT_IDS, recipientIds);
660         if (numberOfRecipients > 1) {
661             values.put(Threads.TYPE, Threads.BROADCAST_THREAD);
662         }
663         values.put(ThreadsColumns.MESSAGE_COUNT, 0);
664 
665         long result = mOpenHelper.getWritableDatabase().insert(TABLE_THREADS, null, values);
666         Log.d(LOG_TAG, "insertThread: created new thread_id " + result +
667                 " for recipientIds " + /*recipientIds*/ "xxxxxxx");
668 
669         getContext().getContentResolver().notifyChange(MmsSms.CONTENT_URI, null, true,
670                 UserHandle.USER_ALL);
671     }
672 
673     private static final String THREAD_QUERY =
674             "SELECT _id FROM threads " + "WHERE recipient_ids=?";
675 
676     /**
677      * Return the thread ID for this list of
678      * recipients IDs.  If no thread exists with this ID, create
679      * one and return it.  Callers should always use
680      * Threads.getThreadId to access this information.
681      */
getThreadId(List<String> recipients)682     private synchronized Cursor getThreadId(List<String> recipients) {
683         Set<Long> addressIds = getAddressIds(recipients);
684         String recipientIds = "";
685 
686         if (addressIds.size() == 0) {
687             Log.e(LOG_TAG, "getThreadId: NO receipients specified -- NOT creating thread",
688                     new Exception());
689             return null;
690         } else if (addressIds.size() == 1) {
691             // optimize for size==1, which should be most of the cases
692             for (Long addressId : addressIds) {
693                 recipientIds = Long.toString(addressId);
694             }
695         } else {
696             recipientIds = getSpaceSeparatedNumbers(getSortedSet(addressIds));
697         }
698 
699         if (Log.isLoggable(LOG_TAG, Log.VERBOSE)) {
700             Log.d(LOG_TAG, "getThreadId: recipientIds (selectionArgs) =" +
701                     /*recipientIds*/ "xxxxxxx");
702         }
703 
704         String[] selectionArgs = new String[] { recipientIds };
705 
706         SQLiteDatabase db = mOpenHelper.getReadableDatabase();
707         db.beginTransaction();
708         Cursor cursor = null;
709         try {
710             // Find the thread with the given recipients
711             cursor = db.rawQuery(THREAD_QUERY, selectionArgs);
712 
713             if (cursor.getCount() == 0) {
714                 // No thread with those recipients exists, so create the thread.
715                 cursor.close();
716 
717                 Log.d(LOG_TAG, "getThreadId: create new thread_id for recipients " +
718                         /*recipients*/ "xxxxxxxx");
719                 insertThread(recipientIds, recipients.size());
720 
721                 // The thread was just created, now find it and return it.
722                 cursor = db.rawQuery(THREAD_QUERY, selectionArgs);
723             }
724             db.setTransactionSuccessful();
725         } catch (Throwable ex) {
726             Log.e(LOG_TAG, ex.getMessage(), ex);
727         } finally {
728             db.endTransaction();
729         }
730 
731         if (cursor != null && cursor.getCount() > 1) {
732             Log.w(LOG_TAG, "getThreadId: why is cursorCount=" + cursor.getCount());
733         }
734         return cursor;
735     }
736 
concatSelections(String selection1, String selection2)737     private static String concatSelections(String selection1, String selection2) {
738         if (TextUtils.isEmpty(selection1)) {
739             return selection2;
740         } else if (TextUtils.isEmpty(selection2)) {
741             return selection1;
742         } else {
743             return selection1 + " AND " + selection2;
744         }
745     }
746 
747     /**
748      * If a null projection is given, return the union of all columns
749      * in both the MMS and SMS messages tables.  Otherwise, return the
750      * given projection.
751      */
handleNullMessageProjection( String[] projection)752     private static String[] handleNullMessageProjection(
753             String[] projection) {
754         return projection == null ? UNION_COLUMNS : projection;
755     }
756 
757     /**
758      * If a null projection is given, return the set of all columns in
759      * the threads table.  Otherwise, return the given projection.
760      */
handleNullThreadsProjection( String[] projection)761     private static String[] handleNullThreadsProjection(
762             String[] projection) {
763         return projection == null ? THREADS_COLUMNS : projection;
764     }
765 
766     /**
767      * If a null sort order is given, return "normalized_date ASC".
768      * Otherwise, return the given sort order.
769      */
handleNullSortOrder(String sortOrder)770     private static String handleNullSortOrder (String sortOrder) {
771         return sortOrder == null ? "normalized_date ASC" : sortOrder;
772     }
773 
774     /**
775      * Return existing threads in the database.
776      */
getSimpleConversations(String[] projection, String selection, String[] selectionArgs, String sortOrder)777     private Cursor getSimpleConversations(String[] projection, String selection,
778             String[] selectionArgs, String sortOrder) {
779         return mOpenHelper.getReadableDatabase().query(TABLE_THREADS, projection,
780                 selection, selectionArgs, null, null, " date DESC");
781     }
782 
783     /**
784      * Return the thread which has draft in both MMS and SMS.
785      *
786      * Use this query:
787      *
788      *   SELECT ...
789      *     FROM (SELECT _id, thread_id, ...
790      *             FROM pdu
791      *             WHERE msg_box = 3 AND ...
792      *           UNION
793      *           SELECT _id, thread_id, ...
794      *             FROM sms
795      *             WHERE type = 3 AND ...
796      *          )
797      *   ;
798      */
getDraftThread(String[] projection, String selection, String sortOrder, String smsTable, String pduTable)799     private Cursor getDraftThread(String[] projection, String selection,
800             String sortOrder, String smsTable, String pduTable) {
801         String[] innerProjection = new String[] {BaseColumns._ID, Conversations.THREAD_ID};
802         SQLiteQueryBuilder mmsQueryBuilder = new SQLiteQueryBuilder();
803         SQLiteQueryBuilder smsQueryBuilder = new SQLiteQueryBuilder();
804 
805         mmsQueryBuilder.setTables(pduTable);
806         smsQueryBuilder.setTables(smsTable);
807 
808         String mmsSubQuery = mmsQueryBuilder.buildUnionSubQuery(
809                 MmsSms.TYPE_DISCRIMINATOR_COLUMN, innerProjection,
810                 MMS_COLUMNS, 1, "mms",
811                 concatSelections(selection, Mms.MESSAGE_BOX + "=" + Mms.MESSAGE_BOX_DRAFTS),
812                 null, null);
813         String smsSubQuery = smsQueryBuilder.buildUnionSubQuery(
814                 MmsSms.TYPE_DISCRIMINATOR_COLUMN, innerProjection,
815                 SMS_COLUMNS, 1, "sms",
816                 concatSelections(selection, Sms.TYPE + "=" + Sms.MESSAGE_TYPE_DRAFT),
817                 null, null);
818         SQLiteQueryBuilder unionQueryBuilder = new SQLiteQueryBuilder();
819 
820         unionQueryBuilder.setDistinct(true);
821 
822         String unionQuery = unionQueryBuilder.buildUnionQuery(
823                 new String[] { mmsSubQuery, smsSubQuery }, null, null);
824 
825         SQLiteQueryBuilder outerQueryBuilder = new SQLiteQueryBuilder();
826 
827         outerQueryBuilder.setTables("(" + unionQuery + ")");
828 
829         String outerQuery = outerQueryBuilder.buildQuery(
830                 projection, null, null, null, sortOrder, null);
831 
832         return mOpenHelper.getReadableDatabase().rawQuery(outerQuery, EMPTY_STRING_ARRAY);
833     }
834 
835     /**
836      * Return the most recent message in each conversation in both MMS
837      * and SMS.
838      *
839      * Use this query:
840      *
841      *   SELECT ...
842      *     FROM (SELECT thread_id AS tid, date * 1000 AS normalized_date, ...
843      *             FROM pdu
844      *             WHERE msg_box != 3 AND ...
845      *             GROUP BY thread_id
846      *             HAVING date = MAX(date)
847      *           UNION
848      *           SELECT thread_id AS tid, date AS normalized_date, ...
849      *             FROM sms
850      *             WHERE ...
851      *             GROUP BY thread_id
852      *             HAVING date = MAX(date))
853      *     GROUP BY tid
854      *     HAVING normalized_date = MAX(normalized_date);
855      *
856      * The msg_box != 3 comparisons ensure that we don't include draft
857      * messages.
858      */
getConversations(String[] projection, String selection, String sortOrder, String smsTable, String pduTable)859     private Cursor getConversations(String[] projection, String selection,
860             String sortOrder, String smsTable, String pduTable) {
861         SQLiteQueryBuilder mmsQueryBuilder = new SQLiteQueryBuilder();
862         SQLiteQueryBuilder smsQueryBuilder = new SQLiteQueryBuilder();
863 
864         mmsQueryBuilder.setTables(pduTable);
865         smsQueryBuilder.setTables(smsTable);
866 
867         String[] columns = handleNullMessageProjection(projection);
868         String[] innerMmsProjection = makeProjectionWithDateAndThreadId(
869                 UNION_COLUMNS, 1000);
870         String[] innerSmsProjection = makeProjectionWithDateAndThreadId(
871                 UNION_COLUMNS, 1);
872         String mmsSubQuery = mmsQueryBuilder.buildUnionSubQuery(
873                 MmsSms.TYPE_DISCRIMINATOR_COLUMN, innerMmsProjection,
874                 MMS_COLUMNS, 1, "mms",
875                 concatSelections(selection, MMS_CONVERSATION_CONSTRAINT),
876                 "thread_id", "date = MAX(date)");
877         String smsSubQuery = smsQueryBuilder.buildUnionSubQuery(
878                 MmsSms.TYPE_DISCRIMINATOR_COLUMN, innerSmsProjection,
879                 SMS_COLUMNS, 1, "sms",
880                 concatSelections(selection, SMS_CONVERSATION_CONSTRAINT),
881                 "thread_id", "date = MAX(date)");
882         SQLiteQueryBuilder unionQueryBuilder = new SQLiteQueryBuilder();
883 
884         unionQueryBuilder.setDistinct(true);
885 
886         String unionQuery = unionQueryBuilder.buildUnionQuery(
887                 new String[] { mmsSubQuery, smsSubQuery }, null, null);
888 
889         SQLiteQueryBuilder outerQueryBuilder = new SQLiteQueryBuilder();
890 
891         outerQueryBuilder.setTables("(" + unionQuery + ")");
892 
893         String outerQuery = outerQueryBuilder.buildQuery(
894                 columns, null, "tid",
895                 "normalized_date = MAX(normalized_date)", sortOrder, null);
896 
897         return mOpenHelper.getReadableDatabase().rawQuery(outerQuery, EMPTY_STRING_ARRAY);
898     }
899 
900     /**
901      * Return the first locked message found in the union of MMS
902      * and SMS messages.
903      *
904      * Use this query:
905      *
906      *  SELECT _id FROM pdu GROUP BY _id HAVING locked=1 UNION SELECT _id FROM sms GROUP
907      *      BY _id HAVING locked=1 LIMIT 1
908      *
909      * We limit by 1 because we're only interested in knowing if
910      * there is *any* locked message, not the actual messages themselves.
911      */
getFirstLockedMessage(String[] projection, String selection, String sortOrder, String smsTable, String pduTable)912     private Cursor getFirstLockedMessage(String[] projection, String selection,
913             String sortOrder, String smsTable, String pduTable) {
914         SQLiteQueryBuilder mmsQueryBuilder = new SQLiteQueryBuilder();
915         SQLiteQueryBuilder smsQueryBuilder = new SQLiteQueryBuilder();
916 
917         mmsQueryBuilder.setTables(pduTable);
918         smsQueryBuilder.setTables(smsTable);
919 
920         String[] idColumn = new String[] { BaseColumns._ID };
921 
922         // NOTE: buildUnionSubQuery *ignores* selectionArgs
923         String mmsSubQuery = mmsQueryBuilder.buildUnionSubQuery(
924                 MmsSms.TYPE_DISCRIMINATOR_COLUMN, idColumn,
925                 null, 1, "mms",
926                 selection,
927                 BaseColumns._ID, "locked=1");
928 
929         String smsSubQuery = smsQueryBuilder.buildUnionSubQuery(
930                 MmsSms.TYPE_DISCRIMINATOR_COLUMN, idColumn,
931                 null, 1, "sms",
932                 selection,
933                 BaseColumns._ID, "locked=1");
934 
935         SQLiteQueryBuilder unionQueryBuilder = new SQLiteQueryBuilder();
936 
937         unionQueryBuilder.setDistinct(true);
938 
939         String unionQuery = unionQueryBuilder.buildUnionQuery(
940                 new String[] { mmsSubQuery, smsSubQuery }, null, "1");
941 
942         Cursor cursor = mOpenHelper.getReadableDatabase().rawQuery(unionQuery, EMPTY_STRING_ARRAY);
943 
944         if (DEBUG) {
945             Log.v("MmsSmsProvider", "getFirstLockedMessage query: " + unionQuery);
946             Log.v("MmsSmsProvider", "cursor count: " + cursor.getCount());
947         }
948         return cursor;
949     }
950 
951     /**
952      * Return every message in each conversation in both MMS
953      * and SMS.
954      */
getCompleteConversations(String[] projection, String selection, String sortOrder, String smsTable, String pduTable)955     private Cursor getCompleteConversations(String[] projection,
956             String selection, String sortOrder, String smsTable, String pduTable) {
957         String unionQuery = buildConversationQuery(projection, selection, sortOrder, smsTable,
958                 pduTable);
959 
960         return mOpenHelper.getReadableDatabase().rawQuery(unionQuery, EMPTY_STRING_ARRAY);
961     }
962 
963     /**
964      * Add normalized date and thread_id to the list of columns for an
965      * inner projection.  This is necessary so that the outer query
966      * can have access to these columns even if the caller hasn't
967      * requested them in the result.
968      */
makeProjectionWithDateAndThreadId( String[] projection, int dateMultiple)969     private String[] makeProjectionWithDateAndThreadId(
970             String[] projection, int dateMultiple) {
971         int projectionSize = projection.length;
972         String[] result = new String[projectionSize + 2];
973 
974         result[0] = "thread_id AS tid";
975         result[1] = "date * " + dateMultiple + " AS normalized_date";
976         for (int i = 0; i < projectionSize; i++) {
977             result[i + 2] = projection[i];
978         }
979         return result;
980     }
981 
982     /**
983      * Return the union of MMS and SMS messages for this thread ID.
984      */
getConversationMessages( String threadIdString, String[] projection, String selection, String sortOrder, String smsTable, String pduTable)985     private Cursor getConversationMessages(
986             String threadIdString, String[] projection, String selection,
987             String sortOrder, String smsTable, String pduTable) {
988         try {
989             Long.parseLong(threadIdString);
990         } catch (NumberFormatException exception) {
991             Log.e(LOG_TAG, "Thread ID must be a Long.");
992             return null;
993         }
994 
995         String finalSelection = concatSelections(
996                 selection, "thread_id = " + threadIdString);
997         String unionQuery = buildConversationQuery(projection, finalSelection, sortOrder, smsTable,
998                 pduTable);
999 
1000         return mOpenHelper.getReadableDatabase().rawQuery(unionQuery, EMPTY_STRING_ARRAY);
1001     }
1002 
1003     /**
1004      * Return the union of MMS and SMS messages whose recipients
1005      * included this phone number.
1006      *
1007      * Use this query:
1008      *
1009      * SELECT ...
1010      *   FROM pdu, (SELECT msg_id AS address_msg_id
1011      *              FROM addr
1012      *              WHERE (address='<phoneNumber>' OR
1013      *              PHONE_NUMBERS_EQUAL(addr.address, '<phoneNumber>', 1/0, none/minMatch)))
1014      *             AS matching_addresses
1015      *   WHERE pdu._id = matching_addresses.address_msg_id
1016      * UNION
1017      * SELECT ...
1018      *   FROM sms
1019      *   WHERE (address='<phoneNumber>' OR
1020      *          PHONE_NUMBERS_EQUAL(sms.address, '<phoneNumber>', 1/0, none/minMatch));
1021      */
getMessagesByPhoneNumber( String phoneNumber, String[] projection, String selection, String sortOrder, String smsTable, String pduTable)1022     private Cursor getMessagesByPhoneNumber(
1023             String phoneNumber, String[] projection, String selection,
1024             String sortOrder, String smsTable, String pduTable) {
1025         String escapedPhoneNumber = DatabaseUtils.sqlEscapeString(phoneNumber);
1026         int minMatch =
1027             getContext().getResources().getInteger(
1028                     com.android.internal.R.integer.config_phonenumber_compare_min_match);
1029         String finalMmsSelection =
1030                 concatSelections(
1031                         selection,
1032                         pduTable + "._id = matching_addresses.address_msg_id");
1033         String finalSmsSelection =
1034                 concatSelections(
1035                         selection,
1036                         "(address=" + escapedPhoneNumber + " OR PHONE_NUMBERS_EQUAL(address, " +
1037                         escapedPhoneNumber +
1038                         (mUseStrictPhoneNumberComparation ? ", 1))" : ", 0, " + minMatch + "))"));
1039         SQLiteQueryBuilder mmsQueryBuilder = new SQLiteQueryBuilder();
1040         SQLiteQueryBuilder smsQueryBuilder = new SQLiteQueryBuilder();
1041 
1042         mmsQueryBuilder.setDistinct(true);
1043         smsQueryBuilder.setDistinct(true);
1044         mmsQueryBuilder.setTables(
1045                 pduTable +
1046                 ", (SELECT msg_id AS address_msg_id " +
1047                 "FROM addr WHERE (address=" + escapedPhoneNumber +
1048                 " OR PHONE_NUMBERS_EQUAL(addr.address, " +
1049                 escapedPhoneNumber +
1050                 (mUseStrictPhoneNumberComparation ? ", 1))) " : ", 0, " + minMatch + "))) ") +
1051                 "AS matching_addresses");
1052         smsQueryBuilder.setTables(smsTable);
1053 
1054         String[] columns = handleNullMessageProjection(projection);
1055         String mmsSubQuery = mmsQueryBuilder.buildUnionSubQuery(
1056                 MmsSms.TYPE_DISCRIMINATOR_COLUMN, columns, MMS_COLUMNS,
1057                 0, "mms", finalMmsSelection, null, null);
1058         String smsSubQuery = smsQueryBuilder.buildUnionSubQuery(
1059                 MmsSms.TYPE_DISCRIMINATOR_COLUMN, columns, SMS_COLUMNS,
1060                 0, "sms", finalSmsSelection, null, null);
1061         SQLiteQueryBuilder unionQueryBuilder = new SQLiteQueryBuilder();
1062 
1063         unionQueryBuilder.setDistinct(true);
1064 
1065         String unionQuery = unionQueryBuilder.buildUnionQuery(
1066                 new String[] { mmsSubQuery, smsSubQuery }, sortOrder, null);
1067 
1068         return mOpenHelper.getReadableDatabase().rawQuery(unionQuery, EMPTY_STRING_ARRAY);
1069     }
1070 
1071     /**
1072      * Return the conversation of certain thread ID.
1073      */
getConversationById( String threadIdString, String[] projection, String selection, String[] selectionArgs, String sortOrder)1074     private Cursor getConversationById(
1075             String threadIdString, String[] projection, String selection,
1076             String[] selectionArgs, String sortOrder) {
1077         try {
1078             Long.parseLong(threadIdString);
1079         } catch (NumberFormatException exception) {
1080             Log.e(LOG_TAG, "Thread ID must be a Long.");
1081             return null;
1082         }
1083 
1084         String extraSelection = "_id=" + threadIdString;
1085         String finalSelection = concatSelections(selection, extraSelection);
1086         SQLiteQueryBuilder queryBuilder = new SQLiteQueryBuilder();
1087         String[] columns = handleNullThreadsProjection(projection);
1088 
1089         queryBuilder.setDistinct(true);
1090         queryBuilder.setTables(TABLE_THREADS);
1091         return queryBuilder.query(
1092                 mOpenHelper.getReadableDatabase(), columns, finalSelection,
1093                 selectionArgs, sortOrder, null, null);
1094     }
1095 
joinPduAndPendingMsgTables(String pduTable)1096     private static String joinPduAndPendingMsgTables(String pduTable) {
1097         return pduTable + " LEFT JOIN " + TABLE_PENDING_MSG
1098                 + " ON " + pduTable + "._id = pending_msgs.msg_id";
1099     }
1100 
createMmsProjection(String[] old, String pduTable)1101     private static String[] createMmsProjection(String[] old, String pduTable) {
1102         String[] newProjection = new String[old.length];
1103         for (int i = 0; i < old.length; i++) {
1104             if (old[i].equals(BaseColumns._ID)) {
1105                 newProjection[i] = pduTable + "._id";
1106             } else {
1107                 newProjection[i] = old[i];
1108             }
1109         }
1110         return newProjection;
1111     }
1112 
getUndeliveredMessages( String[] projection, String selection, String[] selectionArgs, String sortOrder, String smsTable, String pduTable)1113     private Cursor getUndeliveredMessages(
1114             String[] projection, String selection, String[] selectionArgs,
1115             String sortOrder, String smsTable, String pduTable) {
1116         String[] mmsProjection = createMmsProjection(projection, pduTable);
1117 
1118         SQLiteQueryBuilder mmsQueryBuilder = new SQLiteQueryBuilder();
1119         SQLiteQueryBuilder smsQueryBuilder = new SQLiteQueryBuilder();
1120 
1121         mmsQueryBuilder.setTables(joinPduAndPendingMsgTables(pduTable));
1122         smsQueryBuilder.setTables(smsTable);
1123 
1124         String finalMmsSelection = concatSelections(
1125                 selection, Mms.MESSAGE_BOX + " = " + Mms.MESSAGE_BOX_OUTBOX);
1126         String finalSmsSelection = concatSelections(
1127                 selection, "(" + Sms.TYPE + " = " + Sms.MESSAGE_TYPE_OUTBOX
1128                 + " OR " + Sms.TYPE + " = " + Sms.MESSAGE_TYPE_FAILED
1129                 + " OR " + Sms.TYPE + " = " + Sms.MESSAGE_TYPE_QUEUED + ")");
1130 
1131         String[] smsColumns = handleNullMessageProjection(projection);
1132         String[] mmsColumns = handleNullMessageProjection(mmsProjection);
1133         String[] innerMmsProjection = makeProjectionWithDateAndThreadId(
1134                 mmsColumns, 1000);
1135         String[] innerSmsProjection = makeProjectionWithDateAndThreadId(
1136                 smsColumns, 1);
1137 
1138         Set<String> columnsPresentInTable = new HashSet<String>(MMS_COLUMNS);
1139         columnsPresentInTable.add(pduTable + "._id");
1140         columnsPresentInTable.add(PendingMessages.ERROR_TYPE);
1141         String mmsSubQuery = mmsQueryBuilder.buildUnionSubQuery(
1142                 MmsSms.TYPE_DISCRIMINATOR_COLUMN, innerMmsProjection,
1143                 columnsPresentInTable, 1, "mms", finalMmsSelection,
1144                 null, null);
1145         String smsSubQuery = smsQueryBuilder.buildUnionSubQuery(
1146                 MmsSms.TYPE_DISCRIMINATOR_COLUMN, innerSmsProjection,
1147                 SMS_COLUMNS, 1, "sms", finalSmsSelection,
1148                 null, null);
1149         SQLiteQueryBuilder unionQueryBuilder = new SQLiteQueryBuilder();
1150 
1151         unionQueryBuilder.setDistinct(true);
1152 
1153         String unionQuery = unionQueryBuilder.buildUnionQuery(
1154                 new String[] { smsSubQuery, mmsSubQuery }, null, null);
1155 
1156         SQLiteQueryBuilder outerQueryBuilder = new SQLiteQueryBuilder();
1157 
1158         outerQueryBuilder.setTables("(" + unionQuery + ")");
1159 
1160         String outerQuery = outerQueryBuilder.buildQuery(
1161                 smsColumns, null, null, null, sortOrder, null);
1162 
1163         return mOpenHelper.getReadableDatabase().rawQuery(outerQuery, EMPTY_STRING_ARRAY);
1164     }
1165 
1166     /**
1167      * Add normalized date to the list of columns for an inner
1168      * projection.
1169      */
makeProjectionWithNormalizedDate( String[] projection, int dateMultiple)1170     private static String[] makeProjectionWithNormalizedDate(
1171             String[] projection, int dateMultiple) {
1172         int projectionSize = projection.length;
1173         String[] result = new String[projectionSize + 1];
1174 
1175         result[0] = "date * " + dateMultiple + " AS normalized_date";
1176         System.arraycopy(projection, 0, result, 1, projectionSize);
1177         return result;
1178     }
1179 
buildConversationQuery(String[] projection, String selection, String sortOrder, String smsTable, String pduTable)1180     private static String buildConversationQuery(String[] projection,
1181             String selection, String sortOrder, String smsTable, String pduTable) {
1182         String[] mmsProjection = createMmsProjection(projection, pduTable);
1183 
1184         SQLiteQueryBuilder mmsQueryBuilder = new SQLiteQueryBuilder();
1185         SQLiteQueryBuilder smsQueryBuilder = new SQLiteQueryBuilder();
1186 
1187         mmsQueryBuilder.setDistinct(true);
1188         smsQueryBuilder.setDistinct(true);
1189         mmsQueryBuilder.setTables(joinPduAndPendingMsgTables(pduTable));
1190         smsQueryBuilder.setTables(smsTable);
1191 
1192         String[] smsColumns = handleNullMessageProjection(projection);
1193         String[] mmsColumns = handleNullMessageProjection(mmsProjection);
1194         String[] innerMmsProjection = makeProjectionWithNormalizedDate(mmsColumns, 1000);
1195         String[] innerSmsProjection = makeProjectionWithNormalizedDate(smsColumns, 1);
1196 
1197         Set<String> columnsPresentInTable = new HashSet<String>(MMS_COLUMNS);
1198         columnsPresentInTable.add(pduTable + "._id");
1199         columnsPresentInTable.add(PendingMessages.ERROR_TYPE);
1200 
1201         String mmsSelection = concatSelections(selection,
1202                                 Mms.MESSAGE_BOX + " != " + Mms.MESSAGE_BOX_DRAFTS);
1203         String mmsSubQuery = mmsQueryBuilder.buildUnionSubQuery(
1204                 MmsSms.TYPE_DISCRIMINATOR_COLUMN, innerMmsProjection,
1205                 columnsPresentInTable, 0, "mms",
1206                 concatSelections(mmsSelection, MMS_CONVERSATION_CONSTRAINT),
1207                 null, null);
1208         String smsSubQuery = smsQueryBuilder.buildUnionSubQuery(
1209                 MmsSms.TYPE_DISCRIMINATOR_COLUMN, innerSmsProjection, SMS_COLUMNS,
1210                 0, "sms", concatSelections(selection, SMS_CONVERSATION_CONSTRAINT),
1211                 null, null);
1212         SQLiteQueryBuilder unionQueryBuilder = new SQLiteQueryBuilder();
1213 
1214         unionQueryBuilder.setDistinct(true);
1215 
1216         String unionQuery = unionQueryBuilder.buildUnionQuery(
1217                 new String[] { smsSubQuery, mmsSubQuery },
1218                 handleNullSortOrder(sortOrder), null);
1219 
1220         SQLiteQueryBuilder outerQueryBuilder = new SQLiteQueryBuilder();
1221 
1222         outerQueryBuilder.setTables("(" + unionQuery + ")");
1223 
1224         return outerQueryBuilder.buildQuery(
1225                 smsColumns, null, null, null, sortOrder, null);
1226     }
1227 
1228     @Override
getType(Uri uri)1229     public String getType(Uri uri) {
1230         return VND_ANDROID_DIR_MMS_SMS;
1231     }
1232 
1233     @Override
delete(Uri uri, String selection, String[] selectionArgs)1234     public int delete(Uri uri, String selection,
1235             String[] selectionArgs) {
1236         SQLiteDatabase db = mOpenHelper.getWritableDatabase();
1237         Context context = getContext();
1238         int affectedRows = 0;
1239 
1240         switch(URI_MATCHER.match(uri)) {
1241             case URI_CONVERSATIONS_MESSAGES:
1242                 long threadId;
1243                 try {
1244                     threadId = Long.parseLong(uri.getLastPathSegment());
1245                 } catch (NumberFormatException e) {
1246                     Log.e(LOG_TAG, "Thread ID must be a long.");
1247                     break;
1248                 }
1249                 affectedRows = deleteConversation(uri, selection, selectionArgs);
1250                 MmsSmsDatabaseHelper.updateThread(db, threadId);
1251                 break;
1252             case URI_CONVERSATIONS:
1253                 affectedRows = MmsProvider.deleteMessages(context, db,
1254                                         selection, selectionArgs, uri)
1255                         + db.delete("sms", selection, selectionArgs);
1256                 // Intentionally don't pass the selection variable to updateThreads.
1257                 // When we pass in "locked=0" there, the thread will get excluded from
1258                 // the selection and not get updated.
1259                 MmsSmsDatabaseHelper.updateThreads(db, null, null);
1260                 break;
1261             case URI_OBSOLETE_THREADS:
1262                 affectedRows = db.delete(TABLE_THREADS,
1263                         "_id NOT IN (SELECT DISTINCT thread_id FROM sms where thread_id NOT NULL " +
1264                         "UNION SELECT DISTINCT thread_id FROM pdu where thread_id NOT NULL)", null);
1265                 break;
1266             default:
1267                 throw new UnsupportedOperationException(NO_DELETES_INSERTS_OR_UPDATES + uri);
1268         }
1269 
1270         if (affectedRows > 0) {
1271             context.getContentResolver().notifyChange(MmsSms.CONTENT_URI, null, true,
1272                     UserHandle.USER_ALL);
1273         }
1274         return affectedRows;
1275     }
1276 
1277     /**
1278      * Delete the conversation with the given thread ID.
1279      */
deleteConversation(Uri uri, String selection, String[] selectionArgs)1280     private int deleteConversation(Uri uri, String selection, String[] selectionArgs) {
1281         String threadId = uri.getLastPathSegment();
1282 
1283         SQLiteDatabase db = mOpenHelper.getWritableDatabase();
1284         String finalSelection = concatSelections(selection, "thread_id = " + threadId);
1285         return MmsProvider.deleteMessages(getContext(), db, finalSelection,
1286                                           selectionArgs, uri)
1287                 + db.delete("sms", finalSelection, selectionArgs);
1288     }
1289 
1290     @Override
insert(Uri uri, ContentValues values)1291     public Uri insert(Uri uri, ContentValues values) {
1292         SQLiteDatabase db = mOpenHelper.getWritableDatabase();
1293         int matchIndex = URI_MATCHER.match(uri);
1294 
1295         if (matchIndex == URI_PENDING_MSG) {
1296             long rowId = db.insert(TABLE_PENDING_MSG, null, values);
1297             return uri.buildUpon().appendPath(Long.toString(rowId)).build();
1298         } else if (matchIndex == URI_CANONICAL_ADDRESS) {
1299             long rowId = db.insert(TABLE_CANONICAL_ADDRESSES, null, values);
1300             return uri.buildUpon().appendPath(Long.toString(rowId)).build();
1301         }
1302         throw new UnsupportedOperationException(NO_DELETES_INSERTS_OR_UPDATES + uri);
1303     }
1304 
1305     @Override
update(Uri uri, ContentValues values, String selection, String[] selectionArgs)1306     public int update(Uri uri, ContentValues values,
1307             String selection, String[] selectionArgs) {
1308         final int callerUid = Binder.getCallingUid();
1309         final String callerPkg = getCallingPackage();
1310         SQLiteDatabase db = mOpenHelper.getWritableDatabase();
1311         int affectedRows = 0;
1312         switch(URI_MATCHER.match(uri)) {
1313             case URI_CONVERSATIONS_MESSAGES:
1314                 String threadIdString = uri.getPathSegments().get(1);
1315                 affectedRows = updateConversation(threadIdString, values,
1316                         selection, selectionArgs, callerUid, callerPkg);
1317                 break;
1318 
1319             case URI_PENDING_MSG:
1320                 affectedRows = db.update(TABLE_PENDING_MSG, values, selection, null);
1321                 break;
1322 
1323             case URI_CANONICAL_ADDRESS: {
1324                 String extraSelection = "_id=" + uri.getPathSegments().get(1);
1325                 String finalSelection = TextUtils.isEmpty(selection)
1326                         ? extraSelection : extraSelection + " AND " + selection;
1327 
1328                 affectedRows = db.update(TABLE_CANONICAL_ADDRESSES, values, finalSelection, null);
1329                 break;
1330             }
1331 
1332             case URI_CONVERSATIONS: {
1333                 final ContentValues finalValues = new ContentValues(1);
1334                 if (values.containsKey(Threads.ARCHIVED)) {
1335                     // Only allow update archived
1336                     finalValues.put(Threads.ARCHIVED, values.getAsBoolean(Threads.ARCHIVED));
1337                 }
1338                 affectedRows = db.update(TABLE_THREADS, finalValues, selection, selectionArgs);
1339                 break;
1340             }
1341 
1342             default:
1343                 throw new UnsupportedOperationException(
1344                         NO_DELETES_INSERTS_OR_UPDATES + uri);
1345         }
1346 
1347         if (affectedRows > 0) {
1348             getContext().getContentResolver().notifyChange(
1349                     MmsSms.CONTENT_URI, null, true, UserHandle.USER_ALL);
1350         }
1351         return affectedRows;
1352     }
1353 
updateConversation(String threadIdString, ContentValues values, String selection, String[] selectionArgs, int callerUid, String callerPkg)1354     private int updateConversation(String threadIdString, ContentValues values, String selection,
1355             String[] selectionArgs, int callerUid, String callerPkg) {
1356         try {
1357             Long.parseLong(threadIdString);
1358         } catch (NumberFormatException exception) {
1359             Log.e(LOG_TAG, "Thread ID must be a Long.");
1360             return 0;
1361 
1362         }
1363         if (ProviderUtil.shouldRemoveCreator(values, callerUid)) {
1364             // CREATOR should not be changed by non-SYSTEM/PHONE apps
1365             Log.w(LOG_TAG, callerPkg + " tries to update CREATOR");
1366             // Sms.CREATOR and Mms.CREATOR are same. But let's do this
1367             // twice in case the names may differ in the future
1368             values.remove(Sms.CREATOR);
1369             values.remove(Mms.CREATOR);
1370         }
1371 
1372         SQLiteDatabase db = mOpenHelper.getWritableDatabase();
1373         String finalSelection = concatSelections(selection, "thread_id=" + threadIdString);
1374         return db.update(MmsProvider.TABLE_PDU, values, finalSelection, selectionArgs)
1375                 + db.update("sms", values, finalSelection, selectionArgs);
1376     }
1377 
1378     /**
1379      * Construct Sets of Strings containing exactly the columns
1380      * present in each table.  We will use this when constructing
1381      * UNION queries across the MMS and SMS tables.
1382      */
initializeColumnSets()1383     private static void initializeColumnSets() {
1384         int commonColumnCount = MMS_SMS_COLUMNS.length;
1385         int mmsOnlyColumnCount = MMS_ONLY_COLUMNS.length;
1386         int smsOnlyColumnCount = SMS_ONLY_COLUMNS.length;
1387         Set<String> unionColumns = new HashSet<String>();
1388 
1389         for (int i = 0; i < commonColumnCount; i++) {
1390             MMS_COLUMNS.add(MMS_SMS_COLUMNS[i]);
1391             SMS_COLUMNS.add(MMS_SMS_COLUMNS[i]);
1392             unionColumns.add(MMS_SMS_COLUMNS[i]);
1393         }
1394         for (int i = 0; i < mmsOnlyColumnCount; i++) {
1395             MMS_COLUMNS.add(MMS_ONLY_COLUMNS[i]);
1396             unionColumns.add(MMS_ONLY_COLUMNS[i]);
1397         }
1398         for (int i = 0; i < smsOnlyColumnCount; i++) {
1399             SMS_COLUMNS.add(SMS_ONLY_COLUMNS[i]);
1400             unionColumns.add(SMS_ONLY_COLUMNS[i]);
1401         }
1402 
1403         int i = 0;
1404         for (String columnName : unionColumns) {
1405             UNION_COLUMNS[i++] = columnName;
1406         }
1407     }
1408 
1409     @Override
dump(FileDescriptor fd, PrintWriter writer, String[] args)1410     public void dump(FileDescriptor fd, PrintWriter writer, String[] args) {
1411         // Dump default SMS app
1412         String defaultSmsApp = Telephony.Sms.getDefaultSmsPackage(getContext());
1413         if (TextUtils.isEmpty(defaultSmsApp)) {
1414             defaultSmsApp = "None";
1415         }
1416         writer.println("Default SMS app: " + defaultSmsApp);
1417     }
1418 
1419     @Override
call(String method, String arg, Bundle extras)1420     public Bundle call(String method, String arg, Bundle extras) {
1421         if (METHOD_IS_RESTORING.equals(method)) {
1422             Bundle result = new Bundle();
1423             result.putBoolean(IS_RESTORING_KEY, TelephonyBackupAgent.getIsRestoring());
1424             return result;
1425         }
1426         Log.w(LOG_TAG, "Ignored unsupported " + method + " call");
1427         return null;
1428     }
1429 }
1430