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