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