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