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