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