Home | History | Annotate | Download | only in telephony
      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
     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      */
    105     public static String getSmsTable(boolean accessRestricted) {
    106         return accessRestricted ? VIEW_SMS_RESTRICTED : TABLE_SMS;
    107     }
    108 
    109     @Override
    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 
    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 
    282     private SQLiteOpenHelper getDBOpenHelper(int match) {
    283         if (match == SMS_RAW_MESSAGE) {
    284             return mDeOpenHelper;
    285         }
    286         return mCeOpenHelper;
    287     }
    288 
    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      */
    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      */
    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 
    367     private Cursor withIccNotificationUri(Cursor cursor) {
    368         cursor.setNotificationUri(getContext().getContentResolver(), ICC_URI);
    369         return cursor;
    370     }
    371 
    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 
    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
    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
    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
    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 
    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
    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      */
    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
    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 
    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