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