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