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