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
17package com.android.providers.telephony;
18
19import android.app.AppOpsManager;
20import android.content.ContentProvider;
21import android.content.ContentResolver;
22import android.content.ContentValues;
23import android.content.UriMatcher;
24import android.database.Cursor;
25import android.database.DatabaseUtils;
26import android.database.MatrixCursor;
27import android.database.sqlite.SQLiteDatabase;
28import android.database.sqlite.SQLiteOpenHelper;
29import android.database.sqlite.SQLiteQueryBuilder;
30import android.net.Uri;
31import android.os.Binder;
32import android.os.UserHandle;
33import android.provider.Contacts;
34import android.provider.Telephony;
35import android.provider.Telephony.MmsSms;
36import android.provider.Telephony.Sms;
37import android.provider.Telephony.TextBasedSmsColumns;
38import android.provider.Telephony.Threads;
39import android.telephony.SmsManager;
40import android.telephony.SmsMessage;
41import android.text.TextUtils;
42import android.util.Log;
43
44import java.util.ArrayList;
45import java.util.HashMap;
46
47public 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