• 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.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.TextBasedSmsColumns;
40 import android.provider.Telephony.Threads;
41 import android.telephony.SmsManager;
42 import android.telephony.SmsMessage;
43 import android.text.TextUtils;
44 import android.util.Log;
45 
46 import com.android.internal.annotations.VisibleForTesting;
47 
48 import java.util.ArrayList;
49 import java.util.HashMap;
50 
51 public class SmsProvider extends ContentProvider {
52     private static final Uri NOTIFICATION_URI = Uri.parse("content://sms");
53     private static final Uri ICC_URI = Uri.parse("content://sms/icc");
54     static final String TABLE_SMS = "sms";
55     static final String TABLE_RAW = "raw";
56     private static final String TABLE_SR_PENDING = "sr_pending";
57     private static final String TABLE_WORDS = "words";
58     static final String VIEW_SMS_RESTRICTED = "sms_restricted";
59 
60     private static final Integer ONE = Integer.valueOf(1);
61 
62     private static final String[] CONTACT_QUERY_PROJECTION =
63             new String[] { Contacts.Phones.PERSON_ID };
64     private static final int PERSON_ID_COLUMN = 0;
65 
66     /** Delete any raw messages or message segments marked deleted that are older than an hour. */
67     static final long RAW_MESSAGE_EXPIRE_AGE_MS = (long) (60 * 60 * 1000);
68 
69     /**
70      * These are the columns that are available when reading SMS
71      * messages from the ICC.  Columns whose names begin with "is_"
72      * have either "true" or "false" as their values.
73      */
74     private final static String[] ICC_COLUMNS = new String[] {
75         // N.B.: These columns must appear in the same order as the
76         // calls to add appear in convertIccToSms.
77         "service_center_address",       // getServiceCenterAddress
78         "address",                      // getDisplayOriginatingAddress
79         "message_class",                // getMessageClass
80         "body",                         // getDisplayMessageBody
81         "date",                         // getTimestampMillis
82         "status",                       // getStatusOnIcc
83         "index_on_icc",                 // getIndexOnIcc
84         "is_status_report",             // isStatusReportMessage
85         "transport_type",               // Always "sms".
86         "type",                         // Always MESSAGE_TYPE_ALL.
87         "locked",                       // Always 0 (false).
88         "error_code",                   // Always 0
89         "_id"
90     };
91 
92     @Override
onCreate()93     public boolean onCreate() {
94         setAppOps(AppOpsManager.OP_READ_SMS, AppOpsManager.OP_WRITE_SMS);
95         // So we have two database files. One in de, one in ce. Here only "raw" table is in
96         // mDeOpenHelper, other tables are all in mCeOpenHelper.
97         mDeOpenHelper = MmsSmsDatabaseHelper.getInstanceForDe(getContext());
98         mCeOpenHelper = MmsSmsDatabaseHelper.getInstanceForCe(getContext());
99         TelephonyBackupAgent.DeferredSmsMmsRestoreService.startIfFilesExist(getContext());
100         return true;
101     }
102 
103     /**
104      * Return the proper view of "sms" table for the current access status.
105      *
106      * @param accessRestricted If the access is restricted
107      * @return the table/view name of the "sms" data
108      */
getSmsTable(boolean accessRestricted)109     public static String getSmsTable(boolean accessRestricted) {
110         return accessRestricted ? VIEW_SMS_RESTRICTED : TABLE_SMS;
111     }
112 
113     @Override
query(Uri url, String[] projectionIn, String selection, String[] selectionArgs, String sort)114     public Cursor query(Uri url, String[] projectionIn, String selection,
115             String[] selectionArgs, String sort) {
116         // First check if a restricted view of the "sms" table should be used based on the
117         // caller's identity. Only system, phone or the default sms app can have full access
118         // of sms data. For other apps, we present a restricted view which only contains sent
119         // or received messages.
120         final boolean accessRestricted = ProviderUtil.isAccessRestricted(
121                 getContext(), getCallingPackage(), Binder.getCallingUid());
122         final String smsTable = getSmsTable(accessRestricted);
123         SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
124 
125         // Generate the body of the query.
126         int match = sURLMatcher.match(url);
127         SQLiteDatabase db = getReadableDatabase(match);
128         switch (match) {
129             case SMS_ALL:
130                 constructQueryForBox(qb, Sms.MESSAGE_TYPE_ALL, smsTable);
131                 break;
132 
133             case SMS_UNDELIVERED:
134                 constructQueryForUndelivered(qb, smsTable);
135                 break;
136 
137             case SMS_FAILED:
138                 constructQueryForBox(qb, Sms.MESSAGE_TYPE_FAILED, smsTable);
139                 break;
140 
141             case SMS_QUEUED:
142                 constructQueryForBox(qb, Sms.MESSAGE_TYPE_QUEUED, smsTable);
143                 break;
144 
145             case SMS_INBOX:
146                 constructQueryForBox(qb, Sms.MESSAGE_TYPE_INBOX, smsTable);
147                 break;
148 
149             case SMS_SENT:
150                 constructQueryForBox(qb, Sms.MESSAGE_TYPE_SENT, smsTable);
151                 break;
152 
153             case SMS_DRAFT:
154                 constructQueryForBox(qb, Sms.MESSAGE_TYPE_DRAFT, smsTable);
155                 break;
156 
157             case SMS_OUTBOX:
158                 constructQueryForBox(qb, Sms.MESSAGE_TYPE_OUTBOX, smsTable);
159                 break;
160 
161             case SMS_ALL_ID:
162                 qb.setTables(smsTable);
163                 qb.appendWhere("(_id = " + url.getPathSegments().get(0) + ")");
164                 break;
165 
166             case SMS_INBOX_ID:
167             case SMS_FAILED_ID:
168             case SMS_SENT_ID:
169             case SMS_DRAFT_ID:
170             case SMS_OUTBOX_ID:
171                 qb.setTables(smsTable);
172                 qb.appendWhere("(_id = " + url.getPathSegments().get(1) + ")");
173                 break;
174 
175             case SMS_CONVERSATIONS_ID:
176                 int threadID;
177 
178                 try {
179                     threadID = Integer.parseInt(url.getPathSegments().get(1));
180                     if (Log.isLoggable(TAG, Log.VERBOSE)) {
181                         Log.d(TAG, "query conversations: threadID=" + threadID);
182                     }
183                 }
184                 catch (Exception ex) {
185                     Log.e(TAG,
186                           "Bad conversation thread id: "
187                           + url.getPathSegments().get(1));
188                     return null;
189                 }
190 
191                 qb.setTables(smsTable);
192                 qb.appendWhere("thread_id = " + threadID);
193                 break;
194 
195             case SMS_CONVERSATIONS:
196                 qb.setTables(smsTable + ", "
197                         + "(SELECT thread_id AS group_thread_id, "
198                         + "MAX(date) AS group_date, "
199                         + "COUNT(*) AS msg_count "
200                         + "FROM " + smsTable + " "
201                         + "GROUP BY thread_id) AS groups");
202                 qb.appendWhere(smsTable + ".thread_id=groups.group_thread_id"
203                         + " AND " + smsTable + ".date=groups.group_date");
204                 final HashMap<String, String> projectionMap = new HashMap<>();
205                 projectionMap.put(Sms.Conversations.SNIPPET,
206                         smsTable + ".body AS snippet");
207                 projectionMap.put(Sms.Conversations.THREAD_ID,
208                         smsTable + ".thread_id AS thread_id");
209                 projectionMap.put(Sms.Conversations.MESSAGE_COUNT,
210                         "groups.msg_count AS msg_count");
211                 projectionMap.put("delta", null);
212                 qb.setProjectionMap(projectionMap);
213                 break;
214 
215             case SMS_RAW_MESSAGE:
216                 // before querying purge old entries with deleted = 1
217                 purgeDeletedMessagesInRawTable(db);
218                 qb.setTables("raw");
219                 break;
220 
221             case SMS_STATUS_PENDING:
222                 qb.setTables("sr_pending");
223                 break;
224 
225             case SMS_ATTACHMENT:
226                 qb.setTables("attachments");
227                 break;
228 
229             case SMS_ATTACHMENT_ID:
230                 qb.setTables("attachments");
231                 qb.appendWhere(
232                         "(sms_id = " + url.getPathSegments().get(1) + ")");
233                 break;
234 
235             case SMS_QUERY_THREAD_ID:
236                 qb.setTables("canonical_addresses");
237                 if (projectionIn == null) {
238                     projectionIn = sIDProjection;
239                 }
240                 break;
241 
242             case SMS_STATUS_ID:
243                 qb.setTables(smsTable);
244                 qb.appendWhere("(_id = " + url.getPathSegments().get(1) + ")");
245                 break;
246 
247             case SMS_ALL_ICC:
248                 return getAllMessagesFromIcc();
249 
250             case SMS_ICC:
251                 String messageIndexString = url.getPathSegments().get(1);
252 
253                 return getSingleMessageFromIcc(messageIndexString);
254 
255             default:
256                 Log.e(TAG, "Invalid request: " + url);
257                 return null;
258         }
259 
260         String orderBy = null;
261 
262         if (!TextUtils.isEmpty(sort)) {
263             orderBy = sort;
264         } else if (qb.getTables().equals(smsTable)) {
265             orderBy = Sms.DEFAULT_SORT_ORDER;
266         }
267 
268         Cursor ret = qb.query(db, projectionIn, selection, selectionArgs,
269                               null, null, orderBy);
270 
271         // TODO: Since the URLs are a mess, always use content://sms
272         ret.setNotificationUri(getContext().getContentResolver(),
273                 NOTIFICATION_URI);
274         return ret;
275     }
276 
purgeDeletedMessagesInRawTable(SQLiteDatabase db)277     private void purgeDeletedMessagesInRawTable(SQLiteDatabase db) {
278         long oldTimestamp = System.currentTimeMillis() - RAW_MESSAGE_EXPIRE_AGE_MS;
279         int num = db.delete(TABLE_RAW, "deleted = 1 AND date < " + oldTimestamp, null);
280         if (Log.isLoggable(TAG, Log.VERBOSE)) {
281             Log.d(TAG, "purgeDeletedMessagesInRawTable: num rows older than " + oldTimestamp +
282                     " purged: " + num);
283         }
284     }
285 
getDBOpenHelper(int match)286     private SQLiteOpenHelper getDBOpenHelper(int match) {
287         // Raw table is stored on de database. Other tables are stored in ce database.
288         if (match == SMS_RAW_MESSAGE || match == SMS_RAW_MESSAGE_PERMANENT_DELETE) {
289             return mDeOpenHelper;
290         }
291         return mCeOpenHelper;
292     }
293 
convertIccToSms(SmsMessage message, int id)294     private Object[] convertIccToSms(SmsMessage message, int id) {
295         // N.B.: These calls must appear in the same order as the
296         // columns appear in ICC_COLUMNS.
297         Object[] row = new Object[13];
298         row[0] = message.getServiceCenterAddress();
299         row[1] = message.getDisplayOriginatingAddress();
300         row[2] = String.valueOf(message.getMessageClass());
301         row[3] = message.getDisplayMessageBody();
302         row[4] = message.getTimestampMillis();
303         row[5] = Sms.STATUS_NONE;
304         row[6] = message.getIndexOnIcc();
305         row[7] = message.isStatusReportMessage();
306         row[8] = "sms";
307         row[9] = TextBasedSmsColumns.MESSAGE_TYPE_ALL;
308         row[10] = 0;      // locked
309         row[11] = 0;      // error_code
310         row[12] = id;
311         return row;
312     }
313 
314     /**
315      * Return a Cursor containing just one message from the ICC.
316      */
getSingleMessageFromIcc(String messageIndexString)317     private Cursor getSingleMessageFromIcc(String messageIndexString) {
318         int messageIndex = -1;
319         try {
320             messageIndex = Integer.parseInt(messageIndexString);
321         } catch (NumberFormatException exception) {
322             throw new IllegalArgumentException("Bad SMS ICC ID: " + messageIndexString);
323         }
324         ArrayList<SmsMessage> messages;
325         final SmsManager smsManager = SmsManager.getDefault();
326         // Use phone id to avoid AppOps uid mismatch in telephony
327         long token = Binder.clearCallingIdentity();
328         try {
329             messages = smsManager.getAllMessagesFromIcc();
330         } finally {
331             Binder.restoreCallingIdentity(token);
332         }
333         if (messages == null) {
334             throw new IllegalArgumentException("ICC message not retrieved");
335         }
336         final SmsMessage message = messages.get(messageIndex);
337         if (message == null) {
338             throw new IllegalArgumentException(
339                     "Message not retrieved. ID: " + messageIndexString);
340         }
341         MatrixCursor cursor = new MatrixCursor(ICC_COLUMNS, 1);
342         cursor.addRow(convertIccToSms(message, 0));
343         return withIccNotificationUri(cursor);
344     }
345 
346     /**
347      * Return a Cursor listing all the messages stored on the ICC.
348      */
getAllMessagesFromIcc()349     private Cursor getAllMessagesFromIcc() {
350         SmsManager smsManager = SmsManager.getDefault();
351         ArrayList<SmsMessage> messages;
352 
353         // use phone app permissions to avoid UID mismatch in AppOpsManager.noteOp() call
354         long token = Binder.clearCallingIdentity();
355         try {
356             messages = smsManager.getAllMessagesFromIcc();
357         } finally {
358             Binder.restoreCallingIdentity(token);
359         }
360 
361         final int count = messages.size();
362         MatrixCursor cursor = new MatrixCursor(ICC_COLUMNS, count);
363         for (int i = 0; i < count; i++) {
364             SmsMessage message = messages.get(i);
365             if (message != null) {
366                 cursor.addRow(convertIccToSms(message, i));
367             }
368         }
369         return withIccNotificationUri(cursor);
370     }
371 
withIccNotificationUri(Cursor cursor)372     private Cursor withIccNotificationUri(Cursor cursor) {
373         cursor.setNotificationUri(getContext().getContentResolver(), ICC_URI);
374         return cursor;
375     }
376 
constructQueryForBox(SQLiteQueryBuilder qb, int type, String smsTable)377     private void constructQueryForBox(SQLiteQueryBuilder qb, int type, String smsTable) {
378         qb.setTables(smsTable);
379 
380         if (type != Sms.MESSAGE_TYPE_ALL) {
381             qb.appendWhere("type=" + type);
382         }
383     }
384 
constructQueryForUndelivered(SQLiteQueryBuilder qb, String smsTable)385     private void constructQueryForUndelivered(SQLiteQueryBuilder qb, String smsTable) {
386         qb.setTables(smsTable);
387 
388         qb.appendWhere("(type=" + Sms.MESSAGE_TYPE_OUTBOX +
389                        " OR type=" + Sms.MESSAGE_TYPE_FAILED +
390                        " OR type=" + Sms.MESSAGE_TYPE_QUEUED + ")");
391     }
392 
393     @Override
getType(Uri url)394     public String getType(Uri url) {
395         switch (url.getPathSegments().size()) {
396         case 0:
397             return VND_ANDROID_DIR_SMS;
398             case 1:
399                 try {
400                     Integer.parseInt(url.getPathSegments().get(0));
401                     return VND_ANDROID_SMS;
402                 } catch (NumberFormatException ex) {
403                     return VND_ANDROID_DIR_SMS;
404                 }
405             case 2:
406                 // TODO: What about "threadID"?
407                 if (url.getPathSegments().get(0).equals("conversations")) {
408                     return VND_ANDROID_SMSCHAT;
409                 } else {
410                     return VND_ANDROID_SMS;
411                 }
412         }
413         return null;
414     }
415 
416     @Override
bulkInsert(@onNull Uri url, @NonNull ContentValues[] values)417     public int bulkInsert(@NonNull Uri url, @NonNull ContentValues[] values) {
418         final int callerUid = Binder.getCallingUid();
419         final String callerPkg = getCallingPackage();
420         long token = Binder.clearCallingIdentity();
421         try {
422             int messagesInserted = 0;
423             for (ContentValues initialValues : values) {
424                 Uri insertUri = insertInner(url, initialValues, callerUid, callerPkg);
425                 if (insertUri != null) {
426                     messagesInserted++;
427                 }
428             }
429 
430             // The raw table is used by the telephony layer for storing an sms before
431             // sending out a notification that an sms has arrived. We don't want to notify
432             // the default sms app of changes to this table.
433             final boolean notifyIfNotDefault = sURLMatcher.match(url) != SMS_RAW_MESSAGE;
434             notifyChange(notifyIfNotDefault, url, callerPkg);
435             return messagesInserted;
436         } finally {
437             Binder.restoreCallingIdentity(token);
438         }
439     }
440 
441     @Override
insert(Uri url, ContentValues initialValues)442     public Uri insert(Uri url, ContentValues initialValues) {
443         final int callerUid = Binder.getCallingUid();
444         final String callerPkg = getCallingPackage();
445         long token = Binder.clearCallingIdentity();
446         try {
447             Uri insertUri = insertInner(url, initialValues, callerUid, callerPkg);
448 
449             // The raw table is used by the telephony layer for storing an sms before
450             // sending out a notification that an sms has arrived. We don't want to notify
451             // the default sms app of changes to this table.
452             final boolean notifyIfNotDefault = sURLMatcher.match(url) != SMS_RAW_MESSAGE;
453             notifyChange(notifyIfNotDefault, insertUri, callerPkg);
454             return insertUri;
455         } finally {
456             Binder.restoreCallingIdentity(token);
457         }
458     }
459 
insertInner(Uri url, ContentValues initialValues, int callerUid, String callerPkg)460     private Uri insertInner(Uri url, ContentValues initialValues, int callerUid, String callerPkg) {
461         ContentValues values;
462         long rowID;
463         int type = Sms.MESSAGE_TYPE_ALL;
464 
465         int match = sURLMatcher.match(url);
466         String table = TABLE_SMS;
467 
468         switch (match) {
469             case SMS_ALL:
470                 Integer typeObj = initialValues.getAsInteger(Sms.TYPE);
471                 if (typeObj != null) {
472                     type = typeObj.intValue();
473                 } else {
474                     // default to inbox
475                     type = Sms.MESSAGE_TYPE_INBOX;
476                 }
477                 break;
478 
479             case SMS_INBOX:
480                 type = Sms.MESSAGE_TYPE_INBOX;
481                 break;
482 
483             case SMS_FAILED:
484                 type = Sms.MESSAGE_TYPE_FAILED;
485                 break;
486 
487             case SMS_QUEUED:
488                 type = Sms.MESSAGE_TYPE_QUEUED;
489                 break;
490 
491             case SMS_SENT:
492                 type = Sms.MESSAGE_TYPE_SENT;
493                 break;
494 
495             case SMS_DRAFT:
496                 type = Sms.MESSAGE_TYPE_DRAFT;
497                 break;
498 
499             case SMS_OUTBOX:
500                 type = Sms.MESSAGE_TYPE_OUTBOX;
501                 break;
502 
503             case SMS_RAW_MESSAGE:
504                 table = "raw";
505                 break;
506 
507             case SMS_STATUS_PENDING:
508                 table = "sr_pending";
509                 break;
510 
511             case SMS_ATTACHMENT:
512                 table = "attachments";
513                 break;
514 
515             case SMS_NEW_THREAD_ID:
516                 table = "canonical_addresses";
517                 break;
518 
519             default:
520                 Log.e(TAG, "Invalid request: " + url);
521                 return null;
522         }
523 
524         SQLiteDatabase db = getWritableDatabase(match);
525 
526         if (table.equals(TABLE_SMS)) {
527             boolean addDate = false;
528             boolean addType = false;
529 
530             // Make sure that the date and type are set
531             if (initialValues == null) {
532                 values = new ContentValues(1);
533                 addDate = true;
534                 addType = true;
535             } else {
536                 values = new ContentValues(initialValues);
537 
538                 if (!initialValues.containsKey(Sms.DATE)) {
539                     addDate = true;
540                 }
541 
542                 if (!initialValues.containsKey(Sms.TYPE)) {
543                     addType = true;
544                 }
545             }
546 
547             if (addDate) {
548                 values.put(Sms.DATE, new Long(System.currentTimeMillis()));
549             }
550 
551             if (addType && (type != Sms.MESSAGE_TYPE_ALL)) {
552                 values.put(Sms.TYPE, Integer.valueOf(type));
553             }
554 
555             // thread_id
556             Long threadId = values.getAsLong(Sms.THREAD_ID);
557             String address = values.getAsString(Sms.ADDRESS);
558 
559             if (((threadId == null) || (threadId == 0)) && (!TextUtils.isEmpty(address))) {
560                 values.put(Sms.THREAD_ID, Threads.getOrCreateThreadId(
561                                    getContext(), address));
562             }
563 
564             // If this message is going in as a draft, it should replace any
565             // other draft messages in the thread.  Just delete all draft
566             // messages with this thread ID.  We could add an OR REPLACE to
567             // the insert below, but we'd have to query to find the old _id
568             // to produce a conflict anyway.
569             if (values.getAsInteger(Sms.TYPE) == Sms.MESSAGE_TYPE_DRAFT) {
570                 db.delete(TABLE_SMS, "thread_id=? AND type=?",
571                         new String[] { values.getAsString(Sms.THREAD_ID),
572                                        Integer.toString(Sms.MESSAGE_TYPE_DRAFT) });
573             }
574 
575             if (type == Sms.MESSAGE_TYPE_INBOX) {
576                 // Look up the person if not already filled in.
577                 if ((values.getAsLong(Sms.PERSON) == null) && (!TextUtils.isEmpty(address))) {
578                     Cursor cursor = null;
579                     Uri uri = Uri.withAppendedPath(Contacts.Phones.CONTENT_FILTER_URL,
580                             Uri.encode(address));
581                     try {
582                         cursor = getContext().getContentResolver().query(
583                                 uri,
584                                 CONTACT_QUERY_PROJECTION,
585                                 null, null, null);
586 
587                         if (cursor.moveToFirst()) {
588                             Long id = Long.valueOf(cursor.getLong(PERSON_ID_COLUMN));
589                             values.put(Sms.PERSON, id);
590                         }
591                     } catch (Exception ex) {
592                         Log.e(TAG, "insert: query contact uri " + uri + " caught ", ex);
593                     } finally {
594                         if (cursor != null) {
595                             cursor.close();
596                         }
597                     }
598                 }
599             } else {
600                 // Mark all non-inbox messages read.
601                 values.put(Sms.READ, ONE);
602             }
603             if (ProviderUtil.shouldSetCreator(values, callerUid)) {
604                 // Only SYSTEM or PHONE can set CREATOR
605                 // If caller is not SYSTEM or PHONE, or SYSTEM or PHONE does not set CREATOR
606                 // set CREATOR using the truth on caller.
607                 // Note: Inferring package name from UID may include unrelated package names
608                 values.put(Sms.CREATOR, callerPkg);
609             }
610         } else {
611             if (initialValues == null) {
612                 values = new ContentValues(1);
613             } else {
614                 values = initialValues;
615             }
616         }
617 
618         rowID = db.insert(table, "body", values);
619 
620         // Don't use a trigger for updating the words table because of a bug
621         // in FTS3.  The bug is such that the call to get the last inserted
622         // row is incorrect.
623         if (table == TABLE_SMS) {
624             // Update the words table with a corresponding row.  The words table
625             // allows us to search for words quickly, without scanning the whole
626             // table;
627             ContentValues cv = new ContentValues();
628             cv.put(Telephony.MmsSms.WordsTable.ID, rowID);
629             cv.put(Telephony.MmsSms.WordsTable.INDEXED_TEXT, values.getAsString("body"));
630             cv.put(Telephony.MmsSms.WordsTable.SOURCE_ROW_ID, rowID);
631             cv.put(Telephony.MmsSms.WordsTable.TABLE_ID, 1);
632             db.insert(TABLE_WORDS, Telephony.MmsSms.WordsTable.INDEXED_TEXT, cv);
633         }
634         if (rowID > 0) {
635             Uri uri = Uri.withAppendedPath(url, String.valueOf(rowID));
636             if (Log.isLoggable(TAG, Log.VERBOSE)) {
637                 Log.d(TAG, "insert " + uri + " succeeded");
638             }
639             return uri;
640         } else {
641             Log.e(TAG, "insert: failed!");
642         }
643 
644         return null;
645     }
646 
647     @Override
delete(Uri url, String where, String[] whereArgs)648     public int delete(Uri url, String where, String[] whereArgs) {
649         int count;
650         int match = sURLMatcher.match(url);
651         SQLiteDatabase db = getWritableDatabase(match);
652         boolean notifyIfNotDefault = true;
653         switch (match) {
654             case SMS_ALL:
655                 count = db.delete(TABLE_SMS, where, whereArgs);
656                 if (count != 0) {
657                     // Don't update threads unless something changed.
658                     MmsSmsDatabaseHelper.updateThreads(db, where, whereArgs);
659                 }
660                 break;
661 
662             case SMS_ALL_ID:
663                 try {
664                     int message_id = Integer.parseInt(url.getPathSegments().get(0));
665                     count = MmsSmsDatabaseHelper.deleteOneSms(db, message_id);
666                 } catch (Exception e) {
667                     throw new IllegalArgumentException(
668                         "Bad message id: " + url.getPathSegments().get(0));
669                 }
670                 break;
671 
672             case SMS_CONVERSATIONS_ID:
673                 int threadID;
674 
675                 try {
676                     threadID = Integer.parseInt(url.getPathSegments().get(1));
677                 } catch (Exception ex) {
678                     throw new IllegalArgumentException(
679                             "Bad conversation thread id: "
680                             + url.getPathSegments().get(1));
681                 }
682 
683                 // delete the messages from the sms table
684                 where = DatabaseUtils.concatenateWhere("thread_id=" + threadID, where);
685                 count = db.delete(TABLE_SMS, where, whereArgs);
686                 MmsSmsDatabaseHelper.updateThread(db, threadID);
687                 break;
688 
689             case SMS_RAW_MESSAGE:
690                 ContentValues cv = new ContentValues();
691                 cv.put("deleted", 1);
692                 count = db.update(TABLE_RAW, cv, where, whereArgs);
693                 if (Log.isLoggable(TAG, Log.VERBOSE)) {
694                     Log.d(TAG, "delete: num rows marked deleted in raw table: " + count);
695                 }
696                 notifyIfNotDefault = false;
697                 break;
698 
699             case SMS_RAW_MESSAGE_PERMANENT_DELETE:
700                 count = db.delete(TABLE_RAW, where, whereArgs);
701                 if (Log.isLoggable(TAG, Log.VERBOSE)) {
702                     Log.d(TAG, "delete: num rows permanently deleted in raw table: " + count);
703                 }
704                 notifyIfNotDefault = false;
705                 break;
706 
707             case SMS_STATUS_PENDING:
708                 count = db.delete("sr_pending", where, whereArgs);
709                 break;
710 
711             case SMS_ICC:
712                 String messageIndexString = url.getPathSegments().get(1);
713 
714                 return deleteMessageFromIcc(messageIndexString);
715 
716             default:
717                 throw new IllegalArgumentException("Unknown URL");
718         }
719 
720         if (count > 0) {
721             notifyChange(notifyIfNotDefault, url, getCallingPackage());
722         }
723         return count;
724     }
725 
726     /**
727      * Delete the message at index from ICC.  Return true iff
728      * successful.
729      */
deleteMessageFromIcc(String messageIndexString)730     private int deleteMessageFromIcc(String messageIndexString) {
731         SmsManager smsManager = SmsManager.getDefault();
732         // Use phone id to avoid AppOps uid mismatch in telephony
733         long token = Binder.clearCallingIdentity();
734         try {
735             return smsManager.deleteMessageFromIcc(
736                     Integer.parseInt(messageIndexString))
737                     ? 1 : 0;
738         } catch (NumberFormatException exception) {
739             throw new IllegalArgumentException(
740                     "Bad SMS ICC ID: " + messageIndexString);
741         } finally {
742             ContentResolver cr = getContext().getContentResolver();
743             cr.notifyChange(ICC_URI, null, true, UserHandle.USER_ALL);
744 
745             Binder.restoreCallingIdentity(token);
746         }
747     }
748 
749     @Override
update(Uri url, ContentValues values, String where, String[] whereArgs)750     public int update(Uri url, ContentValues values, String where, String[] whereArgs) {
751         final int callerUid = Binder.getCallingUid();
752         final String callerPkg = getCallingPackage();
753         int count = 0;
754         String table = TABLE_SMS;
755         String extraWhere = null;
756         boolean notifyIfNotDefault = true;
757         int match = sURLMatcher.match(url);
758         SQLiteDatabase db = getWritableDatabase(match);
759 
760         switch (match) {
761             case SMS_RAW_MESSAGE:
762                 table = TABLE_RAW;
763                 notifyIfNotDefault = false;
764                 break;
765 
766             case SMS_STATUS_PENDING:
767                 table = TABLE_SR_PENDING;
768                 break;
769 
770             case SMS_ALL:
771             case SMS_FAILED:
772             case SMS_QUEUED:
773             case SMS_INBOX:
774             case SMS_SENT:
775             case SMS_DRAFT:
776             case SMS_OUTBOX:
777             case SMS_CONVERSATIONS:
778                 break;
779 
780             case SMS_ALL_ID:
781                 extraWhere = "_id=" + url.getPathSegments().get(0);
782                 break;
783 
784             case SMS_INBOX_ID:
785             case SMS_FAILED_ID:
786             case SMS_SENT_ID:
787             case SMS_DRAFT_ID:
788             case SMS_OUTBOX_ID:
789                 extraWhere = "_id=" + url.getPathSegments().get(1);
790                 break;
791 
792             case SMS_CONVERSATIONS_ID: {
793                 String threadId = url.getPathSegments().get(1);
794 
795                 try {
796                     Integer.parseInt(threadId);
797                 } catch (Exception ex) {
798                     Log.e(TAG, "Bad conversation thread id: " + threadId);
799                     break;
800                 }
801 
802                 extraWhere = "thread_id=" + threadId;
803                 break;
804             }
805 
806             case SMS_STATUS_ID:
807                 extraWhere = "_id=" + url.getPathSegments().get(1);
808                 break;
809 
810             default:
811                 throw new UnsupportedOperationException(
812                         "URI " + url + " not supported");
813         }
814 
815         if (table.equals(TABLE_SMS) && ProviderUtil.shouldRemoveCreator(values, callerUid)) {
816             // CREATOR should not be changed by non-SYSTEM/PHONE apps
817             Log.w(TAG, callerPkg + " tries to update CREATOR");
818             values.remove(Sms.CREATOR);
819         }
820 
821         where = DatabaseUtils.concatenateWhere(where, extraWhere);
822         count = db.update(table, values, where, whereArgs);
823 
824         if (count > 0) {
825             if (Log.isLoggable(TAG, Log.VERBOSE)) {
826                 Log.d(TAG, "update " + url + " succeeded");
827             }
828             notifyChange(notifyIfNotDefault, url, callerPkg);
829         }
830         return count;
831     }
832 
notifyChange(boolean notifyIfNotDefault, Uri uri, final String callingPackage)833     private void notifyChange(boolean notifyIfNotDefault, Uri uri, final String callingPackage) {
834         final Context context = getContext();
835         ContentResolver cr = context.getContentResolver();
836         cr.notifyChange(uri, null, true, UserHandle.USER_ALL);
837         cr.notifyChange(MmsSms.CONTENT_URI, null, true, UserHandle.USER_ALL);
838         cr.notifyChange(Uri.parse("content://mms-sms/conversations/"), null, true,
839                 UserHandle.USER_ALL);
840         if (notifyIfNotDefault) {
841             ProviderUtil.notifyIfNotDefaultSmsApp(uri, callingPackage, context);
842         }
843     }
844 
845     // Db open helper for tables stored in CE(Credential Encrypted) storage.
846     @VisibleForTesting
847     public SQLiteOpenHelper mCeOpenHelper;
848     // Db open helper for tables stored in DE(Device Encrypted) storage. It's currently only used
849     // to store raw table.
850     @VisibleForTesting
851     public SQLiteOpenHelper mDeOpenHelper;
852 
853     private final static String TAG = "SmsProvider";
854     private final static String VND_ANDROID_SMS = "vnd.android.cursor.item/sms";
855     private final static String VND_ANDROID_SMSCHAT =
856             "vnd.android.cursor.item/sms-chat";
857     private final static String VND_ANDROID_DIR_SMS =
858             "vnd.android.cursor.dir/sms";
859 
860     private static final String[] sIDProjection = new String[] { "_id" };
861 
862     private static final int SMS_ALL = 0;
863     private static final int SMS_ALL_ID = 1;
864     private static final int SMS_INBOX = 2;
865     private static final int SMS_INBOX_ID = 3;
866     private static final int SMS_SENT = 4;
867     private static final int SMS_SENT_ID = 5;
868     private static final int SMS_DRAFT = 6;
869     private static final int SMS_DRAFT_ID = 7;
870     private static final int SMS_OUTBOX = 8;
871     private static final int SMS_OUTBOX_ID = 9;
872     private static final int SMS_CONVERSATIONS = 10;
873     private static final int SMS_CONVERSATIONS_ID = 11;
874     private static final int SMS_RAW_MESSAGE = 15;
875     private static final int SMS_ATTACHMENT = 16;
876     private static final int SMS_ATTACHMENT_ID = 17;
877     private static final int SMS_NEW_THREAD_ID = 18;
878     private static final int SMS_QUERY_THREAD_ID = 19;
879     private static final int SMS_STATUS_ID = 20;
880     private static final int SMS_STATUS_PENDING = 21;
881     private static final int SMS_ALL_ICC = 22;
882     private static final int SMS_ICC = 23;
883     private static final int SMS_FAILED = 24;
884     private static final int SMS_FAILED_ID = 25;
885     private static final int SMS_QUEUED = 26;
886     private static final int SMS_UNDELIVERED = 27;
887     private static final int SMS_RAW_MESSAGE_PERMANENT_DELETE = 28;
888 
889     private static final UriMatcher sURLMatcher =
890             new UriMatcher(UriMatcher.NO_MATCH);
891 
892     static {
893         sURLMatcher.addURI("sms", null, SMS_ALL);
894         sURLMatcher.addURI("sms", "#", SMS_ALL_ID);
895         sURLMatcher.addURI("sms", "inbox", SMS_INBOX);
896         sURLMatcher.addURI("sms", "inbox/#", SMS_INBOX_ID);
897         sURLMatcher.addURI("sms", "sent", SMS_SENT);
898         sURLMatcher.addURI("sms", "sent/#", SMS_SENT_ID);
899         sURLMatcher.addURI("sms", "draft", SMS_DRAFT);
900         sURLMatcher.addURI("sms", "draft/#", SMS_DRAFT_ID);
901         sURLMatcher.addURI("sms", "outbox", SMS_OUTBOX);
902         sURLMatcher.addURI("sms", "outbox/#", SMS_OUTBOX_ID);
903         sURLMatcher.addURI("sms", "undelivered", SMS_UNDELIVERED);
904         sURLMatcher.addURI("sms", "failed", SMS_FAILED);
905         sURLMatcher.addURI("sms", "failed/#", SMS_FAILED_ID);
906         sURLMatcher.addURI("sms", "queued", SMS_QUEUED);
907         sURLMatcher.addURI("sms", "conversations", SMS_CONVERSATIONS);
908         sURLMatcher.addURI("sms", "conversations/*", SMS_CONVERSATIONS_ID);
909         sURLMatcher.addURI("sms", "raw", SMS_RAW_MESSAGE);
910         sURLMatcher.addURI("sms", "raw/permanentDelete", SMS_RAW_MESSAGE_PERMANENT_DELETE);
911         sURLMatcher.addURI("sms", "attachments", SMS_ATTACHMENT);
912         sURLMatcher.addURI("sms", "attachments/#", SMS_ATTACHMENT_ID);
913         sURLMatcher.addURI("sms", "threadID", SMS_NEW_THREAD_ID);
914         sURLMatcher.addURI("sms", "threadID/*", SMS_QUERY_THREAD_ID);
915         sURLMatcher.addURI("sms", "status/#", SMS_STATUS_ID);
916         sURLMatcher.addURI("sms", "sr_pending", SMS_STATUS_PENDING);
917         sURLMatcher.addURI("sms", "icc", SMS_ALL_ICC);
918         sURLMatcher.addURI("sms", "icc/#", SMS_ICC);
919         //we keep these for not breaking old applications
920         sURLMatcher.addURI("sms", "sim", SMS_ALL_ICC);
921         sURLMatcher.addURI("sms", "sim/#", SMS_ICC);
922     }
923 
924     /**
925      * These methods can be overridden in a subclass for testing SmsProvider using an
926      * in-memory database.
927      */
getReadableDatabase(int match)928     SQLiteDatabase getReadableDatabase(int match) {
929         return getDBOpenHelper(match).getReadableDatabase();
930     }
931 
getWritableDatabase(int match)932     SQLiteDatabase getWritableDatabase(int match) {
933         return  getDBOpenHelper(match).getWritableDatabase();
934     }
935 }
936