• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2006 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.annotation.NonNull;
20 import android.app.AppOpsManager;
21 import android.content.BroadcastReceiver;
22 import android.content.ContentProvider;
23 import android.content.ContentResolver;
24 import android.content.ContentValues;
25 import android.content.Context;
26 import android.content.Intent;
27 import android.content.IntentFilter;
28 import android.content.UriMatcher;
29 import android.database.Cursor;
30 import android.database.DatabaseUtils;
31 import android.database.MatrixCursor;
32 import android.database.sqlite.SQLiteDatabase;
33 import android.database.sqlite.SQLiteOpenHelper;
34 import android.database.sqlite.SQLiteQueryBuilder;
35 import android.net.Uri;
36 import android.os.Binder;
37 import android.os.UserHandle;
38 import android.os.UserManager;
39 import android.provider.Contacts;
40 import android.provider.Telephony;
41 import android.provider.Telephony.MmsSms;
42 import android.provider.Telephony.Sms;
43 import android.provider.Telephony.Threads;
44 import android.telephony.SmsManager;
45 import android.telephony.SmsMessage;
46 import android.telephony.SubscriptionManager;
47 import android.text.TextUtils;
48 import android.util.Log;
49 
50 import com.android.internal.annotations.VisibleForTesting;
51 import com.android.internal.telephony.TelephonyPermissions;
52 import com.android.internal.telephony.util.TelephonyUtils;
53 
54 import java.util.HashMap;
55 import java.util.List;
56 
57 public class SmsProvider extends ContentProvider {
58     /* No response constant from SmsResponse */
59     static final int NO_ERROR_CODE = -1;
60 
61     private static final Uri NOTIFICATION_URI = Uri.parse("content://sms");
62     private static final Uri ICC_URI = Uri.parse("content://sms/icc");
63     private static final Uri ICC_SUBID_URI = Uri.parse("content://sms/icc_subId");
64     static final String TABLE_SMS = "sms";
65     static final String TABLE_RAW = "raw";
66     static final String TABLE_ATTACHMENTS = "attachments";
67     static final String TABLE_CANONICAL_ADDRESSES = "canonical_addresses";
68     static final String TABLE_SR_PENDING = "sr_pending";
69     private static final String TABLE_WORDS = "words";
70     static final String VIEW_SMS_RESTRICTED = "sms_restricted";
71 
72     private static final Integer ONE = Integer.valueOf(1);
73 
74     private static final String[] CONTACT_QUERY_PROJECTION =
75             new String[] { Contacts.Phones.PERSON_ID };
76     private static final int PERSON_ID_COLUMN = 0;
77 
78     /** Delete any raw messages or message segments marked deleted that are older than an hour. */
79     static final long RAW_MESSAGE_EXPIRE_AGE_MS = (long) (60 * 60 * 1000);
80 
81     /**
82      * These are the columns that are available when reading SMS
83      * messages from the ICC.  Columns whose names begin with "is_"
84      * have either "true" or "false" as their values.
85      */
86     private final static String[] ICC_COLUMNS = new String[] {
87         // N.B.: These columns must appear in the same order as the
88         // calls to add appear in convertIccToSms.
89         "service_center_address",       // getServiceCenterAddress
90         "address",                      // getDisplayOriginatingAddress or getRecipientAddress
91         "message_class",                // getMessageClass
92         "body",                         // getDisplayMessageBody
93         "date",                         // getTimestampMillis
94         "status",                       // getStatusOnIcc
95         "index_on_icc",                 // getIndexOnIcc (1-based index)
96         "is_status_report",             // isStatusReportMessage
97         "transport_type",               // Always "sms".
98         "type",                         // depend on getStatusOnIcc
99         "locked",                       // Always 0 (false).
100         "error_code",                   // Always -1 (NO_ERROR_CODE), previously it was 0 always.
101         "_id"
102     };
103 
104     @Override
onCreate()105     public boolean onCreate() {
106         setAppOps(AppOpsManager.OP_READ_SMS, AppOpsManager.OP_WRITE_SMS);
107         // So we have two database files. One in de, one in ce. Here only "raw" table is in
108         // mDeOpenHelper, other tables are all in mCeOpenHelper.
109         mDeOpenHelper = MmsSmsDatabaseHelper.getInstanceForDe(getContext());
110         mCeOpenHelper = MmsSmsDatabaseHelper.getInstanceForCe(getContext());
111         TelephonyBackupAgent.DeferredSmsMmsRestoreService.startIfFilesExist(getContext());
112 
113         // Creating intent broadcast receiver for user actions like Intent.ACTION_USER_REMOVED,
114         // where we would need to remove SMS related to removed user.
115         IntentFilter userIntentFilter = new IntentFilter(Intent.ACTION_USER_REMOVED);
116         getContext().registerReceiver(mUserIntentReceiver, userIntentFilter,
117                 Context.RECEIVER_NOT_EXPORTED);
118 
119         return true;
120     }
121 
122     /**
123      * Return the proper view of "sms" table for the current access status.
124      *
125      * @param accessRestricted If the access is restricted
126      * @return the table/view name of the "sms" data
127      */
getSmsTable(boolean accessRestricted)128     public static String getSmsTable(boolean accessRestricted) {
129         return accessRestricted ? VIEW_SMS_RESTRICTED : TABLE_SMS;
130     }
131 
132     @Override
query(Uri url, String[] projectionIn, String selection, String[] selectionArgs, String sort)133     public Cursor query(Uri url, String[] projectionIn, String selection,
134             String[] selectionArgs, String sort) {
135         final int callingUid = Binder.getCallingUid();
136         final UserHandle callerUserHandle = Binder.getCallingUserHandle();
137 
138         // First check if a restricted view of the "sms" table should be used based on the
139         // caller's identity. Only system, phone or the default sms app can have full access
140         // of sms data. For other apps, we present a restricted view which only contains sent
141         // or received messages.
142         final boolean accessRestricted = ProviderUtil.isAccessRestricted(
143                 getContext(), getCallingPackage(), callingUid);
144         final String smsTable = getSmsTable(accessRestricted);
145         SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
146 
147         // If access is restricted, we don't allow subqueries in the query.
148         if (accessRestricted) {
149             try {
150                 SqlQueryChecker.checkQueryParametersForSubqueries(projectionIn, selection, sort);
151             } catch (IllegalArgumentException e) {
152                 Log.w(TAG, "Query rejected: " + e.getMessage());
153                 return null;
154             }
155         }
156 
157         Cursor emptyCursor = new MatrixCursor((projectionIn == null) ?
158                 (new String[] {}) : projectionIn);
159 
160         // Generate the body of the query.
161         int match = sURLMatcher.match(url);
162         SQLiteDatabase db = getReadableDatabase(match);
163         switch (match) {
164             case SMS_ALL:
165                 constructQueryForBox(qb, Sms.MESSAGE_TYPE_ALL, smsTable);
166                 break;
167 
168             case SMS_UNDELIVERED:
169                 constructQueryForUndelivered(qb, smsTable);
170                 break;
171 
172             case SMS_FAILED:
173                 constructQueryForBox(qb, Sms.MESSAGE_TYPE_FAILED, smsTable);
174                 break;
175 
176             case SMS_QUEUED:
177                 constructQueryForBox(qb, Sms.MESSAGE_TYPE_QUEUED, smsTable);
178                 break;
179 
180             case SMS_INBOX:
181                 constructQueryForBox(qb, Sms.MESSAGE_TYPE_INBOX, smsTable);
182                 break;
183 
184             case SMS_SENT:
185                 constructQueryForBox(qb, Sms.MESSAGE_TYPE_SENT, smsTable);
186                 break;
187 
188             case SMS_DRAFT:
189                 constructQueryForBox(qb, Sms.MESSAGE_TYPE_DRAFT, smsTable);
190                 break;
191 
192             case SMS_OUTBOX:
193                 constructQueryForBox(qb, Sms.MESSAGE_TYPE_OUTBOX, smsTable);
194                 break;
195 
196             case SMS_ALL_ID:
197                 qb.setTables(smsTable);
198                 qb.appendWhere("(_id = " + url.getPathSegments().get(0) + ")");
199                 break;
200 
201             case SMS_INBOX_ID:
202             case SMS_FAILED_ID:
203             case SMS_SENT_ID:
204             case SMS_DRAFT_ID:
205             case SMS_OUTBOX_ID:
206                 qb.setTables(smsTable);
207                 qb.appendWhere("(_id = " + url.getPathSegments().get(1) + ")");
208                 break;
209 
210             case SMS_CONVERSATIONS_ID:
211                 int threadID;
212 
213                 try {
214                     threadID = Integer.parseInt(url.getPathSegments().get(1));
215                     if (Log.isLoggable(TAG, Log.VERBOSE)) {
216                         Log.d(TAG, "query conversations: threadID=" + threadID);
217                     }
218                 }
219                 catch (Exception ex) {
220                     Log.e(TAG,
221                           "Bad conversation thread id: "
222                           + url.getPathSegments().get(1));
223                     return null;
224                 }
225 
226                 qb.setTables(smsTable);
227                 qb.appendWhere("thread_id = " + threadID);
228                 break;
229 
230             case SMS_CONVERSATIONS:
231                 qb.setTables(smsTable + ", "
232                         + "(SELECT thread_id AS group_thread_id, "
233                         + "MAX(date) AS group_date, "
234                         + "COUNT(*) AS msg_count "
235                         + "FROM " + smsTable + " "
236                         + "GROUP BY thread_id) AS groups");
237                 qb.appendWhere(smsTable + ".thread_id=groups.group_thread_id"
238                         + " AND " + smsTable + ".date=groups.group_date");
239                 final HashMap<String, String> projectionMap = new HashMap<>();
240                 projectionMap.put(Sms.Conversations.SNIPPET,
241                         smsTable + ".body AS snippet");
242                 projectionMap.put(Sms.Conversations.THREAD_ID,
243                         smsTable + ".thread_id AS thread_id");
244                 projectionMap.put(Sms.Conversations.MESSAGE_COUNT,
245                         "groups.msg_count AS msg_count");
246                 projectionMap.put("delta", null);
247                 qb.setProjectionMap(projectionMap);
248                 break;
249 
250             case SMS_RAW_MESSAGE:
251                 // before querying purge old entries with deleted = 1
252                 purgeDeletedMessagesInRawTable(db);
253                 qb.setTables("raw");
254                 break;
255 
256             case SMS_STATUS_PENDING:
257                 qb.setTables("sr_pending");
258                 break;
259 
260             case SMS_ATTACHMENT:
261                 qb.setTables("attachments");
262                 break;
263 
264             case SMS_ATTACHMENT_ID:
265                 qb.setTables("attachments");
266                 qb.appendWhere(
267                         "(sms_id = " + url.getPathSegments().get(1) + ")");
268                 break;
269 
270             case SMS_QUERY_THREAD_ID:
271                 qb.setTables("canonical_addresses");
272                 if (projectionIn == null) {
273                     projectionIn = sIDProjection;
274                 }
275                 break;
276 
277             case SMS_STATUS_ID:
278                 qb.setTables(smsTable);
279                 qb.appendWhere("(_id = " + url.getPathSegments().get(1) + ")");
280                 break;
281 
282             case SMS_ALL_ICC:
283             case SMS_ALL_ICC_SUBID:
284                 {
285                     int subId;
286                     if (match == SMS_ALL_ICC) {
287                         subId = SmsManager.getDefaultSmsSubscriptionId();
288                     } else {
289                         try {
290                             subId = Integer.parseInt(url.getPathSegments().get(1));
291                         } catch (NumberFormatException e) {
292                             throw new IllegalArgumentException("Wrong path segements, uri= " + url);
293                         }
294                     }
295 
296                     if (!TelephonyPermissions.checkSubscriptionAssociatedWithUser(getContext(),
297                             subId, callerUserHandle)) {
298                         // If subId is not associated with user, return empty cursor.
299                         return emptyCursor;
300                     }
301 
302                     Cursor ret = getAllMessagesFromIcc(subId);
303                     ret.setNotificationUri(getContext().getContentResolver(),
304                             match == SMS_ALL_ICC ? ICC_URI : ICC_SUBID_URI);
305                     return ret;
306                 }
307 
308             case SMS_ICC:
309             case SMS_ICC_SUBID:
310                 {
311                     int subId;
312                     int messageIndex;
313                     try {
314                         if (match == SMS_ICC) {
315                             subId = SmsManager.getDefaultSmsSubscriptionId();
316                             messageIndex = Integer.parseInt(url.getPathSegments().get(1));
317                         } else {
318                             subId = Integer.parseInt(url.getPathSegments().get(1));
319                             messageIndex = Integer.parseInt(url.getPathSegments().get(2));
320                         }
321                     } catch (NumberFormatException e) {
322                         throw new IllegalArgumentException("Wrong path segements, uri= " + url);
323                     }
324 
325                     if (!TelephonyPermissions.checkSubscriptionAssociatedWithUser(getContext(),
326                             subId, callerUserHandle)) {
327                         // If subId is not associated with user, return empty cursor.
328                         return emptyCursor;
329                     }
330 
331                     Cursor ret = getSingleMessageFromIcc(subId, messageIndex);
332                     ret.setNotificationUri(getContext().getContentResolver(),
333                             match == SMS_ICC ? ICC_URI : ICC_SUBID_URI);
334                     return ret;
335                 }
336 
337             default:
338                 Log.e(TAG, "Invalid request: " + url);
339                 return null;
340         }
341 
342         final long token = Binder.clearCallingIdentity();
343         String selectionBySubIds = null;
344         String selectionByEmergencyNumbers = null;
345         try {
346             // Filter SMS based on subId and emergency numbers.
347             selectionBySubIds = ProviderUtil.getSelectionBySubIds(getContext(),
348                     callerUserHandle);
349             if (qb.getTables().equals(smsTable)) {
350                 selectionByEmergencyNumbers = ProviderUtil
351                         .getSelectionByEmergencyNumbers(getContext());
352             }
353         } finally {
354             Binder.restoreCallingIdentity(token);
355         }
356 
357         if (qb.getTables().equals(smsTable)) {
358             if (selectionBySubIds == null && selectionByEmergencyNumbers == null) {
359                 // No subscriptions associated with user
360                 // and no emergency numbers return empty cursor.
361                 return emptyCursor;
362             }
363         } else {
364             if (selectionBySubIds == null) {
365                 // No subscriptions associated with user return empty cursor.
366                 return emptyCursor;
367             }
368         }
369 
370         String filter = "";
371         if (selectionBySubIds != null && selectionByEmergencyNumbers != null) {
372             filter = (selectionBySubIds + " OR " + selectionByEmergencyNumbers);
373         } else {
374             filter = selectionBySubIds == null ?
375                     selectionByEmergencyNumbers : selectionBySubIds;
376         }
377         selection = DatabaseUtils.concatenateWhere(selection, filter);
378 
379         String orderBy = null;
380 
381         if (!TextUtils.isEmpty(sort)) {
382             orderBy = sort;
383         } else if (qb.getTables().equals(smsTable)) {
384             orderBy = Sms.DEFAULT_SORT_ORDER;
385         }
386 
387         Cursor ret = qb.query(db, projectionIn, selection, selectionArgs,
388                               null, null, orderBy);
389         // TODO: Since the URLs are a mess, always use content://sms
390         ret.setNotificationUri(getContext().getContentResolver(),
391                 NOTIFICATION_URI);
392         return ret;
393     }
394 
purgeDeletedMessagesInRawTable(SQLiteDatabase db)395     private void purgeDeletedMessagesInRawTable(SQLiteDatabase db) {
396         long oldTimestamp = System.currentTimeMillis() - RAW_MESSAGE_EXPIRE_AGE_MS;
397         int num = db.delete(TABLE_RAW, "deleted = 1 AND date < " + oldTimestamp, null);
398         if (Log.isLoggable(TAG, Log.VERBOSE)) {
399             Log.d(TAG, "purgeDeletedMessagesInRawTable: num rows older than " + oldTimestamp +
400                     " purged: " + num);
401         }
402     }
403 
getDBOpenHelper(int match)404     private SQLiteOpenHelper getDBOpenHelper(int match) {
405         // Raw table is stored on de database. Other tables are stored in ce database.
406         if (match == SMS_RAW_MESSAGE || match == SMS_RAW_MESSAGE_PERMANENT_DELETE) {
407             return mDeOpenHelper;
408         }
409         return mCeOpenHelper;
410     }
411 
convertIccToSms(SmsMessage message, int id)412     private Object[] convertIccToSms(SmsMessage message, int id) {
413         int statusOnIcc = message.getStatusOnIcc();
414         int type = Sms.MESSAGE_TYPE_ALL;
415         switch (statusOnIcc) {
416             case SmsManager.STATUS_ON_ICC_READ:
417             case SmsManager.STATUS_ON_ICC_UNREAD:
418                 type = Sms.MESSAGE_TYPE_INBOX;
419                 break;
420             case SmsManager.STATUS_ON_ICC_SENT:
421                 type = Sms.MESSAGE_TYPE_SENT;
422                 break;
423             case SmsManager.STATUS_ON_ICC_UNSENT:
424                 type = Sms.MESSAGE_TYPE_OUTBOX;
425                 break;
426         }
427 
428         String address = (type == Sms.MESSAGE_TYPE_INBOX)
429                 ? message.getDisplayOriginatingAddress()
430                 : message.getRecipientAddress();
431 
432         int index = message.getIndexOnIcc();
433         if (address == null) {
434             // The status byte of an EF_SMS record may not be correct. try to read other address
435             // type again.
436             Log.e(TAG, "convertIccToSms: EF_SMS(" + index + ")=> address=null, type=" + type
437                     + ", status=" + statusOnIcc + "(may not be correct). fallback to other type.");
438             address = (type == Sms.MESSAGE_TYPE_INBOX)
439                     ? message.getRecipientAddress()
440                     : message.getDisplayOriginatingAddress();
441 
442             if (address != null) {
443                 // Rely on actual PDU(address) to set type again.
444                 type = (type == Sms.MESSAGE_TYPE_INBOX)
445                         ? Sms.MESSAGE_TYPE_SENT
446                         : Sms.MESSAGE_TYPE_INBOX;
447                 Log.d(TAG, "convertIccToSms: new type=" + type + ", address=xxxxxx");
448             } else {
449                 Log.e(TAG, "convertIccToSms: no change");
450             }
451         }
452 
453         // N.B.: These calls must appear in the same order as the
454         // columns appear in ICC_COLUMNS.
455         Object[] row = new Object[13];
456         row[0] = message.getServiceCenterAddress();
457         row[1] = address;
458         row[2] = String.valueOf(message.getMessageClass());
459         row[3] = message.getDisplayMessageBody();
460         row[4] = message.getTimestampMillis();
461         row[5] = statusOnIcc;
462         row[6] = index;
463         row[7] = message.isStatusReportMessage();
464         row[8] = "sms";
465         row[9] = type;
466         row[10] = 0;      // locked
467         row[11] = NO_ERROR_CODE;
468         row[12] = id;
469         return row;
470     }
471 
472     /**
473      * Gets single message from the ICC for a subscription ID.
474      *
475      * @param subId the subscription ID.
476      * @param messageIndex the message index of the messaage in the ICC (1-based index).
477      * @return a cursor containing just one message from the ICC for the subscription ID.
478      */
getSingleMessageFromIcc(int subId, int messageIndex)479     private Cursor getSingleMessageFromIcc(int subId, int messageIndex) {
480         if (!SubscriptionManager.isValidSubscriptionId(subId)) {
481             throw new IllegalArgumentException("Invalid Subscription ID " + subId);
482         }
483         SmsManager smsManager = SmsManager.getSmsManagerForSubscriptionId(subId);
484         List<SmsMessage> messages;
485 
486         // Use phone app permissions to avoid UID mismatch in AppOpsManager.noteOp() call.
487         long token = Binder.clearCallingIdentity();
488         try {
489             // getMessagesFromIcc() returns a zero-based list of valid messages in the ICC.
490             messages = smsManager.getMessagesFromIcc();
491         } finally {
492             Binder.restoreCallingIdentity(token);
493         }
494 
495         final int count = messages.size();
496         for (int i = 0; i < count; i++) {
497             SmsMessage message = messages.get(i);
498             if (message != null && message.getIndexOnIcc() == messageIndex) {
499                 MatrixCursor cursor = new MatrixCursor(ICC_COLUMNS, 1);
500                 cursor.addRow(convertIccToSms(message, 0));
501                 return cursor;
502             }
503         }
504 
505         throw new IllegalArgumentException(
506                 "No message in index " + messageIndex + " for subId " + subId);
507     }
508 
509     /**
510      * Gets all the messages in the ICC for a subscription ID.
511      *
512      * @param subId the subscription ID.
513      * @return a cursor listing all the message in the ICC for the subscription ID.
514      */
getAllMessagesFromIcc(int subId)515     private Cursor getAllMessagesFromIcc(int subId) {
516         if (!SubscriptionManager.isValidSubscriptionId(subId)) {
517             throw new IllegalArgumentException("Invalid Subscription ID " + subId);
518         }
519         SmsManager smsManager = SmsManager.getSmsManagerForSubscriptionId(subId);
520         List<SmsMessage> messages;
521 
522         // Use phone app permissions to avoid UID mismatch in AppOpsManager.noteOp() call
523         long token = Binder.clearCallingIdentity();
524         try {
525             // getMessagesFromIcc() returns a zero-based list of valid messages in the ICC.
526             messages = smsManager.getMessagesFromIcc();
527         } finally {
528             Binder.restoreCallingIdentity(token);
529         }
530 
531         final int count = messages.size();
532         MatrixCursor cursor = new MatrixCursor(ICC_COLUMNS, count);
533         for (int i = 0; i < count; i++) {
534             SmsMessage message = messages.get(i);
535             if (message != null) {
536                 cursor.addRow(convertIccToSms(message, i));
537             }
538         }
539         return cursor;
540     }
541 
constructQueryForBox(SQLiteQueryBuilder qb, int type, String smsTable)542     private void constructQueryForBox(SQLiteQueryBuilder qb, int type, String smsTable) {
543         qb.setTables(smsTable);
544 
545         if (type != Sms.MESSAGE_TYPE_ALL) {
546             qb.appendWhere("type=" + type);
547         }
548     }
549 
constructQueryForUndelivered(SQLiteQueryBuilder qb, String smsTable)550     private void constructQueryForUndelivered(SQLiteQueryBuilder qb, String smsTable) {
551         qb.setTables(smsTable);
552 
553         qb.appendWhere("(type=" + Sms.MESSAGE_TYPE_OUTBOX +
554                        " OR type=" + Sms.MESSAGE_TYPE_FAILED +
555                        " OR type=" + Sms.MESSAGE_TYPE_QUEUED + ")");
556     }
557 
558     @Override
getType(Uri url)559     public String getType(Uri url) {
560         switch (url.getPathSegments().size()) {
561         case 0:
562             return VND_ANDROID_DIR_SMS;
563             case 1:
564                 try {
565                     Integer.parseInt(url.getPathSegments().get(0));
566                     return VND_ANDROID_SMS;
567                 } catch (NumberFormatException ex) {
568                     return VND_ANDROID_DIR_SMS;
569                 }
570             case 2:
571                 // TODO: What about "threadID"?
572                 if (url.getPathSegments().get(0).equals("conversations")) {
573                     return VND_ANDROID_SMSCHAT;
574                 } else {
575                     return VND_ANDROID_SMS;
576                 }
577         }
578         return null;
579     }
580 
581     @Override
bulkInsert(@onNull Uri url, @NonNull ContentValues[] values)582     public int bulkInsert(@NonNull Uri url, @NonNull ContentValues[] values) {
583         final int callerUid = Binder.getCallingUid();
584         final UserHandle callerUserHandle = Binder.getCallingUserHandle();
585         final String callerPkg = getCallingPackage();
586         long token = Binder.clearCallingIdentity();
587         try {
588             int messagesInserted = 0;
589             for (ContentValues initialValues : values) {
590                 Uri insertUri = insertInner(url, initialValues, callerUid, callerPkg,
591                         callerUserHandle);
592                 if (insertUri != null) {
593                     messagesInserted++;
594                 }
595             }
596 
597             // The raw table is used by the telephony layer for storing an sms before
598             // sending out a notification that an sms has arrived. We don't want to notify
599             // the default sms app of changes to this table.
600             final boolean notifyIfNotDefault = sURLMatcher.match(url) != SMS_RAW_MESSAGE;
601             notifyChange(notifyIfNotDefault, url, callerPkg);
602             return messagesInserted;
603         } finally {
604             Binder.restoreCallingIdentity(token);
605         }
606     }
607 
608     @Override
insert(Uri url, ContentValues initialValues)609     public Uri insert(Uri url, ContentValues initialValues) {
610         final int callerUid = Binder.getCallingUid();
611         final UserHandle callerUserHandle = Binder.getCallingUserHandle();
612         final String callerPkg = getCallingPackage();
613         long token = Binder.clearCallingIdentity();
614         try {
615             Uri insertUri = insertInner(url, initialValues, callerUid, callerPkg, callerUserHandle);
616 
617             // Skip notifyChange() if insertUri is null
618             if (insertUri != null) {
619                 int match = sURLMatcher.match(url);
620                 // The raw table is used by the telephony layer for storing an sms before sending
621                 // out a notification that an sms has arrived. We don't want to notify the default
622                 // sms app of changes to this table.
623                 final boolean notifyIfNotDefault = match != SMS_RAW_MESSAGE;
624                 notifyChange(notifyIfNotDefault, insertUri, callerPkg);
625             }
626             return insertUri;
627         } finally {
628             Binder.restoreCallingIdentity(token);
629         }
630     }
631 
insertInner(Uri url, ContentValues initialValues, int callerUid, String callerPkg, UserHandle callerUserHandle)632     private Uri insertInner(Uri url, ContentValues initialValues, int callerUid, String callerPkg,
633             UserHandle callerUserHandle) {
634         ContentValues values;
635         long rowID;
636         int type = Sms.MESSAGE_TYPE_ALL;
637 
638         int match = sURLMatcher.match(url);
639         String table = TABLE_SMS;
640 
641         switch (match) {
642             case SMS_ALL:
643                 Integer typeObj = initialValues.getAsInteger(Sms.TYPE);
644                 if (typeObj != null) {
645                     type = typeObj.intValue();
646                 } else {
647                     // default to inbox
648                     type = Sms.MESSAGE_TYPE_INBOX;
649                 }
650                 break;
651 
652             case SMS_INBOX:
653                 type = Sms.MESSAGE_TYPE_INBOX;
654                 break;
655 
656             case SMS_FAILED:
657                 type = Sms.MESSAGE_TYPE_FAILED;
658                 break;
659 
660             case SMS_QUEUED:
661                 type = Sms.MESSAGE_TYPE_QUEUED;
662                 break;
663 
664             case SMS_SENT:
665                 type = Sms.MESSAGE_TYPE_SENT;
666                 break;
667 
668             case SMS_DRAFT:
669                 type = Sms.MESSAGE_TYPE_DRAFT;
670                 break;
671 
672             case SMS_OUTBOX:
673                 type = Sms.MESSAGE_TYPE_OUTBOX;
674                 break;
675 
676             case SMS_RAW_MESSAGE:
677                 table = "raw";
678                 break;
679 
680             case SMS_STATUS_PENDING:
681                 table = "sr_pending";
682                 break;
683 
684             case SMS_ATTACHMENT:
685                 table = "attachments";
686                 break;
687 
688             case SMS_NEW_THREAD_ID:
689                 table = "canonical_addresses";
690                 break;
691 
692             case SMS_ALL_ICC:
693             case SMS_ALL_ICC_SUBID:
694                 int subId;
695                 if (match == SMS_ALL_ICC) {
696                     subId = SmsManager.getDefaultSmsSubscriptionId();
697                 } else {
698                     try {
699                         subId = Integer.parseInt(url.getPathSegments().get(1));
700                     } catch (NumberFormatException e) {
701                         throw new IllegalArgumentException(
702                                 "Wrong path segements for SMS_ALL_ICC_SUBID, uri= " + url);
703                     }
704                 }
705 
706                 if (!TelephonyPermissions.checkSubscriptionAssociatedWithUser(getContext(), subId,
707                     callerUserHandle)) {
708                     TelephonyUtils.showSwitchToManagedProfileDialogIfAppropriate(getContext(),
709                         subId, callerUid, callerPkg);
710                     return null;
711                 }
712 
713                 if (initialValues == null) {
714                     throw new IllegalArgumentException("ContentValues is null");
715                 }
716 
717                 String scAddress = initialValues.getAsString(Sms.SERVICE_CENTER);
718                 String address = initialValues.getAsString(Sms.ADDRESS);
719                 String message = initialValues.getAsString(Sms.BODY);
720                 boolean isRead = true;
721                 Integer obj = initialValues.getAsInteger(Sms.TYPE);
722 
723                 if (obj == null || address == null || message == null) {
724                     throw new IllegalArgumentException("Missing SMS data");
725                 }
726 
727                 type = obj.intValue();
728                 if (!isSupportedType(type)) {
729                     throw new IllegalArgumentException("Unsupported message type= " + type);
730                 }
731                 obj = initialValues.getAsInteger(Sms.READ); // 0: Unread, 1: Read
732                 if (obj != null && obj.intValue() == 0) {
733                     isRead = false;
734                 }
735 
736                 Long date = initialValues.getAsLong(Sms.DATE);
737                 return insertMessageToIcc(subId, scAddress, address, message, type, isRead,
738                         date != null ? date : 0) ? url : null;
739 
740             default:
741                 Log.e(TAG, "Invalid request: " + url);
742                 return null;
743         }
744 
745         SQLiteDatabase db = getWritableDatabase(match);
746 
747         if (table.equals(TABLE_SMS)) {
748             boolean addDate = false;
749             boolean addType = false;
750 
751             // Make sure that the date and type are set
752             if (initialValues == null) {
753                 values = new ContentValues(1);
754                 addDate = true;
755                 addType = true;
756             } else {
757                 values = new ContentValues(initialValues);
758 
759                 if (!initialValues.containsKey(Sms.DATE)) {
760                     addDate = true;
761                 }
762 
763                 if (!initialValues.containsKey(Sms.TYPE)) {
764                     addType = true;
765                 }
766             }
767 
768             if (addDate) {
769                 values.put(Sms.DATE, new Long(System.currentTimeMillis()));
770             }
771 
772             if (addType && (type != Sms.MESSAGE_TYPE_ALL)) {
773                 values.put(Sms.TYPE, Integer.valueOf(type));
774             }
775 
776             // thread_id
777             Long threadId = values.getAsLong(Sms.THREAD_ID);
778             String address = values.getAsString(Sms.ADDRESS);
779 
780             if (((threadId == null) || (threadId == 0)) && (!TextUtils.isEmpty(address))) {
781                 values.put(Sms.THREAD_ID, Threads.getOrCreateThreadId(
782                                    getContext(), address));
783             }
784 
785             // If this message is going in as a draft, it should replace any
786             // other draft messages in the thread.  Just delete all draft
787             // messages with this thread ID.  We could add an OR REPLACE to
788             // the insert below, but we'd have to query to find the old _id
789             // to produce a conflict anyway.
790             if (values.getAsInteger(Sms.TYPE) == Sms.MESSAGE_TYPE_DRAFT) {
791                 db.delete(TABLE_SMS, "thread_id=? AND type=?",
792                         new String[] { values.getAsString(Sms.THREAD_ID),
793                                        Integer.toString(Sms.MESSAGE_TYPE_DRAFT) });
794             }
795 
796             if (type == Sms.MESSAGE_TYPE_INBOX) {
797                 // Look up the person if not already filled in.
798                 if ((values.getAsLong(Sms.PERSON) == null) && (!TextUtils.isEmpty(address))) {
799                     Cursor cursor = null;
800                     Uri uri = Uri.withAppendedPath(Contacts.Phones.CONTENT_FILTER_URL,
801                             Uri.encode(address));
802                     try {
803                         cursor = getContext().getContentResolver().query(
804                                 uri,
805                                 CONTACT_QUERY_PROJECTION,
806                                 null, null, null);
807 
808                         if (cursor != null && cursor.moveToFirst()) {
809                             Long id = Long.valueOf(cursor.getLong(PERSON_ID_COLUMN));
810                             values.put(Sms.PERSON, id);
811                         }
812                     } catch (Exception ex) {
813                         Log.e(TAG, "insert: query contact uri " + uri + " caught ", ex);
814                     } finally {
815                         if (cursor != null) {
816                             cursor.close();
817                         }
818                     }
819                 }
820             } else {
821                 // Mark all non-inbox messages read.
822                 values.put(Sms.READ, ONE);
823             }
824             if (ProviderUtil.shouldSetCreator(values, callerUid)) {
825                 // Only SYSTEM or PHONE can set CREATOR
826                 // If caller is not SYSTEM or PHONE, or SYSTEM or PHONE does not set CREATOR
827                 // set CREATOR using the truth on caller.
828                 // Note: Inferring package name from UID may include unrelated package names
829                 values.put(Sms.CREATOR, callerPkg);
830             }
831         } else {
832             if (initialValues == null) {
833                 values = new ContentValues(1);
834             } else {
835                 values = initialValues;
836             }
837         }
838 
839         // Insert subId value
840         int subId;
841         if (values.containsKey(Telephony.Sms.SUBSCRIPTION_ID)) {
842             subId = values.getAsInteger(Telephony.Sms.SUBSCRIPTION_ID);
843         } else {
844             // TODO (b/256992531): Currently, one sim card is set as default sms subId in work
845             //  profile. Default sms subId should be updated based on user pref.
846             subId = SmsManager.getDefaultSmsSubscriptionId();
847             if (SubscriptionManager.isValidSubscriptionId(subId)) {
848                 values.put(Telephony.Sms.SUBSCRIPTION_ID, subId);
849             }
850         }
851 
852 
853         if (table.equals(TABLE_SMS)) {
854             // Get destination address from values
855             String address = "";
856             if (values.containsKey(Sms.ADDRESS)) {
857                 address = values.getAsString(Sms.ADDRESS);
858             }
859 
860             if (!TelephonyPermissions.checkSubscriptionAssociatedWithUser(getContext(), subId,
861                     callerUserHandle, address)) {
862                 TelephonyUtils.showSwitchToManagedProfileDialogIfAppropriate(getContext(), subId,
863                         callerUid, callerPkg);
864                 return null;
865             }
866         }
867 
868         rowID = db.insert(table, "body", values);
869 
870         // Don't use a trigger for updating the words table because of a bug
871         // in FTS3.  The bug is such that the call to get the last inserted
872         // row is incorrect.
873         if (table == TABLE_SMS) {
874             // Update the words table with a corresponding row.  The words table
875             // allows us to search for words quickly, without scanning the whole
876             // table;
877             ContentValues cv = new ContentValues();
878             cv.put(Telephony.MmsSms.WordsTable.ID, rowID);
879             cv.put(Telephony.MmsSms.WordsTable.INDEXED_TEXT, values.getAsString("body"));
880             cv.put(Telephony.MmsSms.WordsTable.SOURCE_ROW_ID, rowID);
881             cv.put(Telephony.MmsSms.WordsTable.TABLE_ID, 1);
882             cv.put(MmsSms.WordsTable.SUBSCRIPTION_ID, subId);
883             db.insert(TABLE_WORDS, Telephony.MmsSms.WordsTable.INDEXED_TEXT, cv);
884         }
885         if (rowID > 0) {
886             Uri uri = null;
887             if (table == TABLE_SMS) {
888                 uri = Uri.withAppendedPath(Sms.CONTENT_URI, String.valueOf(rowID));
889             } else {
890                 uri = Uri.withAppendedPath(url, String.valueOf(rowID));
891             }
892             if (Log.isLoggable(TAG, Log.VERBOSE)) {
893                 Log.d(TAG, "insert " + uri + " succeeded");
894             }
895             return uri;
896         } else {
897             Log.e(TAG, "insert: failed!");
898         }
899 
900         return null;
901     }
902 
isSupportedType(int messageType)903     private boolean isSupportedType(int messageType) {
904         return (messageType == Sms.MESSAGE_TYPE_INBOX)
905                 || (messageType == Sms.MESSAGE_TYPE_OUTBOX)
906                 || (messageType == Sms.MESSAGE_TYPE_SENT);
907     }
908 
getMessageStatusForIcc(int messageType, boolean isRead)909     private int getMessageStatusForIcc(int messageType, boolean isRead) {
910         if (messageType == Sms.MESSAGE_TYPE_SENT) {
911             return SmsManager.STATUS_ON_ICC_SENT;
912         } else if (messageType == Sms.MESSAGE_TYPE_OUTBOX) {
913             return SmsManager.STATUS_ON_ICC_UNSENT;
914         } else { // Sms.MESSAGE_BOX_INBOX
915             if (isRead) {
916                 return SmsManager.STATUS_ON_ICC_READ;
917             } else {
918                 return SmsManager.STATUS_ON_ICC_UNREAD;
919             }
920         }
921     }
922 
923     /**
924      * Inserts new message to the ICC for a subscription ID.
925      *
926      * @param subId the subscription ID.
927      * @param scAddress the SMSC for this message.
928      * @param address destination or originating address.
929      * @param message the message text.
930      * @param messageType type of the message.
931      * @param isRead ture if the message has been read. Otherwise false.
932      * @param date the date the message was received.
933      * @return true for succeess. Otherwise false.
934      */
insertMessageToIcc(int subId, String scAddress, String address, String message, int messageType, boolean isRead, long date)935     private boolean insertMessageToIcc(int subId, String scAddress, String address, String message,
936             int messageType, boolean isRead, long date) {
937         if (!SubscriptionManager.isValidSubscriptionId(subId)) {
938             throw new IllegalArgumentException("Invalid Subscription ID " + subId);
939         }
940         SmsManager smsManager = SmsManager.getSmsManagerForSubscriptionId(subId);
941 
942         int status = getMessageStatusForIcc(messageType, isRead);
943         SmsMessage.SubmitPdu smsPdu =
944                 SmsMessage.getSmsPdu(subId, status, scAddress, address, message, date);
945 
946         if (smsPdu == null) {
947             throw new IllegalArgumentException("Failed to create SMS PDU");
948         }
949 
950         // Use phone app permissions to avoid UID mismatch in AppOpsManager.noteOp() call.
951         long token = Binder.clearCallingIdentity();
952         try {
953             return smsManager.copyMessageToIcc(
954                     smsPdu.encodedScAddress, smsPdu.encodedMessage, status);
955         } finally {
956             Binder.restoreCallingIdentity(token);
957         }
958     }
959 
960     @Override
delete(Uri url, String where, String[] whereArgs)961     public int delete(Uri url, String where, String[] whereArgs) {
962         final UserHandle callerUserHandle = Binder.getCallingUserHandle();
963         final int callerUid = Binder.getCallingUid();
964         final long token = Binder.clearCallingIdentity();
965 
966         String selectionBySubIds = null;
967         String selectionByEmergencyNumbers = null;
968         try {
969             // Filter SMS based on subId and emergency numbers.
970             selectionBySubIds = ProviderUtil.getSelectionBySubIds(getContext(),
971                     callerUserHandle);
972             selectionByEmergencyNumbers = ProviderUtil
973                     .getSelectionByEmergencyNumbers(getContext());
974         } finally {
975             Binder.restoreCallingIdentity(token);
976         }
977 
978         String filter = "";
979         if (selectionBySubIds == null && selectionByEmergencyNumbers == null) {
980             // No subscriptions associated with user and no emergency numbers
981             filter = null;
982         } else if (selectionBySubIds != null && selectionByEmergencyNumbers != null) {
983             filter = (selectionBySubIds + " OR " + selectionByEmergencyNumbers);
984         } else {
985             filter = selectionBySubIds == null ?
986                     selectionByEmergencyNumbers : selectionBySubIds;
987         }
988 
989         int count;
990         int match = sURLMatcher.match(url);
991         SQLiteDatabase db = getWritableDatabase(match);
992         boolean notifyIfNotDefault = true;
993         switch (match) {
994             case SMS_ALL:
995                 if (filter == null) {
996                     // No subscriptions associated with user and no emergency numbers, return 0.
997                     return 0;
998                 }
999                 where = DatabaseUtils.concatenateWhere(where, filter);
1000                 count = db.delete(TABLE_SMS, where, whereArgs);
1001                 if (count != 0) {
1002                     // Don't update threads unless something changed.
1003                     MmsSmsDatabaseHelper.updateThreads(db, where, whereArgs);
1004                 }
1005                 break;
1006 
1007             case SMS_ALL_ID:
1008                 try {
1009                     int message_id = Integer.parseInt(url.getPathSegments().get(0));
1010                     count = MmsSmsDatabaseHelper.deleteOneSms(db, message_id);
1011                 } catch (Exception e) {
1012                     throw new IllegalArgumentException(
1013                         "Bad message id: " + url.getPathSegments().get(0));
1014                 }
1015                 break;
1016 
1017             case SMS_CONVERSATIONS_ID:
1018                 int threadID;
1019 
1020                 try {
1021                     threadID = Integer.parseInt(url.getPathSegments().get(1));
1022                 } catch (Exception ex) {
1023                     throw new IllegalArgumentException(
1024                             "Bad conversation thread id: "
1025                             + url.getPathSegments().get(1));
1026                 }
1027 
1028                 // delete the messages from the sms table
1029                 where = DatabaseUtils.concatenateWhere("thread_id=" + threadID, where);
1030                 if (filter == null) {
1031                     // No subscriptions associated with user and no emergency numbers, return 0.
1032                     return 0;
1033                 }
1034                 where = DatabaseUtils.concatenateWhere(where, filter);
1035                 count = db.delete(TABLE_SMS, where, whereArgs);
1036                 MmsSmsDatabaseHelper.updateThread(db, threadID);
1037                 break;
1038 
1039             case SMS_RAW_MESSAGE:
1040                 ContentValues cv = new ContentValues();
1041                 cv.put("deleted", 1);
1042                 count = db.update(TABLE_RAW, cv, where, whereArgs);
1043                 if (Log.isLoggable(TAG, Log.VERBOSE)) {
1044                     Log.d(TAG, "delete: num rows marked deleted in raw table: " + count);
1045                 }
1046                 notifyIfNotDefault = false;
1047                 break;
1048 
1049             case SMS_RAW_MESSAGE_PERMANENT_DELETE:
1050                 count = db.delete(TABLE_RAW, where, whereArgs);
1051                 if (Log.isLoggable(TAG, Log.VERBOSE)) {
1052                     Log.d(TAG, "delete: num rows permanently deleted in raw table: " + count);
1053                 }
1054                 notifyIfNotDefault = false;
1055                 break;
1056 
1057             case SMS_STATUS_PENDING:
1058                 if (selectionBySubIds == null) {
1059                     // No subscriptions associated with user, return 0.
1060                     return 0;
1061                 }
1062                 where = DatabaseUtils.concatenateWhere(where, selectionBySubIds);
1063                 count = db.delete("sr_pending", where, whereArgs);
1064                 break;
1065 
1066             case SMS_ALL_ICC:
1067             case SMS_ALL_ICC_SUBID:
1068                 {
1069                     int subId;
1070                     int deletedCnt;
1071                     if (match == SMS_ALL_ICC) {
1072                         subId = SmsManager.getDefaultSmsSubscriptionId();
1073                     } else {
1074                         try {
1075                             subId = Integer.parseInt(url.getPathSegments().get(1));
1076                         } catch (NumberFormatException e) {
1077                             throw new IllegalArgumentException("Wrong path segements, uri= " + url);
1078                         }
1079                     }
1080 
1081                     if (!TelephonyPermissions.checkSubscriptionAssociatedWithUser(getContext(),
1082                             subId, callerUserHandle)) {
1083                         // If subId is not associated with user, return 0.
1084                         return 0;
1085                     }
1086 
1087                     deletedCnt = deleteAllMessagesFromIcc(subId);
1088                     // Notify changes even failure case since there might be some changes should be
1089                     // known.
1090                     getContext()
1091                             .getContentResolver()
1092                             .notifyChange(
1093                                     match == SMS_ALL_ICC ? ICC_URI : ICC_SUBID_URI,
1094                                     null,
1095                                     true,
1096                                     UserHandle.USER_ALL);
1097                     return deletedCnt;
1098                 }
1099 
1100             case SMS_ICC:
1101             case SMS_ICC_SUBID:
1102                 {
1103                     int subId;
1104                     int messageIndex;
1105                     boolean success;
1106                     try {
1107                         if (match == SMS_ICC) {
1108                             subId = SmsManager.getDefaultSmsSubscriptionId();
1109                             messageIndex = Integer.parseInt(url.getPathSegments().get(1));
1110                         } else {
1111                             subId = Integer.parseInt(url.getPathSegments().get(1));
1112                             messageIndex = Integer.parseInt(url.getPathSegments().get(2));
1113                         }
1114                     } catch (NumberFormatException e) {
1115                         throw new IllegalArgumentException("Wrong path segements, uri= " + url);
1116                     }
1117 
1118                     if (!TelephonyPermissions.checkSubscriptionAssociatedWithUser(getContext(),
1119                             subId, callerUserHandle)) {
1120                         // If subId is not associated with user, return 0.
1121                         return 0;
1122                     }
1123 
1124                     success = deleteMessageFromIcc(subId, messageIndex);
1125                     // Notify changes even failure case since there might be some changes should be
1126                     // known.
1127                     getContext()
1128                             .getContentResolver()
1129                             .notifyChange(
1130                                     match == SMS_ICC ? ICC_URI : ICC_SUBID_URI,
1131                                     null,
1132                                     true,
1133                                     UserHandle.USER_ALL);
1134                     return success ? 1 : 0; // return deleted count
1135                 }
1136 
1137             default:
1138                 throw new IllegalArgumentException("Unknown URL");
1139         }
1140 
1141         if (count > 0) {
1142             notifyChange(notifyIfNotDefault, url, getCallingPackage());
1143         }
1144         return count;
1145     }
1146 
1147     /**
1148      * Deletes the message at index from the ICC for a subscription ID.
1149      *
1150      * @param subId the subscription ID.
1151      * @param messageIndex the message index of the message in the ICC (1-based index).
1152      * @return true for succeess. Otherwise false.
1153      */
deleteMessageFromIcc(int subId, int messageIndex)1154     private boolean deleteMessageFromIcc(int subId, int messageIndex) {
1155         if (!SubscriptionManager.isValidSubscriptionId(subId)) {
1156             throw new IllegalArgumentException("Invalid Subscription ID " + subId);
1157         }
1158         SmsManager smsManager = SmsManager.getSmsManagerForSubscriptionId(subId);
1159 
1160         // Use phone app permissions to avoid UID mismatch in AppOpsManager.noteOp() call.
1161         long token = Binder.clearCallingIdentity();
1162         try {
1163             return smsManager.deleteMessageFromIcc(messageIndex);
1164         } finally {
1165             Binder.restoreCallingIdentity(token);
1166         }
1167     }
1168 
1169     /**
1170      * Deletes all the messages from the ICC for a subscription ID.
1171      *
1172      * @param subId the subscription ID.
1173      * @return return deleted messaegs count.
1174      */
deleteAllMessagesFromIcc(int subId)1175     private int deleteAllMessagesFromIcc(int subId) {
1176         if (!SubscriptionManager.isValidSubscriptionId(subId)) {
1177             throw new IllegalArgumentException("Invalid Subscription ID " + subId);
1178         }
1179         SmsManager smsManager = SmsManager.getSmsManagerForSubscriptionId(subId);
1180 
1181         // Use phone app permissions to avoid UID mismatch in AppOpsManager.noteOp() call.
1182         long token = Binder.clearCallingIdentity();
1183         try {
1184             int deletedCnt = 0;
1185             int maxIndex = smsManager.getSmsCapacityOnIcc();
1186             // messageIndex is 1-based index of the message in the ICC.
1187             for (int messageIndex = 1; messageIndex <= maxIndex; messageIndex++) {
1188                 if (smsManager.deleteMessageFromIcc(messageIndex)) {
1189                     deletedCnt++;
1190                 } else {
1191                     Log.e(TAG, "Fail to delete SMS at index " + messageIndex
1192                             + " for subId " + subId);
1193                 }
1194             }
1195             return deletedCnt;
1196         } finally {
1197             Binder.restoreCallingIdentity(token);
1198         }
1199     }
1200 
1201     @Override
update(Uri url, ContentValues values, String where, String[] whereArgs)1202     public int update(Uri url, ContentValues values, String where, String[] whereArgs) {
1203         final int callerUid = Binder.getCallingUid();
1204         final UserHandle callerUserHandle = Binder.getCallingUserHandle();
1205         final String callerPkg = getCallingPackage();
1206         int count = 0;
1207         String table = TABLE_SMS;
1208         String extraWhere = null;
1209         boolean notifyIfNotDefault = true;
1210         int match = sURLMatcher.match(url);
1211         SQLiteDatabase db = getWritableDatabase(match);
1212 
1213         switch (match) {
1214             case SMS_RAW_MESSAGE:
1215                 table = TABLE_RAW;
1216                 notifyIfNotDefault = false;
1217                 break;
1218 
1219             case SMS_STATUS_PENDING:
1220                 table = TABLE_SR_PENDING;
1221                 break;
1222 
1223             case SMS_ALL:
1224             case SMS_FAILED:
1225             case SMS_QUEUED:
1226             case SMS_INBOX:
1227             case SMS_SENT:
1228             case SMS_DRAFT:
1229             case SMS_OUTBOX:
1230             case SMS_CONVERSATIONS:
1231                 break;
1232 
1233             case SMS_ALL_ID:
1234                 extraWhere = "_id=" + url.getPathSegments().get(0);
1235                 break;
1236 
1237             case SMS_INBOX_ID:
1238             case SMS_FAILED_ID:
1239             case SMS_SENT_ID:
1240             case SMS_DRAFT_ID:
1241             case SMS_OUTBOX_ID:
1242                 extraWhere = "_id=" + url.getPathSegments().get(1);
1243                 break;
1244 
1245             case SMS_CONVERSATIONS_ID: {
1246                 String threadId = url.getPathSegments().get(1);
1247 
1248                 try {
1249                     Integer.parseInt(threadId);
1250                 } catch (Exception ex) {
1251                     Log.e(TAG, "Bad conversation thread id: " + threadId);
1252                     break;
1253                 }
1254 
1255                 extraWhere = "thread_id=" + threadId;
1256                 break;
1257             }
1258 
1259             case SMS_STATUS_ID:
1260                 extraWhere = "_id=" + url.getPathSegments().get(1);
1261                 break;
1262 
1263             default:
1264                 throw new UnsupportedOperationException(
1265                         "URI " + url + " not supported");
1266         }
1267 
1268         if (table.equals(TABLE_SMS) && ProviderUtil.shouldRemoveCreator(values, callerUid)) {
1269             // CREATOR should not be changed by non-SYSTEM/PHONE apps
1270             Log.w(TAG, callerPkg + " tries to update CREATOR");
1271             values.remove(Sms.CREATOR);
1272         }
1273 
1274         final long token = Binder.clearCallingIdentity();
1275         String selectionBySubIds = null;
1276         String selectionByEmergencyNumbers = null;
1277         try {
1278             // Filter SMS based on subId and emergency numbers.
1279             selectionBySubIds = ProviderUtil.getSelectionBySubIds(getContext(),
1280                     callerUserHandle);
1281             if (table.equals(TABLE_SMS)) {
1282                 selectionByEmergencyNumbers = ProviderUtil
1283                         .getSelectionByEmergencyNumbers(getContext());
1284             }
1285         } finally {
1286             Binder.restoreCallingIdentity(token);
1287         }
1288 
1289         if (table.equals(TABLE_SMS)) {
1290             if (selectionBySubIds == null && selectionByEmergencyNumbers == null) {
1291                 // No subscriptions associated with user and no emergency numbers, return 0.
1292                 return 0;
1293             }
1294         } else {
1295             if (selectionBySubIds == null) {
1296                 // No subscriptions associated with user, return 0.
1297                 return 0;
1298             }
1299         }
1300 
1301 
1302         String filter = "";
1303         if (selectionBySubIds != null && selectionByEmergencyNumbers != null) {
1304             filter = (selectionBySubIds + " OR " + selectionByEmergencyNumbers);
1305         } else {
1306             filter = selectionBySubIds == null ?
1307                     selectionByEmergencyNumbers : selectionBySubIds;
1308         }
1309         where = DatabaseUtils.concatenateWhere(where, filter);
1310 
1311         where = DatabaseUtils.concatenateWhere(where, extraWhere);
1312         count = db.update(table, values, where, whereArgs);
1313 
1314         if (count > 0) {
1315             if (Log.isLoggable(TAG, Log.VERBOSE)) {
1316                 Log.d(TAG, "update " + url + " succeeded");
1317             }
1318             notifyChange(notifyIfNotDefault, url, callerPkg);
1319         }
1320         return count;
1321     }
1322 
notifyChange(boolean notifyIfNotDefault, Uri uri, final String callingPackage)1323     private void notifyChange(boolean notifyIfNotDefault, Uri uri, final String callingPackage) {
1324         final Context context = getContext();
1325         ContentResolver cr = context.getContentResolver();
1326         cr.notifyChange(uri, null, true, UserHandle.USER_ALL);
1327         cr.notifyChange(MmsSms.CONTENT_URI, null, true, UserHandle.USER_ALL);
1328         cr.notifyChange(Uri.parse("content://mms-sms/conversations/"), null, true,
1329                 UserHandle.USER_ALL);
1330         if (notifyIfNotDefault) {
1331             ProviderUtil.notifyIfNotDefaultSmsApp(uri, callingPackage, context);
1332         }
1333     }
1334 
1335     // Db open helper for tables stored in CE(Credential Encrypted) storage.
1336     @VisibleForTesting
1337     public SQLiteOpenHelper mCeOpenHelper;
1338     // Db open helper for tables stored in DE(Device Encrypted) storage. It's currently only used
1339     // to store raw table.
1340     @VisibleForTesting
1341     public SQLiteOpenHelper mDeOpenHelper;
1342 
1343     private final static String TAG = "SmsProvider";
1344     private final static String VND_ANDROID_SMS = "vnd.android.cursor.item/sms";
1345     private final static String VND_ANDROID_SMSCHAT =
1346             "vnd.android.cursor.item/sms-chat";
1347     private final static String VND_ANDROID_DIR_SMS =
1348             "vnd.android.cursor.dir/sms";
1349 
1350     private static final String[] sIDProjection = new String[] { "_id" };
1351 
1352     private static final int SMS_ALL = 0;
1353     private static final int SMS_ALL_ID = 1;
1354     private static final int SMS_INBOX = 2;
1355     private static final int SMS_INBOX_ID = 3;
1356     private static final int SMS_SENT = 4;
1357     private static final int SMS_SENT_ID = 5;
1358     private static final int SMS_DRAFT = 6;
1359     private static final int SMS_DRAFT_ID = 7;
1360     private static final int SMS_OUTBOX = 8;
1361     private static final int SMS_OUTBOX_ID = 9;
1362     private static final int SMS_CONVERSATIONS = 10;
1363     private static final int SMS_CONVERSATIONS_ID = 11;
1364     private static final int SMS_RAW_MESSAGE = 15;
1365     private static final int SMS_ATTACHMENT = 16;
1366     private static final int SMS_ATTACHMENT_ID = 17;
1367     private static final int SMS_NEW_THREAD_ID = 18;
1368     private static final int SMS_QUERY_THREAD_ID = 19;
1369     private static final int SMS_STATUS_ID = 20;
1370     private static final int SMS_STATUS_PENDING = 21;
1371     private static final int SMS_ALL_ICC = 22;
1372     private static final int SMS_ICC = 23;
1373     private static final int SMS_FAILED = 24;
1374     private static final int SMS_FAILED_ID = 25;
1375     private static final int SMS_QUEUED = 26;
1376     private static final int SMS_UNDELIVERED = 27;
1377     private static final int SMS_RAW_MESSAGE_PERMANENT_DELETE = 28;
1378     private static final int SMS_ALL_ICC_SUBID = 29;
1379     private static final int SMS_ICC_SUBID = 30;
1380 
1381     private static final UriMatcher sURLMatcher =
1382             new UriMatcher(UriMatcher.NO_MATCH);
1383 
1384     static {
1385         sURLMatcher.addURI("sms", null, SMS_ALL);
1386         sURLMatcher.addURI("sms", "#", SMS_ALL_ID);
1387         sURLMatcher.addURI("sms", "inbox", SMS_INBOX);
1388         sURLMatcher.addURI("sms", "inbox/#", SMS_INBOX_ID);
1389         sURLMatcher.addURI("sms", "sent", SMS_SENT);
1390         sURLMatcher.addURI("sms", "sent/#", SMS_SENT_ID);
1391         sURLMatcher.addURI("sms", "draft", SMS_DRAFT);
1392         sURLMatcher.addURI("sms", "draft/#", SMS_DRAFT_ID);
1393         sURLMatcher.addURI("sms", "outbox", SMS_OUTBOX);
1394         sURLMatcher.addURI("sms", "outbox/#", SMS_OUTBOX_ID);
1395         sURLMatcher.addURI("sms", "undelivered", SMS_UNDELIVERED);
1396         sURLMatcher.addURI("sms", "failed", SMS_FAILED);
1397         sURLMatcher.addURI("sms", "failed/#", SMS_FAILED_ID);
1398         sURLMatcher.addURI("sms", "queued", SMS_QUEUED);
1399         sURLMatcher.addURI("sms", "conversations", SMS_CONVERSATIONS);
1400         sURLMatcher.addURI("sms", "conversations/#", SMS_CONVERSATIONS_ID);
1401         sURLMatcher.addURI("sms", "raw", SMS_RAW_MESSAGE);
1402         sURLMatcher.addURI("sms", "raw/permanentDelete", SMS_RAW_MESSAGE_PERMANENT_DELETE);
1403         sURLMatcher.addURI("sms", "attachments", SMS_ATTACHMENT);
1404         sURLMatcher.addURI("sms", "attachments/#", SMS_ATTACHMENT_ID);
1405         sURLMatcher.addURI("sms", "threadID", SMS_NEW_THREAD_ID);
1406         sURLMatcher.addURI("sms", "threadID/#", SMS_QUERY_THREAD_ID);
1407         sURLMatcher.addURI("sms", "status/#", SMS_STATUS_ID);
1408         sURLMatcher.addURI("sms", "sr_pending", SMS_STATUS_PENDING);
1409         sURLMatcher.addURI("sms", "icc", SMS_ALL_ICC);
1410         sURLMatcher.addURI("sms", "icc/#", SMS_ICC);
1411         sURLMatcher.addURI("sms", "icc_subId/#", SMS_ALL_ICC_SUBID);
1412         sURLMatcher.addURI("sms", "icc_subId/#/#", SMS_ICC_SUBID);
1413         //we keep these for not breaking old applications
1414         sURLMatcher.addURI("sms", "sim", SMS_ALL_ICC);
1415         sURLMatcher.addURI("sms", "sim/#", SMS_ICC);
1416     }
1417 
1418     /**
1419      * These methods can be overridden in a subclass for testing SmsProvider using an
1420      * in-memory database.
1421      */
getReadableDatabase(int match)1422     SQLiteDatabase getReadableDatabase(int match) {
1423         return getDBOpenHelper(match).getReadableDatabase();
1424     }
1425 
getWritableDatabase(int match)1426     SQLiteDatabase getWritableDatabase(int match) {
1427         return  getDBOpenHelper(match).getWritableDatabase();
1428     }
1429 
1430     private BroadcastReceiver mUserIntentReceiver = new BroadcastReceiver() {
1431         @Override
1432         public void onReceive(Context context, Intent intent) {
1433             switch (intent.getAction()) {
1434                 case Intent.ACTION_USER_REMOVED:
1435                     UserHandle userToBeRemoved  = intent.getParcelableExtra(Intent.EXTRA_USER,
1436                             UserHandle.class);
1437                     UserManager userManager = context.getSystemService(UserManager.class);
1438                     if ((userToBeRemoved == null) || (userManager == null) ||
1439                             (!userManager.isManagedProfile(userToBeRemoved.getIdentifier()))) {
1440                         // Do not delete SMS if removed profile is not managed profile.
1441                         return;
1442                     }
1443                     Log.d(TAG, "Received ACTION_USER_REMOVED for managed profile - Deleting SMS.");
1444 
1445                     // Deleting SMS related to managed profile.
1446                     Uri uri = Sms.CONTENT_URI;
1447                     int match = sURLMatcher.match(uri);
1448                     SQLiteDatabase db = getWritableDatabase(match);
1449 
1450                     final long token = Binder.clearCallingIdentity();
1451                     String selectionBySubIds;
1452                     try {
1453                         // Filter SMS based on subId.
1454                         selectionBySubIds = ProviderUtil.getSelectionBySubIds(getContext(),
1455                                 userToBeRemoved);
1456                     } finally {
1457                         Binder.restoreCallingIdentity(token);
1458                     }
1459                     if (selectionBySubIds == null) {
1460                         // No subscriptions associated with user, return.
1461                         return;
1462                     }
1463 
1464                     int count = db.delete(TABLE_SMS, selectionBySubIds, null);
1465                     if (count != 0) {
1466                         // Don't update threads unless something changed.
1467                         MmsSmsDatabaseHelper.updateThreads(db, selectionBySubIds, null);
1468                         notifyChange(true, uri, getCallingPackage());
1469                     }
1470                     break;
1471             }
1472         }
1473     };
1474 }
1475