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